Skip to content

fix: per-engine log_prefix — stop clobbering the process-global logger - #173

Merged
Liam0205 merged 24 commits into
masterfrom
fix/172-per-engine-log-prefix
Jul 19, 2026
Merged

fix: per-engine log_prefix — stop clobbering the process-global logger#173
Liam0205 merged 24 commits into
masterfrom
fix/172-per-engine-log-prefix

Conversation

@Liam0205

Copy link
Copy Markdown
Owner

Closes #172

Summary

log_prefix was applied through process-global state guarded by set-once semantics: Go called log.SetPrefix() behind a package-level sync.Once, Java CAS-set a pine.log.prefix System property (which nothing ever read back). With the v0.10.13 embedding API, multiple engines per process are a first-class setup — and whichever engine was constructed first won the prefix while every other engine's configured log_prefix was silently ignored, misattributing its log lines.

log_prefix is now engine-instance-scoped across all three runtimes:

  • pine-go — each Engine owns a *log.Logger (prefix from WithLogPrefix > JSON log_prefix, Ldate|Ltime|Lshortfile flags); the global log package is never touched. Consumers: new LoggerAware/LoggerHolder operator injection (DebugHolder embeds it, so DebugLog and Logf report the operator call site via calldepth 3), scheduler [pine-debug] snapshot lines via runtime.Plan.Logger, observe_log / transform_redis_set diagnostics, and Engine.Logger() for embedders.
  • pine-javaEngine stores the resolved prefix (exposed via logPrefix()), injects it into LoggerAware operators (AbstractOperator implements it with a logf helper), and prepends it to [pine-debug] lines. The dead System-property channel is gone.
  • pine-cpp — the per-instance log_prefix_ (already stored, never consumed) is now actually consumed: run_dag prepends it to [pine-debug] lines, and a new LoggerAware interface feeds it to observe_log.

The observe_log schema description ("writes them to Go standard log") was updated in all three schema sources and doc/operators/ regenerated — cross-engine codegen byte-parity holds.

Testing

  • Go: prefix-from-JSON / option-override tests now assert the engine logger and that the global logger stays untouched; new TestLogPrefixPerEngineIsolation pins that construction order no longer decides whose prefix wins. Full go test ./... green.
  • Java: new LogPrefixTest (per-engine isolation + option override + property channel stays unset). Full suite 295 green.
  • C++: new doctest case for two-engine prefix isolation; ctest green.
  • Cross-validate sections 1/3/5/6/13 (codegen schema + markdown byte-parity, execution, error, server, metrics) green; make lint and make fuzz clean.

Compatibility

Single-engine processes see identical log output (same prefix, same flags, same lines). The only observable change is intentional: log lines and log.Prefix() no longer leak the engine prefix into the process-global logger, and multi-engine processes get correct per-engine attribution.

Liam0205 added 5 commits July 18, 2026 22:28
…er (issue #172)

log_prefix was applied via log.SetPrefix() behind a package-level
sync.Once: process-global and first-engine-wins. With the v0.10.13
embedding API multiple engines per process are a first-class setup,
where every other engine's configured prefix was silently ignored and
its log lines carried a foreign prefix.

Each Engine now owns a *log.Logger (prefix from WithLogPrefix > JSON
log_prefix, same Ldate|Ltime|Lshortfile flags); the global log package
is no longer touched. Consumers:

- new LoggerAware/LoggerHolder operator injection (DebugHolder embeds
  LoggerHolder, so DebugAware operators get it for free); Logf and
  DebugLog use calldepth 3 so Lshortfile reports the operator call
  site, not the wrapper.
- observe_log and transform_redis_set emit through the engine logger.
- runtime.Plan.Logger carries it to the scheduler's [pine-debug]
  snapshot lines.
- Engine.Logger() exposes it for embedders that want per-flow lines
  consistent with engine diagnostics.

Tests: prefix-from-JSON / option-override now assert the engine logger
and that the global logger is untouched; new per-engine isolation test
pins that construction order no longer decides whose prefix wins.
export_test.go's ResetLogOnce helper is gone with the sync.Once.
#172)

The old path CAS-set a System property (pine.log.prefix) once per
process — first-engine-wins with a stderr warning for later engines,
and nothing in the codebase ever read the property back (dead state).

Engine now stores its resolved prefix (option > JSON config) as an
instance field exposed via logPrefix(), injects it into LoggerAware
operators (AbstractOperator implements it and provides the logf
helper), and prepends it to [pine-debug] snapshot lines. observe_log,
transform_redis_set and transform_by_lua diagnostics now carry the
owning engine's prefix. No global state is touched.

LogPrefixTest mirrors the Go per-engine isolation test and pins that
the System property channel stays unset. Full suite: 295 green.
…ware (issue #172)

pine-cpp already stored log_prefix per engine instance but nothing in
the log paths consumed it. run_dag now takes the engine's prefix and
prepends it to [pine-debug] snapshot lines; new LoggerAware interface
(mirroring Go/Java) lets operators receive the engine prefix —
observe_log implements it and prepends the prefix to its output.
Schema description updated away from the Go-specific wording. New
doctest case pins two-engine prefix isolation.
…rd log

The operator now writes through the engine's logger (issue #172); Go
schema source updated and doc/operators regenerated. Java/C++ schema
strings were aligned in their respective commits.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 审查

项目 结果
结论 ⚠️ REQUEST_CHANGES
审查截止 b15f0e1473367b314cfa5ac41f0f1c2af7ca578c

整体方向正确:把 log_prefix 从进程全局的 set-once 状态改成引擎实例级,三个运行时对齐,测试覆盖到了「构造顺序不再决定谁的前缀生效」这一核心回归点。有一处 calldepth off-by-one 会让 [pine-debug] 行的 file:line 指错,建议修复后合并。

🟠 重要建议 (2)

1. Plan.logf 的 calldepth 应为 2,而非 3

  • 文件: pine-go/internal/runtime/scheduler.go 代码链接
  • 问题: logf 是直接封装 Output单层 wrapper,但传入了 calldepth=3。按标准库 log.Logger.Output 契约(「a value of 1 will print the details for the caller of Output」):depth 1 = logf 自身、depth 2 = logf 的调用方(即 scheduler.go:308 那行)。而 logf 是在 go func(idx int) 协程体内直接调用的,depth 3 会越过协程体指向其运行时父帧(runtime.goexit),于是 Lshortfile 会打出类似 asm_*.s/proc.go 的位置。这相较改动前直接用 log.Printf(能正确打出 scheduler 行)是一处回归,也与本函数注释声明的意图相悖。
  • 对比: LoggerHolder.Logf / DebugHolder.DebugLog3 是对的,因为它们经过了两层 wrapper(Logf/DebugLoglogOutputOutput)。Plan.logf 只有一层,应为 2
  • 建议: 两处 Output(3, ...) 改为 Output(2, ...),并同步更新上面的注释。

2. Java 把用户配置的前缀拼进 printf 格式串,含 % 会抛异常

  • 文件: pine-java/.../AbstractOperator.java 代码链接Engine.java 代码链接
  • 问题: System.err.printf(engineLogPrefix + format + "%n", args)engineLogPrefix(以及 Engine 里的 logPrefix)直接拼进格式串。若前缀里含 %(如 "[100%] "),会在运行时抛 UnknownFormatConversionException / MissingFormatArgumentException。Go 侧 log.New 把 prefix 当字面量、C++ 侧用 <<,都不受影响;只有 Java 引入了这个隐患。
  • 建议: 前缀不要进格式串。例如 System.err.print(engineLogPrefix); System.err.printf(format + "%n", args);,或把前缀作为 %s 实参传入。

🟢 小问题 (1)

  • 文件: pine-java/.../Engine.java 代码链接
  • 问题: 删除 logPrefixSet 字段后,import java.util.concurrent.atomic.AtomicBoolean; 已无引用,属残留 import,建议移除。

Liam0205 added 2 commits July 18, 2026 23:02
logf wraps Output directly (one layer), so depth 2 reports logf's
caller — the scheduler [pine-debug] line. Depth 3 walked past the
goroutine body into the runtime parent frame, printing asm/proc.go
locations instead. LoggerHolder.Logf keeps 3 because it goes through
two layers (Logf -> logOutput -> Output).
logf and the [pine-debug] line concatenated the user-configured
log_prefix into the format string; a literal '%' in the prefix (e.g.
"[100%] ") would throw UnknownFormatConversionException at runtime.
The prefix is now emitted as a literal (print / %s argument). Also
drop the AtomicBoolean import left over from the removed set-once
guard.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 b15f0e1473367b314cfa5ac41f0f1c2af7ca578c..00526343e30e1bfc9c30a2637f13c8ad749f40d9
审查截止 00526343e30e1bfc9c30a2637f13c8ad749f40d9

上一轮提出的 3 个问题已全部修复且修复方式正确,可以合并。

✅ 已修复项复核

1. Plan.logf calldepth 3 → 2(已修复)

  • 文件: pine-go/internal/runtime/scheduler.go 代码链接
  • logf 直接封装 Output(单层),改为 Output(2, ...) 后:depth 1 = logf 自身,depth 2 = logf 的调用方,即 scheduler.go:309 那行。Lshortfile 现在能正确指向 scheduler 行,注释也同步更新并说明了与 LoggerHolder.Logf(两层 wrapper 用 3)的区别。修复正确。

2. Java 用户前缀不再拼进 printf 格式串(已修复)

  • 文件: pine-java/.../AbstractOperator.java 代码链接Engine.java 代码链接
  • AbstractOperator.logf 改为先 System.err.print(engineLogPrefix)printf(format + "%n", args)Engine[pine-debug] 行改用 "%s..."logPrefix 作为实参传入。含 % 的前缀(如 "[100%] ")不再触发 UnknownFormatConversionException。修复正确。

3. 残留 AtomicBoolean import(已移除)

  • 文件: pine-java/.../Engine.java
  • 确认 Engine.java 中已无 AtomicBoolean / logPrefixSet 引用,import 已删除。

Reflection per-engine-log-prefix.md: #172 as the third instance of
'new public entrypoint turns old global state into a defect' (after
the #169 stop-reachability and Addr-copy regressions), parity audits
must trace to consumption points (a stored-but-never-read value is
invisible to existence checks — log_prefix had drifted into three
different wrong shapes), calldepth is a function of wrapper layers,
user-controlled strings never enter printf format strings, and schema
description edits fan out to three schema sources + codegen.

Promotions: conventions.md adds the consumption-point audit dimension
and rewrites the global-side-effect note to ask ownership first;
standard-workflow.md adds the global-state ownership audit;
operator-contract.md documents the LoggerAware operator logging
contract (three-runtime table, printf isolation rule, injection order
now four-stage). index synced.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 00526343e3..c7c129d3ea
审查截止 c7c129d3ea9589ba2e5ccb814d553034d6deed54

自上次审查(截止 0052634)以来仅新增一个 llmdoc 反思提交 c7c129d,纯文档改动,无代码变更。内容准确、结论 APPROVE。

✅ 增量内容复核(文档)

改动范围llmdoc/ 5 个文件,均为 issue #172 复盘沉淀,无任何运行时代码变更。

  • 新增 llmdoc/memory/reflections/per-engine-log-prefix.md —— 记录三引擎三种错误形态(Go sync.Once first-engine-wins / Java System property 无消费者 / C++ 成员无消费者)、消费点审计盲区、calldepth 逐层推导、printf 格式串注入两处 review 命中缺陷。与实际 commit 链(d061e7b981e86ef2f9da078b4ab804d4,review 修复 8e8a6913+00526343)一致。
  • must/conventions.md 新增「消费点追踪」审计维度、重写「全局副作用保护」为先问归属。
  • guides/standard-workflow.md 新增「新公开入口上线后审计既有全局状态归属」条目。
  • reference/operator-contract.md 新增 LoggerAware 算子日志规范(三引擎入口表 + printf 隔离规则 + 注入顺序四段)。
  • index.md 同步以上指针。

事实性抽查(对照源码)

代码层面的两处 review 缺陷(Go Plan.logf calldepth、Java printf 格式串)已在上一轮确认修复正确,本轮无需重复审查。文档如实沉淀了这些教训,可以合并。

Liam0205 added 4 commits July 19, 2026 00:02
engineOptions.logPrefix used the empty string for both 'option not
passed' and 'explicitly set to empty', so WithLogPrefix("") fell back
to the JSON prefix — violating the documented option-over-JSON
precedence and diverging from Java (nullable String) and C++
(std::optional), where the same call yields an empty prefix. Now
*string, mirroring WithDebug: nil falls back to JSON, an explicit
empty string wins. Test pins non-empty JSON + empty option -> empty.
Splitting logf into print(prefix) + printf(body) reopened a
concurrency window: PrintStream only serializes within a single call,
so another engine's line could land between the two writes and produce
'[engine-a] [engine-b] message' — breaking exactly the per-engine
attribution this feature guarantees. Now the body is formatted first
(prefix still never enters the format string) and prefix + body +
newline go out in ONE print call.

New tests: explicit empty option overrides JSON prefix (tri-state
parity), and a concurrency test that captures real stderr from two
engines under parallel execute and asserts no interleaved or doubled
prefixes.
…iew)

std::optional already had the right tri-state semantics; the new case
locks it against regressions and mirrors the Go/Java tests.
dag-engine.md still described WithLogPrefix as touching the process
logger on one line while the section below said engine-scoped, and the
compile-pipeline injection order missed LoggerAware. metrics-
observability.md, pine-cpp-runtime.md, design_doc/08_observability.md
and design_doc/06_json_config.md all still said log.SetPrefix /
process-global. All now state the engine-instance semantics (tri-state
option, per-runtime accessors, LoggerAware consumption paths);
08_observability keeps a one-line historical note.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 c7c129d..efbd43f
审查截止 efbd43fdb8805dff5197e45205a703eaf01d3ba7

自上次审查(截止 c7c129d)以来新增 4 个提交,均为 review 修复与文档同步,改动小且正确,可以合并。

✅ 增量内容复核

1. WithLogPrefix 改为 nullable 三态(004befd,已正确)

  • 文件: pine-go/pine.go 代码链接
  • engineOptions.logPrefixstring 改为 *stringWithLogPrefix&prefix;解析逻辑 pine.go:89-91 改为默认取 cfg.LogPrefixeo.logPrefix != nil 时才覆盖。修复了此前空串既表示「未设置」又表示「显式设空」的二义性,与 Java nullable String、C++ std::optional 对齐,也与 WithDebug*bool 写法一致。已核对全部 logPrefix 引用(38/57/89-93 行)类型一致,无残留 string 用法。新增 TestLogPrefixEmptyOptionOverridesJSON 钉住「非空 JSON + 空串 option → 空前缀」。

2. Java logf 单次写出(7751741,已正确)

  • 文件: AbstractOperator.java 代码链接
  • 上一轮把 logf 拆成 print(prefix) + printf(body) 两次调用,重新打开了并发交错窗口(PrintStream 只在单次调用内串行化,两次调用间可能插入另一引擎的输出,产生 [engine-a] [engine-b] ...)。现改为先 String.format(format, args) 格式化 body,再 prefix + body + lineSeparator() 一次 print 写出。前缀仍不进 format 串,含 % 的前缀不会抛异常。修复正确。
  • 新增并发测试 concurrentEnginesDoNotInterleavePrefixAndBody:捕获真实 stderr,双引擎各 50 次并行 execute,断言每条 observe_log 行以单一引擎前缀开头且无重复前缀。测试设计合理。

3. C++ 空 option 覆盖 Config(a8609d6,测试补强)

  • 文件: pine-cpp/tests/test_engine.cpp 代码链接
  • std::optional 本就有正确三态语义,新增 doctest 锁定「空串 option 覆盖非空 Config」,与 Go/Java 平行。仅测试新增,无运行时改动。

4. 文档去除进程全局旧措辞(efbd43f,事实核对通过)

  • design_doc/06_json_config.md08_observability.mdllmdoc/architecture/dag-engine.mdpine-cpp-runtime.mdmetrics-observability.md 中残留的 log.SetPrefix() / 进程全局措辞已更新为引擎实例级语义(三态 option、各运行时访问器、LoggerAware 消费路径)。
  • 抽查 dag-engine.md 新增的注入顺序 Metadata → Debug → Logger → Metrics 与实际代码 pine.go:158-181SetMetadataSetDebugInfoSetEngineLoggerSetMetricsProvider)一致。08_observability.md 保留了一行历史说明,符合上轮约定。

代码层面的三态语义与并发写出两处修复均正确,文档如实同步,无新增问题。

Sentinel values must not stand in for 'option unset' (use the type
system — *string/optional, the *bool precedent was in the same file);
a fix must be re-validated against ALL existing constraints of the
code path, not just the one being fixed (printf-injection fix reopened
the concurrency-attribution window); semantic changes require a
repo-wide grep of the doc surface (five docs still said
process-global); concurrency attribution tests must capture real
output.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 efbd43f..b37a5b1
审查截止 b37a5b196fb3730c7138cd0e9fb1c3a5f482402d

自上次审查(截止 efbd43f)以来仅新增 1 个提交 b37a5b1,纯 llmdoc 文档改动,无任何运行时代码变更。内容准确,可以合并。

✅ 增量内容复核(文档)

改动范围:仅 llmdoc/memory/reflections/per-engine-log-prefix.md 追加 11 行,记录第二轮深度 code-review 的 4 项修复复盘,无代码变更。

  • 文件: llmdoc/memory/reflections/per-engine-log-prefix.md 代码链接
  • 新增「第二轮」小节,沉淀 4 条教训:① 可选参数「未设置」必须用类型系统(nullable/optional/指针)表达,不能用值域哨兵值(Go 空串二义性 → 改 *string 对齐 WithDebug*bool);② 修复要在该代码路径的全部既有约束下重新验证(printf 注入修复重开并发归属窗口);③ 改语义须全量 grep 文档面;④ 并发归属断言要捕获真实输出。

事实性抽查

  • 引用的修复 commit SHA 004befd7(Go 三态)、77517416(Java 单次写出)、a8609d61(C++ 测试)、efbd43fd(docs)均与本 PR 实际提交链一致。
  • 教训与前几轮 review 已确认修复正确的两处代码缺陷(Go WithLogPrefix 三态、Java logf 单次原子写出)如实对应,无夸大或偏差。

代码层面的所有修复已在前几轮增量审查中确认正确,本轮为纯文档沉淀,无新增问题。

Liam0205 added 2 commits July 19, 2026 01:06
…(review)

The C++ injection order was Metrics -> Resource -> Logger while Go/Java
inject Logger -> Metrics (-> Resource). No operator currently observes
the difference, but the documented cross-runtime invariant ('logger and
debug context available before provider injection') was not true for
C++. Reordered to Logger -> Metrics -> Resource; metadata/debug still
arrive via init(op_cfg), which precedes all injection.
Four places declared three mutually inconsistent 'fixed injection
orders', and the declared unified order matched no runtime: Go has no
ResourceAware interface at all (resources are ctx-injected at execute
time) and C++ injected Logger last. dag-engine invariant 11 now states
the actual cross-runtime invariant (metadata/debug before providers,
then Logger -> Metrics -> Resource) plus each runtime's real path; the
compile-pipeline line, the Java parity section, operator-contract.md
and metrics-observability.md (both sites) all reference the same
definition instead of restating divergent copies.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 efbd43f..23eac48
审查截止 23eac48b216cfc075b3d8b66f2f9504ba5fec064

自上次审查(截止 efbd43f)以来新增 3 个提交:1 处 C++ 代码改动 + 2 处 llmdoc 文档同步。改动小且正确,已核对源码,可以合并。

✅ 增量内容复核

1. C++ 可选接口注入顺序对齐 Go/Java(4b9831b,已正确)

  • 文件: pine-cpp/src/runtime/engine.cpp 代码链接
  • 此前 C++ 注入顺序是 Metrics → Resource → Logger,把 LoggerAware 排在最后;而 Go/Java 都是 Logger → Metrics (→ Resource)。本次把 set_engine_log_prefix 上移到 set_metrics_provider 之前,得到 init(op_cfg) → Logger → Metrics → Resource,与另外两个运行时的相对顺序一致。
  • 功能上,这些 setter 之间相互独立、都在 execute 之前完成,重排不改变任何算子的可观察行为(commit 亦如实说明「No operator currently observes the difference」);意义在于让代码符合文档声明的跨运行时不变量(logger/debug 上下文在 provider 注入前就位)。注释同步指向 dag-engine.md 不变量 11。修复正确。

2. 注入顺序文档改为「每个运行时一份真实契约」(23eac48,事实核对通过)

  • 文件: llmdoc/architecture/dag-engine.md(不变量 11 + 编译流程行 + Java parity 段)、operator-contract.mdmetrics-observability.md(两处)
  • 修改前四处各自声明了互相矛盾的「固定注入顺序」,且声明的统一顺序与任一运行时都不符(Go 根本没有 ResourceAware 接口——资源在 execute 时经 ctx 注入;C++ 当时把 Logger 排最后)。现文档如实描述各运行时实际路径:
    • Go:Metadata → Debug → Logger → Metrics(无 Resource),核对 pine.go:158-181 一致。
    • Java:Metadata → Debug → Logger → Metrics → Resource,核对 Engine.java:153-182 一致。
    • C++:init(含 metadata/debug) → Logger → Metrics → Resource,与本 PR 代码一致。
  • 其余站点(operator-contract、metrics-observability)改为引用同一份定义而非各自复述,消除了分歧副本。文档表述真实、内部一致。

3. 反思文档补记第二轮 review 教训(b37a5b1,纯文档)

  • 文件: llmdoc/memory/reflections/per-engine-log-prefix.md
  • 补记「哨兵值不能代替 option 未设置」「修一处约束需在全部既有约束下重新验证(printf 修复重开并发窗口)」「改语义须全仓 grep 文档面」「并发归属断言要捕获真实输出」四条教训,与实际 commit 链(004befd7/77517416/a8609d61/efbd43fd)对应准确。

此前三轮 review 提出的代码缺陷(Go Plan.logf calldepth、Java printf 格式串注入与并发交错、WithLogPrefix 三态、残留 import)均已确认修复正确,本轮无新增代码风险。三个运行时 log_prefix 已完全对齐为引擎实例级,可以合并。

Verify per-runtime code before declaring a cross-runtime invariant
(the documented unified order matched no runtime), and keep exactly
one authoritative definition of a contract — restated copies drift
apart structurally; converge them to a single definition plus
references.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 23eac48b216cfc075b3d8b66f2f9504ba5fec064..1807e6dec18136435497647b4c8561241596c76f
审查截止 1807e6dec18136435497647b4c8561241596c76f

本次增量仅一处 docs 改动:llmdoc/memory/reflections/per-engine-log-prefix.md 追加 9 行,记录第三轮注入顺序契约矛盾的复盘。无代码变更。

✅ 复核

  • 文件: llmdoc/memory/reflections/per-engine-log-prefix.md 代码链接
  • 新增「第三轮:注入顺序契约矛盾」小节,记录的两条教训(声明跨运行时不变量前先逐运行时核对代码、同一契约只保留一份权威定义 + 引用)与其引用的修复 commit 4b9831b1(C++ 注入顺序对齐 Logger → Metrics → Resource)、23eac48b(docs 收敛为 dag-engine 不变量 11 单点定义)一致,事实描述准确。
  • 纯文档记录,无 bug、无规范违反。

前几轮提出的 Go calldepth、Java printf % 注入、tri-state 语义、并发归属、C++ 注入顺序等问题均已在对应 commit 修复并复核通过,本 PR 可以合并。

… log prefixes

Three runtime examples for the issue #169/#172 embedding pattern —
one process, several pipelines, each on its own endpoint with its own
log_prefix, and the legacy /execute deliberately retired to 410 Gone:

- pine-go/examples/multi-pipeline: two server.NewServer embedded
  runtimes (hot-reload + refcounted snapshots intact) behind an
  app-owned net/http mux; shared feed.json/search.json configs declare
  "[feed] "/"[search] " prefixes.
- pine-java/examples/MultiPipelineServer.java: two PineServer.load()
  embedded runtimes behind a com.sun.net.httpserver mux, execute()
  against the live snapshot.
- pine-cpp/examples/multi_pipeline_server.cpp: two pine::Engine
  instances behind a deliberately tiny blocking HTTP loop; built by
  default (new CMake target multi_pipeline_server) so it cannot rot.

All three smoke-verified: stderr shows [feed] / [search] prefixed
observe_log lines concurrently from one process, /execute answers 410
with a migration hint. bump-version.sh now also rewrites
_PINEAPPLE_VERSION in pine-go/examples configs; README points at the
examples from the embedding feature bullet.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 1807e6dec18136435497647b4c8561241596c76f..b36f270e03d9384eddd96185d8e712b7577c3715
审查截止 b36f270e03d9384eddd96185d8e712b7577c3715

自上次审查(截止 1807e6d)以来仅新增一个提交 b36f270(docs(examples)),为三运行时补充「一进程多 pipeline 多 endpoint、各自独立 log_prefix」示例,配套 README、CMake 目标与 bump-version 脚本调整。无运行时代码变更。逐项核对 API、配置与脚本均正确,可以合并。

✅ 增量内容复核

改动范围pine-go/examples/multi-pipeline/(main.go + feed.json + search.json)、pine-java/examples/MultiPipelineServer.javapine-cpp/examples/multi_pipeline_server.cppREADME.mdpine-cpp/CMakeLists.txtscripts/bump-version.sh

API 逐项核对(对照源码)

  • Go main.goserver.NewServer(server.Config{ConfigPath})Server.Execute(ctx, *pine.Request)Server.Close()pine.Request{Common, Items}pine.Result{Common, Items}pine.ValidationError 均与 pkg/server/server.go:322/522/567internal/types/request.go 一致。示例位于 pine-go 模块内,go build ./... 可覆盖。
  • Java MultiPipelineServer.javanew PineServer(config, 0) + load()(不 start(),端口 0 仅存储不绑定,PineServer.java:125/154 的 javadoc 明确 embedding 入口无需 HTTP)、execute(common, items) 返回 Engine.Result{common, items}Engine.java:708-710)、PineErrors.ValidationError 均存在。
  • C++ multi_pipeline_server.cpppine::Engine(load_config_from_file(...))engine.execute(request)result_to_jsonengine.close()parse_json/as_object/as_arrayVariant::object_tValidationError 均与 include/pine/pine.hpp 一致。const auto root = parse_json(body).as_object();auto 按值推导为 object_t 拷贝,不会悬垂引用临时对象。新增 CMake 目标 multi_pipeline_server 默认构建,编译错误会被 CI 拦截。

配置有效性feed.json/search.json 顶层声明引擎级 log_prefix"[feed] "/"[search] "),算子 recall_static/observe_log/reorder_sort/filter_truncate 均为标准算子;observe_log 的算子级 log_prefix 参数存在(operators/observe/log.go:32),与引擎级前缀是两个独立概念,叠加输出符合预期。

脚本bump-version.sh 新增 pine-go/examples/**/*.json glob,脚本第 24 行已 shopt -s globstar** 能正确递归匹配到 examples/multi-pipeline/*.json

前几轮的 Go calldepth、Java printf % 注入、tri-state 语义、并发归属、C++ 注入顺序等问题均已在对应 commit 修复并复核通过。本轮纯示例/文档,无 bug、无规范违反。

Liam0205 added 3 commits July 19, 2026 10:32
The embedded example bypassed the bundled server's 10 MB default body
limit with an unbounded io.ReadAll. Issue #169 treats the body cap as a
shared-dispatch-layer safety contract, so the embedding HTTP layer must
keep the boundary itself: wrap the body in http.MaxBytesReader and map
MaxBytesError to 413.
Four gaps versus the bundled PineServer contracts:

- Map Engine.Result.error to HTTP 500 (throw/return split: validation
  throws, operator failures are returned). PanicError logs its detailed
  stack server-side while clients only see the safe message.
- Enforce the 10 MB request-body cap with a counting reader; over-limit
  bodies answer 413 instead of growing memory unbounded (issue #169
  shared-dispatch-layer safety contract).
- Guard every context with exact-path matching: HttpServer contexts use
  longest-prefix matching, so /api/feed/anything would otherwise run the
  feed pipeline (the issue #169 routing trap, fixed in wrapHandler).
- Make the documented compile/run commands actually work from the repo
  root: build the Maven runtime classpath via dependency:build-classpath
  and point at the real config paths under pine-go/examples/.
- Replace the bare ::write with a ::send loop using MSG_NOSIGNAL,
  handling EINTR and short writes. Every pine-cpp raw-socket write path
  must suppress SIGPIPE (llmdoc/must/conventions.md) — a client
  disconnecting mid-response would otherwise kill the whole process.
- Route error messages through the Variant serializer instead of
  hand-concatenating them into JSON: ValidationError text quotes field
  names, which produced invalid JSON bodies.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 b36f270..1ffdb40
审查截止 1ffdb408f25e1c72fc77b470ec51b8663daeb8e2

自上次审查(截止 b36f270)以来新增 3 个提交,全部是三运行时 multi-pipeline 示例的 HTTP 层加固,无运行时代码变更。逐项对照源码 API 核对无误,可以合并。

✅ 增量内容复核

1. Go:请求体大小上限(180bdcc,正确)

2. Java:三处对齐 bundled PineServer 契约(4043c7b,正确)

  • 文件: pine-java/examples/MultiPipelineServer.java
  • 请求体上限:新增 readLimitedBody(8KB 分块计数,超过 10MB 返回 null → 413),不信任 Content-Length,内存有界。取代原先无界的 readAllBytes()
  • 精确路径匹配:新增 exact() 包装器,因 HttpServer context 采用最长前缀匹配,未加守卫时 /api/feed/anything 会误路由到 feed pipeline;现对子路径返回 404,与 PineServer.wrapHandler 及 Go/C++ 示例的精确匹配对齐(issue docs: serverplus — reference implementation for embedding pine-go in existing HTTP services #169 路由陷阱)。
  • 错误映射:算子失败在 result.error 中返回而非抛出(validation 抛出、execution 返回的 throw/return 分离)。核对 Engine.Result.errorEngine.java:713public final Exception error 存在;error != null → 500PanicErrordetailedError()PineErrors.java:98 在服务端记录堆栈、客户端仅见安全消息。均与 bundled handler 行为一致。
  • javadoc 编译命令改用 mvn dependency:build-classpath 构造真实 classpath 并指向 pine-go/examples/ 下真实配置路径,可从仓库根实际执行。

3. C++:安全 socket 写入 + JSON 转义(1ffdb40,正确)

  • 文件: pine-cpp/examples/multi_pipeline_server.cpp 代码链接
  • send_response 把裸 ::write 换成 ::send(..., MSG_NOSIGNAL) 循环,处理 EINTR 与短写;符合 llmdoc/must/conventions.md「每个裸 socket 写路径必须抑制 SIGPIPE」,避免客户端中途断连打死进程。新增 #include <cerrno>errno/EINTR
  • json_error 改为经 Variant 序列化器输出:Variant::object_t obj; obj["error"] = Variant(message); dump_json(Variant(std::move(obj)), 0)。核对 object_t = ObjectFlatMap<Variant>operator[] 返回 Variant&Variant(std::string) / Variant(object_t) / dump_json(const Variant&, int) 均存在。修复了 ValidationError 消息含引号时手工拼接产生非法 JSON 的问题。CMake 默认目标 multi_pipeline_server 会被 CI 编译拦截。

三个改动均只涉及示例文件,不触碰运行时代码;三处均把 embedding 示例的 HTTP 层对齐到 bundled server 的安全契约(body cap、精确路由、错误映射、SIGPIPE 抑制、JSON 转义)。前几轮的 Go calldepth、Java printf % 注入、tri-state 语义、并发归属、C++ 注入顺序等问题此前已复核通过,本轮无新增风险。

Fourth review round on the multi-pipeline examples found six production
contracts missing from 'demo' code (error mapping, body cap, exact
routing, SIGPIPE suppression, JSON escaping, runnable doc commands).
Append the round-4 lessons to the per-engine-log-prefix reflection and
promote the stable rule — examples are bound by every production
contract, verify the negative space, keep doc commands actually
runnable, build examples by default — into standard-workflow.md.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 1ffdb40..5f86db46
审查截止 5f86db46dceb393993f1eaa8551aa06b3aa29ba5

自上次审查(截止 1ffdb40)以来仅新增一个提交 5f86db4(docs(llmdoc)),为纯文档改动,无任何运行时代码变更。可以合并。

✅ 增量内容复核

改动范围llmdoc/guides/standard-workflow.mdllmdoc/index.mdllmdoc/memory/reflections/per-engine-log-prefix.md 三个文档文件。

内容核对

  • standard-workflow.md 新增「示例代码的契约要求」小节,记录 examples/ 受全部生产契约约束、文档命令须真实执行、冒烟覆盖负空间、示例纳入默认构建防 rot 四条规则,与本 PR 前三轮增量审查(body cap / 精确路由 / 错误映射 / SIGPIPE / JSON 转义 / 可运行文档命令)修复内容一致。
  • per-engine-log-prefix.md 追加「第四轮:multi-pipeline 示例遗漏生产契约」复盘,6 项契约遗漏与 180bdcc/4043c7b/1ffdb40 三个修复 commit 一一对应,描述准确。
  • index.md 同步更新上述两处的一行索引摘要,无遗漏。

三个被引用文件均存在、内容已就位,文档间引用自洽。无 bug、无规范违反。

前几轮的 Go calldepth、Java printf % 注入、tri-state 语义、并发归属、C++ 注入顺序,以及三运行时示例 HTTP 层加固(body cap / 精确路由 / 错误映射 / SIGPIPE / JSON 转义)等问题均已在对应 commit 修复并复核通过。本轮纯文档复盘,本 PR 可以合并。

Liam0205 added 2 commits July 19, 2026 11:15
The C++ example is a default CMake target and the Go example is
compiled by 'go test ./...', but pine-java/examples/ sat outside the
Maven source tree — 'mvn package'/'mvn test' never compiled it, so API
drift would rot the README-recommended example silently.

Attach examples/ as an extra test-source root via
build-helper-maven-plugin: 'mvn test-compile' (and CI's 'mvn test -B')
now compiles QuickStart and MultiPipelineServer; test scope keeps them
out of the library jar, and the surefire *Test name pattern keeps them
out of the test run. Update the example's doc commands to the simpler
test-compile + target/test-classes form (each command verified from a
clean shell).
)

Round 4 promoted 'examples must be in the default build' into
standard-workflow.md while Java itself did not comply; increment-4
review caught it. Record the lesson (a rule-writing commit must audit
all runtimes and fix or note non-compliance on the spot) plus the
minimal Maven approach (build-helper add-test-source), and update the
stable rule with the per-runtime wiring.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 5f86db46..675bbd57
审查截止 675bbd57a82832eb57e76ba4e195ac37715651aa

自上次审查(截止 5f86db46)以来新增 2 个提交:2eb94a9(把 pine-java/examples/ 接入 Maven 默认构建)与 675bbd5(纯 llmdoc 复盘文档)。无任何运行时代码变更,逐项核对无误,可以合并。

✅ 增量内容复核

1. Java:examples/ 纳入默认构建(2eb94a9,正确)

  • 文件: pine-java/pom.xml 代码链接
  • 新增 build-helper-maven-plugin:3.6.0,在 generate-test-sources 阶段以 add-test-source 目标把 examples 挂为额外 test-source root。核对无重复插件声明(全文件仅此一处),插件块位于 <build><plugins> 内、XML 闭合正确。
  • surefire 不会误跑示例QuickStart / MultiPipelineServer 均不匹配 surefire 默认测试类名模式(*Test 等),不会被当测试执行;surefire 配置 仅排除 JazzerFuzzTest,与本改动无冲突。
  • checkstyle 无冲突:checkstyle check 绑定在 validate 阶段(早于 generate-test-sources),示例 source root 此时尚未注入,且插件未开 includeTestSourceDirectory,故不会因示例触发风格失败。
  • test scope 保证示例类不进库 jar(作者已用 unzip -l 验证),选型(add-test-source)相较新建 example module 或手工 javac 更轻,判断合理。

2. Java 示例 javadoc 命令同步(2eb94a9,正确)

  • 文件: pine-java/examples/MultiPipelineServer.java 代码链接
  • 仅改 javadoc 注释:编译命令由 mvn -q package -DskipTests + 手工 javac 改为 mvn -q test-compile + target/test-classes 上 classpath,与 pom 改动一致。package 为 page.liam.pine.examples、目录扁平(examples/*.java),javac 编译显式源文件不强制目录匹配包名,可正常编译到 target/test-classes/page/liam/pine/examples/

3. llmdoc 复盘(675bbd5,纯文档)

  • standard-workflow.md:把「示例纳入默认构建防 rot」一行展开为三运行时各自接法(C++ 默认 CMake target / Go 被 go test ./... 编译 / Java add-test-source),与实际实现一致。
  • per-engine-log-prefix.md:新增「第五轮」复盘,记录「写下规则的 commit 就要让自己合规」教训,与 2eb94a9 修复一一对应,描述准确。

前几轮的 Go calldepth、Java printf % 注入、tri-state 语义、并发归属、C++ 注入顺序,以及三运行时示例 HTTP 层加固(body cap / 精确路由 / 错误映射 / SIGPIPE / JSON 转义)均已在对应 commit 修复并复核通过。本轮为构建配置 + 文档补齐,无新增风险,本 PR 可以合并。

@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 675bbd57..a76536d2
审查截止 a76536d2615ac69fc97fd190666494c80884c548

自上次审查(截止 675bbd57)以来仅新增 1 个提交 a76536d(bump: v0.10.14),为纯版本号提升,无任何运行时逻辑变更。可以合并。

✅ 增量内容复核

改动范围:69 个文件,全部是 0.10.13 → 0.10.14 的单行替换(git diff --stat 显示 69 insertions / 69 deletions,逐文件均为 +1/-1)。

四个 source of truth 一致提升

  • pine-go/version.go 代码链接const Version = "0.10.14"
  • pine-cpp/include/pine/pine.hpp 代码链接kVersion = "0.10.14"
  • pine-java/pom.xml 代码链接<version>0.10.14</version>
  • apple/_version.py 代码链接__version__ = "0.10.14"

其余文件:fixtures/benchmarks、fixtures/pipelines、pine-go/testdata、pine-cpp/tests、pine-java/examples、scripts/*-fuzz.py 中嵌入的 _PINEAPPLE_VERSION 字段同步更新,与四个源头一致。

残留检查git grep "0\.10\.13"(排除文档)在 a76536d2 上无任何命中,版本提升彻底、无遗漏。抽查 version.go / pine.hpp / pom.xml / differential-fuzz.py 均只改版本号,无夹带逻辑改动。

本 PR 前几轮(Go calldepth、Java printf % 注入、tri-state 语义、并发归属、C++ 注入顺序、三运行时示例 HTTP 层加固、examples 纳入默认构建)均已修复并复核通过。本轮为发布版本号提升,无新增风险,本 PR 可以合并。

@Liam0205
Liam0205 merged commit 51f0ee0 into master Jul 19, 2026
21 checks passed
@Liam0205
Liam0205 deleted the fix/172-per-engine-log-prefix branch July 19, 2026 04:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

log_prefix is process-global: multiple engines in one process clobber each other's prefix

1 participant