Uh oh!
There was an error while loading. Please reload this page.
fix(runtime): serve the hook/action ['log'] capability from a logger that exists (#7448) - #7660
Conversation
…r that exists (#7448) A body declaring `['log']` and calling `ctx.log.info(...)` ran to completion, returned normally, and produced nothing. QA run #7439 measured it on the showcase at `--log-level debug`: `[BodyRunner] hook fired` appeared while the body's own `task completed: ...` line did not. Cause: `body-runner.ts` wired the capability to `engineCtx?.logger` (hooks) and `actionCtx?.logger` (actions) — a key no producer writes. `HookContextSchema` declares no `logger`; ObjectQL's engine builds all four of its HookContexts without one; neither action-context assembly site writes one. So `ctx.log` was `undefined` on every path and the VM bridge's `ctx.log?.[level]?.(...)` optional-called into nothing. The BodyRunner's own diagnostics stayed visible because they use `opts.logger`, which every construction site supplies. Serve the capability from `opts.logger` instead — the engine's own Logger, passed by all four app-plugin sites — and delete the dead context limbs rather than keep them as a second de-facto contract (as #5906 and #6316 did in this same file). 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 so structured data no longer lands in the `Error` slot. Also fix the payload half of the same capability: the VM bridge read the optional `data` argument with `vm.getString`, which coerces inside the VM, so an object arrived as the literal "[object Object]". Use `vm.dump`, the marshalling every other host-call bridge in that file already uses. With no logger at all (no production path, but reachable for embedders) the capability 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.
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 20 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also reference the affected code. These are read-only:
|
os-help
commented
Aug 11, 2026
Adjacent finding filed, not fixed here: #7661 — Kept out of this PR deliberately: #7448 is about the three installed methods producing nothing, whereas that is a fourth method that does not exist — and closing it is an enforce-or-remove capability-surface decision (the Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31483342423 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Uh oh!
There was an error while loading. Please reload this page.
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31485726120 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Fixes#7448
Diagnosis — it is (a): the context arrives with no logger at all
Not "on the showcase boot path" — on every path.
engineCtx.loggeris a key that no producer writes and no schema declares.main@245d1dc)packages/runtime/src/sandbox/body-runner.ts:339log: engineCtx?.logger— the hook wiringpackages/runtime/src/sandbox/body-runner.ts:377log: actionCtx?.logger— the action wiringpackages/spec/src/data/hook.zod.ts:334(HookContextSchema)logger—grep -c loggerover the file returns 0packages/objectql/src/engine.ts:6832, 6972, 7825, 8857const hookContext: HookContext = {…}assembly sites — none writes aloggerkeypackages/runtime/src/domains/actions.ts:330andpackages/runtime/src/action-execution.ts:1044loggerkeyThe divergence point the card asked for:
packages/objectql/src/engine.ts:1874. That is where the engine hands its own logger to the BodyRunner —— and there is no counterpart on the per-invocation context. So
opts.logger(body-runner.ts:74,[BodyRunner] hook fired) resolves to the engine's realLogger, whileengineCtx.logger(:339) reads a key nobody ever set. One logger, oneundefined, from the same object graph. That is the whole of the divergence.Downstream it becomes silent rather than fatal at
packages/runtime/src/sandbox/quickjs-runner.ts:646:The capability gate passes, the VM-side
ctx.log.infoexists, the body runs to completion and returns normally — and the line goes nowhere. Exactly the "cannot tell did not run from ran and logged into the void" shape the card names.This is the third limb of this shape removed from this same file, not the first:
doc/previousDoc(#5906) andsession.user(#6316) were also keys no producer ever wrote, deleted rather than left as a second de-facto contract (Prime Directive #12).Reproduction — showcase,
--log-level debug, before → afterBooted the way QA run #7439 did (
pnpm dev -- --fresh -p <port> --log-level debug), signed in as the seeded admin, andPATCHed ashowcase_taskfromdone: false→trueto fireshowcase_audit_task_completion(whose body isctx.log.info('task completed: ' + …), capabilities['log']).BEFORE (the two source files reverted to
origin/main,@objectstack/runtimerebuilt):The hook fires, does not throw — and the line it emits is absent from the entire debug log. That is the measurement.
AFTER:
The fix, and why this shape follows from the cause
The cause is a read of a key nobody writes, so the fix is to read the source that exists — not to add a fallback behind the dead one.
opts.logger. It is the engine's ownLogger, passed by all four construction sites inapp-plugin.ts(logger: ctx.logger), and it is the very logger whose[BodyRunner] hook firedwas already observable in the same QA run. So "why does this reach the process log stream when the current wiring does not" has a measured answer rather than an argued one: it is the stream the run already proved reachable.engineCtx.logger/actionCtx.loggerlimbs instead of keeping them as a preferred source (engineCtx?.logger ?? …). Under PD Add comprehensive test suite for Zod schema validation #12 a key no producer writes and no schema declares is not a contract to accommodate. The alternative — declaringloggeronHookContextSchema— would widen the metadata contract to re-supply, per invocation, something the runner already holds for the lifetime of the bind.[hook '<name>']/[action '<name>']prefix, because an author running many hooks otherwise cannot tell which one spoke, and the BodyRunner is the only layer that knows. The message body is untouched, sotask completed: …still matches as a substring.errorthrough the real contract.Logger.erroris(message, error, meta)— three args (packages/spec/src/contracts/logger.ts:36). Passing the body's data second lands a meta object in theErrorslot, whereConsoleLogger/JsonLoggerreaderror.message/error.stackasundefinedand drop every field. Same traphook-wrappers.tsdocuments forHookDiagnosticsLogger; the test asserts against a three-arg double so a regression here shows up as a lostmeta, not as a silent pass.Both call sites are covered
Hook (
:339) and action (:377) both go through onebuildBodyLogSurface(opts, origin). The action path needed the identical fix for the identical reason — neither of its two context assembly sites writesloggereither — and its test is in the same file.Second defect in the same capability, found by the reproduction
The VM bridge read the optional
dataargument withvm.getString, which applies JS string coercion inside the VM. Soctx.log.info('msg', { code: 'E1' })arrived at the host as the literal string"[object Object]", whichsafeJsonParsethen failed to parse and returned verbatim — every structured field of every body log call was lost. Nowvm.dump, the marshalling every other host-call bridge in that file already uses (ctx.api'sargHandles.map((h) => vm.dump(h))). In scope because it is the payload half of the same declared surface: fixing the destination while leaving the payload mangled would be half a fix.When the capability genuinely cannot be served
The closing condition is either it works, or the author is told it cannot. With no logger on the factory at all, working is off the table, so this takes the told branch: warn once per invocation, naming the body and the remedy.
It deliberately does not fall back to
console. That would override a decision belonging to the host — aLoggercarries the level threshold, formatting and sinks the host chose — and a host running atwarnwould start receiving bodyinfolines on an unfiltered second stream it never configured. No production path reaches this branch (all fourapp-plugin.tssites passctx.logger), so it is a diagnostic for embedders constructing the factory directly. Once per invocation rather than once per call, so a chatty body cannot bury the rest of the log.Verification
The new test asserts observability — what the host logger actually received after a real QuickJS body ran — not that the wiring is non-null. A null-check test would pass on a logger that drops the level, which is the wrong oracle for an observability defect.
Fails without the change (all 5, on unmodified source):
Note the action case failed at the log assertion while
expect(value).toEqual({ ok: true })passed — the body ran to completion and logged into the void.Targeted test, with the change:
ESLint (changed files):
TypeScript:
Full runtime suite (regression):
pnpm buildalso green end to end (71/71 tasks) — that is the build both showcase boots above ran against.Scope
Confined to
packages/runtime/src/sandbox/(body-runner.ts,quickjs-runner.ts, one new test) plus a changeset. No contact with the three live regions in this wave —rest-server.ts(#7603, #7566),package-routes.ts(#7563), the 400-envelope work (#7543). The diagnosis reached intopackages/objectql/src/engine.tsandpackages/spec/src/data/hook.zod.tsto establish that nobody writes the key, but neither needed changing: the correct logger was already in the BodyRunner's hand.No
content/docs/releases/edit; the changeset is the release-notes input. Nodocs/adr/**contact.Generated by Claude Code