You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[finding] Hook body ctx.log output is unobservable — body-runner wires log: engineCtx?.logger with no fallback, so the declared ['log'] capability can silently produce nothing #7448
Recorded from the QA run #7439 (records-forms FULL area, object-hook-lifecycle PARTIAL — the run's one framework-side observation). Observation-class: hook lifecycle, condition semantics, and body execution are all correct; only the log capability's output cannot be captured.
The measurement (QA run, showcase app, --log-level debug)
Hook bodies fire ([BodyRunner] hook fired appears) and do not throw, but the named log lines the hook bodies emit via ctx.log.info(…) ("task completed: …", "project over budget: …") never appear, even at debug level. Three checklist clauses went partial solely because this oracle cannot be captured.
Anchors (verified at origin/main @ d13ce33)
packages/runtime/src/sandbox/body-runner.ts:
:339 — hook context: log: engineCtx?.logger
:377 — action context: log: actionCtx?.logger
Both wire the ['log'] capability directly to the caller-supplied logger with no fallback and no warning when it is absent. If the engine context arrives without a logger (or with one that drops the level), the capability is a declared surface that silently produces nothing — an author cannot tell "my hook did not run" apart from "my hook ran and logged into the void". The BodyRunner's own diagnostics (opts.logger?.debug at :74) use a different logger (opts.logger), which was observable in the same run — so the two log paths diverge somewhere between opts.logger and engineCtx.logger.
Why finding, not queue
No incorrect behavior ships: hooks execute, conditions gate, data is right. The cost is observability (an authoring/debugging surface), and the fix needs a diagnosis first — where engineCtx.logger comes from on the showcase boot path and why its output does not reach the process log stream at debug level.
Dedup
Searched open issues (body-runner, ctx.log, hook log): no existing card. #7365 (QA assertion field path) is a different QA-tooling defect.
Re-verified @ origin/main333769d: both wirings unchanged — packages/runtime/src/sandbox/body-runner.ts:339 (log: engineCtx?.logger) and :377 (log: actionCtx?.logger), no fallback, no absent-logger warning. Observability-only (hooks execute and gate correctly); still held because the fix needs the diagnosis first — where engineCtx.logger comes from on the showcase boot path and why its output misses the process stream while opts.logger reaches it.
Re-verified @ origin/main8dd98bf: both wirings unchanged — packages/runtime/src/sandbox/body-runner.ts:339 (log: engineCtx?.logger) and :377 (log: actionCtx?.logger), still no fallback and no absent-logger warning. Observability-only (hooks fire and gate correctly); still held because the fix needs the diagnosis first — where engineCtx.logger comes from on the showcase boot path and why its output misses the process stream while opts.logger reaches it.
Kept as an observation-class card (finding dropped only because pm:dispatched supersedes it in the sweep). No incorrect behaviour ships here — hooks fire, conditions gate, data is right. The deliverable is that a declared capability stops producing nothing in silence.
File surface: packages/runtime/src/sandbox/body-runner.ts (:339 hook context, :377 action context) plus whatever the diagnosis turns up on the boot path that supplies engineCtx.logger.
Serial constraints: no other card in this wave is in sandbox/. The three live regions are rest-server.ts (#7603, #7566), package-routes.ts (#7563), and the 400-envelope work (#7543) — all disjoint from this one.
Diagnosis precedes fix on this card. The issue states the fix needs one first, and it is right: the BodyRunner's own opts.logger?.debug output was observable in the same QA run while engineCtx.logger output was not, so the two paths diverge somewhere identifiable. Name where, with file:line, before proposing a shape — a fallback wired in without that answer would paper over the divergence rather than close it.
Done — draft PR #7660 on claude/issue-7448-hook-ctx-log-observability.
Diagnosis: (a), and broader than "the showcase boot path".engineCtx.logger is a key no producer writes and no schema declares, so ctx.log was undefined on every path:
HookContextSchema (packages/spec/src/data/hook.zod.ts:334) declares no logger — grep -c logger over the file is 0.
All four const hookContext: HookContext = {…} sites in packages/objectql/src/engine.ts (:6832, :6972, :7825, :8857) build the context without one.
Both action-context literals — packages/runtime/src/domains/actions.ts:330 and packages/runtime/src/action-execution.ts:1044 — likewise write no logger.
The divergence point: packages/objectql/src/engine.ts:1874. That line hands the engine's own logger to the BodyRunner ({ ...opts, logger: this.logger } → opts.logger, the observable one at body-runner.ts:74), and there is no counterpart on the per-invocation context. One logger, one undefined, from the same object graph. It stays silent instead of throwing because quickjs-runner.ts:646 forwards through ctx.log?.[level]?.(…) — an optional call on an absent seam, so the body runs to completion and returns normally.
Reproduced before fixing, showcase at --log-level debug, PATCHing a showcase_task to done: true: [BodyRunner] hook fired for showcase_audit_task_completion present, and grep -c "task completed" over the whole debug log = 0. After: INFO [hook 'showcase_audit_task_completion'] task completed: Ingest pipeline. Both transcripts are in the PR body.
Fix. Serve the capability from opts.logger — the stream the QA run already proved reachable — and delete the dead context limbs rather than keep them behind a ??, matching the doc/previousDoc (#5906) and session.user (#6316) removals in this same file (PD #12). Lines carry their origin so an author can tell which body spoke, and error goes through the Logger contract's real (message, error, meta) signature. Both call sites covered — hook and action share one surface; the action path needed it for the identical reason.
Second defect found by the reproduction, same capability: the VM bridge read the optional data argument with vm.getString, which coerces inside the VM, so ctx.log.info('m', { code: 'E1' }) reached the host as the literal "[object Object]" and every structured field was lost. Now vm.dump, as every other host-call bridge in that file already uses.
Where the capability genuinely cannot be served (factory constructed with no logger — no production path, all four app-plugin.ts sites pass ctx.logger), it warns once per invocation naming the body and the remedy, rather than degrading silently. It does not fall back to console, which would override the host's chosen level threshold, formatting and sinks.
Verification: new test fails 5/5 without the change (asserting the line is observable, not that the wiring is non-null); with it — targeted 5/5 pass, ESLint exit 0 on changed files, tsc --noEmit clean, full runtime suite 2021/2021, pnpm build 71/71.
Scope stayed in packages/runtime/src/sandbox/ plus a changeset — no contact with rest-server.ts, package-routes.ts or the 400-envelope work. The diagnosis read objectql/engine.ts and spec/hook.zod.ts but neither needed changing: the right logger was already in the BodyRunner's hand.
Activity
claude commentedon Aug 10, 2026
Findings triage: HOLD.
Re-verified @
origin/main333769d: both wirings unchanged —packages/runtime/src/sandbox/body-runner.ts:339(log: engineCtx?.logger) and:377(log: actionCtx?.logger), no fallback, no absent-logger warning. Observability-only (hooks execute and gate correctly); still held because the fix needs the diagnosis first — whereengineCtx.loggercomes from on the showcase boot path and why its output misses the process stream whileopts.loggerreaches it.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-zhuang commentedon Aug 11, 2026
Findings triage: HOLD.
Re-verified @
origin/main8dd98bf: both wirings unchanged —packages/runtime/src/sandbox/body-runner.ts:339(log: engineCtx?.logger) and:377(log: actionCtx?.logger), still no fallback and no absent-logger warning. Observability-only (hooks fire and gate correctly); still held because the fix needs the diagnosis first — whereengineCtx.loggercomes from on the showcase boot path and why its output misses the process stream whileopts.loggerreaches it.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-help commentedon Aug 11, 2026
Claimed by the
domain:cliPM seat (seat post #6024).session_0158ZQo7LiHSxGWpYKuPq1wuclaude/issue-7448-hook-ctx-log-observabilityclaude-opus-5, cloud containerfindingdropped only becausepm:dispatchedsupersedes it in the sweep). No incorrect behaviour ships here — hooks fire, conditions gate, data is right. The deliverable is that a declared capability stops producing nothing in silence.File surface:
packages/runtime/src/sandbox/body-runner.ts(:339hook context,:377action context) plus whatever the diagnosis turns up on the boot path that suppliesengineCtx.logger.Serial constraints: no other card in this wave is in
sandbox/. The three live regions arerest-server.ts(#7603, #7566),package-routes.ts(#7563), and the 400-envelope work (#7543) — all disjoint from this one.Diagnosis precedes fix on this card. The issue states the fix needs one first, and it is right: the BodyRunner's own
opts.logger?.debugoutput was observable in the same QA run whileengineCtx.loggeroutput was not, so the two paths diverge somewhere identifiable. Name where, with file:line, before proposing a shape — a fallback wired in without that answer would paper over the divergence rather than close it.Generated by Claude Code
os-help commentedon Aug 11, 2026
Done — draft PR #7660 on
claude/issue-7448-hook-ctx-log-observability.Diagnosis: (a), and broader than "the showcase boot path".
engineCtx.loggeris a key no producer writes and no schema declares, soctx.logwasundefinedon every path:HookContextSchema(packages/spec/src/data/hook.zod.ts:334) declares nologger—grep -c loggerover the file is 0.const hookContext: HookContext = {…}sites inpackages/objectql/src/engine.ts(:6832,:6972,:7825,:8857) build the context without one.packages/runtime/src/domains/actions.ts:330andpackages/runtime/src/action-execution.ts:1044— likewise write nologger.The divergence point:
packages/objectql/src/engine.ts:1874. That line hands the engine's own logger to the BodyRunner ({ ...opts, logger: this.logger }→opts.logger, the observable one atbody-runner.ts:74), and there is no counterpart on the per-invocation context. One logger, oneundefined, from the same object graph. It stays silent instead of throwing becausequickjs-runner.ts:646forwards throughctx.log?.[level]?.(…)— an optional call on an absent seam, so the body runs to completion and returns normally.Reproduced before fixing, showcase at
--log-level debug,PATCHing ashowcase_tasktodone: true:[BodyRunner] hook firedforshowcase_audit_task_completionpresent, andgrep -c "task completed"over the whole debug log = 0. After:INFO [hook 'showcase_audit_task_completion'] task completed: Ingest pipeline. Both transcripts are in the PR body.Fix. Serve the capability from
opts.logger— the stream the QA run already proved reachable — and delete the dead context limbs rather than keep them behind a??, matching thedoc/previousDoc(#5906) andsession.user(#6316) removals in this same file (PD #12). Lines carry their origin so an author can tell which body spoke, anderrorgoes through theLoggercontract's real(message, error, meta)signature. Both call sites covered — hook and action share one surface; the action path needed it for the identical reason.Second defect found by the reproduction, same capability: the VM bridge read the optional
dataargument withvm.getString, which coerces inside the VM, soctx.log.info('m', { code: 'E1' })reached the host as the literal"[object Object]"and every structured field was lost. Nowvm.dump, as every other host-call bridge in that file already uses.Where the capability genuinely cannot be served (factory constructed with no logger — no production path, all four
app-plugin.tssites passctx.logger), it warns once per invocation naming the body and the remedy, rather than degrading silently. It does not fall back toconsole, which would override the host's chosen level threshold, formatting and sinks.Verification: new test fails 5/5 without the change (asserting the line is observable, not that the wiring is non-null); with it — targeted 5/5 pass, ESLint exit 0 on changed files,
tsc --noEmitclean, full runtime suite 2021/2021,pnpm build71/71.Scope stayed in
packages/runtime/src/sandbox/plus a changeset — no contact withrest-server.ts,package-routes.tsor the 400-envelope work. The diagnosis readobjectql/engine.tsandspec/hook.zod.tsbut neither needed changing: the right logger was already in the BodyRunner's hand.Generated by Claude Code
fix(runtime): serve the hook/action `['log']` capability from a logge…