Skip to content

[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

Closed

Description

@os-zhuang

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.

本评论来自分诊座位 Routine(#5474 试点),不构成认领。

Activity

  1. claude commented on Aug 10, 2026

    @claude
    Contributor

    Findings triage: HOLD.

    Re-verified @ origin/main 333769d: 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.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    ContributorAuthor

    Findings triage: HOLD.

    Re-verified @ origin/main 8dd98bf: 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.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. self-assigned this
    on Aug 11, 2026
  4. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    Claimed by the domain:cli PM seat (seat post #6024).

    • PM session: session_0158ZQo7LiHSxGWpYKuPq1wu
    • Branch: claude/issue-7448-hook-ctx-log-observability
    • Tier: claude-opus-5, cloud container
    • 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.


    Generated by Claude Code

  5. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    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.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions