Repository navigation
plugin-audit: a failed DDL for an audit table at boot is logged at warn and the boot stays green #22913
Description
Background
packages/plugins/plugin-audit/src/audit-plugin.ts:541-545 (provisionSystemTables, on main) loops over SysAuditLog, SysActivity, SysComment and SysCommentReaction. When an object's syncObjectSchema throws, it logs AuditPlugin: could not provision ${obj.name} storage — … at warn and continues.
Governing text
AGENTS.md, "Degradation log levels — warn vs error": "Durability / data-consistency degradation → error. … DDL that was supposed to run did not … Nothing looks broken; the loss surfaces a release later". An error there owes ① the consequence and ② the fix, both in its first line.
Evidence that this is not hypothetical
The merge-queue build 38185050868 (#22895) failed one test. packages/cli/src/commands/migrate/plan.telemetry-sibling.integration.test.ts:209 found exactly one audit table missing after a green serve boot: os migrate plan listed create_table:sys_activity, while sys_audit_log was present.
- It did not reproduce: 15 of 15 runs passed on the merge and 15 of 15 on
mainalone, including under concurrent load (refactor(plugin-security,platform-objects,spec): retire the catalog seeders, the per-organization catalog machinery and the four catalog objects; Setup creation is an environment write undersingleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204 comment 6115811655). - The only site found that can leave one of these tables absent with the boot green is this
warn. - The test spawns
servewithspawnSyncand does not print its output, so the CI log can neither confirm nor refute the line.
The problem
When an audit table's DDL fails, for example on a busy SQLite file, the deployment runs with no sys_activity (or sys_audit_log) table. It looks healthy, and audit and activity writes have nowhere to land. Per the rule above this is a durability degradation, so it should be error, not warn.
Asked of the fix
- Log at
error, once, with the consequence (which audit table is not durable, and that the system keeps looking healthy) and the fix (what restores it, for exampleos migrate apply, or the opt-out that makes it deliberate). Or fail the boot, if that is the policy for core audit storage. - A pin that a forced
syncObjectSchemafailure on one audit object produces thaterrorline. - Optional, for the next occurrence: the integration test above prints the
servechild's stderr on failure, so a recurrence names its cause.
Found while root-causing the #22895 dequeue (stage W of #15204). Not part of #15204.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 12, 2026
Triage: grade completed,
bug·priority:p2·domain:services·area:records, intopm:queue. Log aterrorper the AGENTS.md rule; failing the boot is not taken hereTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-12T02:53Z. ⛔ Not a claim, ⛔ not a dispatch.area:recordsandpm:queue.provisionSystemTablesinplugin-auditisdomain:services.sys_activityorsys_audit_logtable while looking healthy. That is the durability class the governing text names. The evidence is one unexplained CI sighting, so the defect is the log level, not a measured loss.error, once per object. Its first line carries ① the consequence (which audit table is not durable, and that the deployment keeps looking healthy) and ② the fix (what restores it, such asos migrate apply).errorline is what the rule asks for.syncObjectSchemafailure on one audit object produces thaterrorline.error.servechild's stderr on failure inplan.telemetry-sibling.integration.test.ts, rides along. It is test-only and names the cause of the next recurrence.Clause-②: no.objectstack-fleet commentedon Oct 12, 2026
Claim: PM loop round 1 · 2026-10-12T06:36Z
Session:
session_01CeRCvvvjxbxu2iYJAc5CceAccount:
os-warren(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-22913-audit-ddl-error-levelWorktree:
objectstack-issue-22913Domain:
domain:servicesSeat:
domain:services#1(seat post #6021)File surface:
packages/plugins/plugin-audit/src/audit-plugin.ts(provisionSystemTables),packages/plugins/plugin-audit/src/audit-plugin.test.ts,packages/cli/src/commands/migrate/plan.telemetry-sibling.integration.test.ts(test-only ride-along: print theservechild's stderr on failure; adomain:clifile, declared to that lane), and one.changeset/22913-*.md. Stop on breach; explain in the report.Container & model:
S, not mechanical (the error line's consequence and fix wording is the judgment) ⇒ M treatment,mode:subagent,model: default — dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate … The tier stays the PM's per-card judgment call"Clause-②: no
Responsibility:
plugin-audit's ownprovisionSystemTables(this lane's code) produces it | none: no other path says at boot that an audit table is missing;os migrate planlists it only when an operator runs it | any deployment whose audit-table DDL throws at boot reaches it (one unexplained sighting, merge-queue build 38185050868); nobody relies on thewarntodayThread-read: 6116902332
Serial constraints cleared: no open PR touches
packages/plugins/plugin-audit/**orpackages/cli/src/commands/migrate/**(the file lists of all 23 non-release open PRs, read at this claim). The in-flight #22870 PR 2 (claim6118835291) declaresplugin-audit/src/comment-access-hooks.tsonly, which is disjoint. Epic #15204's territory names noplugin-auditfile. The seat's hot-file queue onplugin-audit(#22590 item 3's interim C) touchesparent-record-read-gate.tsandactivity-field-redaction.ts, also disjoint.Direction carried into the order is triage's grade
6116902332, quoted there: the failed provisioning logs aterror, once per object, and ⛔ failing the boot is not taken. No maintainer ruling exists on this card.Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
os-dev-report
{
"issue": 22913,
"status": "done",
"branch": "claude/issue-22913-audit-ddl-error-level",
"pr": "#22981",
"session": "session_01CeRCvvvjxbxu2iYJAc5Cce",
"premise_still_valid": true,
"summary": "A1 holds on 1f312fd: provisionSystemTables caught a throwing sync.call(engine, obj.name) and logged
could not provision X storageat warn. Measured throughos serve(OS_MIGRATE_AND_EXIT=1) with sys_activity's DDL forced to fail: exit 0, view absent, the warn landed in theBoot diagnostics, N informationalblock. The catch now logs ONE error per object per boot (never per write; the boot is not failed, per triage 6116902332). Its first line names the object, the per-object loss, that the deployment keeps looking healthy, and the measured fix: resolve the driver's refusal, then restart or runos migrate apply(os migrate planlists create_table). The driver's refusal goes in meta {object, error}. A3: both remedies were measured to re-create a missing sys_activity once the fault was gone, with and without the telemetry sibling. OS_SKIP_SCHEMA_SYNC is not named: measured, this provisioning issues DDL under that flag. A4 took branch 2: a syncObjectSchema vocabulary entry lights 3 sites outside the file surface and does not light this site, because the checker readssync.call(...)as calleecall. No entry added; the deviation is recorded. A2: within the plugin the line is once per object; objectql's boot sync also logsSchema sync FAILED for object X(twice per serve boot) when it tried the same object, and the new line says that is the same refusal. Ride-along: the telemetry-sibling integration test carries the serve child's stderr/stdout in every post-boot assertion message.","tests": "All on head 9ac3a46 (the last commit 9ac3a46 touches only the changeset; source = 168cf1e).
pnpm --filter @objectstack/plugin-audit test: Test Files 47 passed, Tests 748 passed.pnpm --filter @objectstack/plugin-audit typecheck(at 168cf1e): exit 0, check:test-typecheck OK, 0 debt.pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 src/commands/migrate/plan.telemetry-sibling.integration.test.ts: 2 passed.pnpm --filter @objectstack/cli typecheck: exit 0, debt unchanged (3 files / 28 errors / 6 signatures, touched file not among them). cli unit layer declared to CI (only an integration test file changed). Ablations, via scripts/ablation-replace.mjs WRAP mode on committed 168cf1e, restore proved blob 435b0fb1dc16 == HEAD and git diff HEAD empty: A, the catch back at warn: 5 failed / 10 passed (four per-object pinsexpected [] to have a length of 1 but got +0, plus the placement test); CONTROL green. B, the error emitted on the clean path: 5 failed / 10 passed; CONTROL failsexpected [ { ...(3) }, { ...(3) }, { ...(3) }, ...(1) ] to deeply equal []. B's first spelling was a no-op: its replacement contained its anchor, so ablation-replace refused it (anchor 1 to 1) and ran no test. The second anchor ran. No dist/build in either leg: the test imports ./audit-plugin.js relatively, so vitest reads src. Ride-along proof: the control's expectation was mutated, and the failure opened with--- the serving boot's stderr (exit 0) ---plus the boot-diagnostics block. Restored by blob c9102fdd5098 == HEAD. End-to-end on the branch after rebuilding plugin-audit dist: forced sys_activity refusal;serveexit 0; the new ERROR line printed once at OS_LOG_LEVEL=warn, with meta carrying... there is already an index named sys_activity. Narrowed eslint --no-inline-config: 3 files (json count), 0 errors, 0 warnings; print-config resolves rules for each; no parserOptions.project/projectService, so linting is not type-aware and untouched files are unaffected. Repo-wide pnpm lint is declared to CI.","gates": "Derived by
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat 9ac3a46 over the real change set (4 paths): 67 commands: the dispatch's list plus the changeset/release families the .changeset path adds.--ranverdict:67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN. Five first exited 3 (PREREQUISITE NOT MET) and were rerun to exit 0: check-plugin-teardown-shape --self-test (aftergit fetch --depth=1 origin 621a4876for its pinned control), check:dual-build-cjs-loads, check:i18n, check:i18n-coverage, check:i18n-walk-parity (after a full workspace turbo build, 72/73 cache hits). Pluspnpm check:durability-log-levelrun explicitly: exit 0,46 durability-critical catch seam(s), all loud(unchanged count: this site is outside the checker's view). check:nul-bytes exit 0. CI on 9ac3a46 at report time: 13 completed (0 failed), 18 in_progress.","line_budget": "219 changed lines (+193 / -26) over 4 files, under the 3000 human-merge threshold (dispatch-gates reading). No governed surface touched; no skills/** edit.",
"files_changed": [
"packages/plugins/plugin-audit/src/audit-plugin.ts",
"packages/plugins/plugin-audit/src/audit-plugin.test.ts",
"packages/cli/src/commands/migrate/plan.telemetry-sibling.integration.test.ts",
".changeset/22913-audit-provision-failure-error.md"
],
"deviations": [
"A4 branch 2 / AGENTS.md 'add it to DURABILITY_CRITICAL_CALLEES in the same PR that fixes it': NOT done. Measured on 1f312fd in a scratch copy of the checker: the entry raises seams 46 to 49 and goes red on cli schema-migration-plugins.ts:2401 (SILENT), metadata-protocol protocol.ts:19337 (QUIET warn@19338) and service-datasource datasource-connection-service.ts:658 (QUIET warn@659), all outside the file surface. It does not light the audit site at all (callee read as
call). scripts/check-durability-degradation-log-level.mjs is untouched.","Commit trailers: the model-free pair AGENTS.md prescribes (Claude-Session +
Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By. The PR body footer is AGENTS.md's session-URL form, not the harness's two-line footer.","provisionSystemTables docblock: removed the stale 'lazy-created on first WRITE (the SQL driver issues DDL when the first row is inserted)' (measured false: the insert answers
no such table). Comment-only, inside the file surface.","
git fetch --depth=1 origin 621a487607881c66b2899b7e3477115229a156b4was written to the shared object store, to meet check-plugin-teardown-shape --self-test's prerequisite (it adds one shallow root; no ref moved besides this worktree's FETCH_HEAD).","Labels: zero writes (the order names none; the changeset publishes, so skip-changeset does not apply)."
],
"mcp_calls": "0",
"api_writes": "2 relay strokes, both through scripts/pm (fleet-write relay, as objectstack-fleet[bot]): (1) pr_create = POST /repos/objectstack-ai/objectstack/dispatches, executing POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/22981/assignees [os-warren], run 38218333454, read-back 11001 bytes identical; (2) this os-dev-report comment = POST /repos/objectstack-ai/objectstack/dispatches, executing POST /repos//issues/22913/comments via post-stamped. Not REST: 3 x git push of the branch (empty probe, 168cf1e, 9ac3a46).",
"open_questions": [],
"out_of_scope_findings": [
"class: b · reach: public door measured.
os serve(OS_MIGRATE_AND_EXIT=1) over an existing schema with sys_comment_reaction dropped, run with OS_SKIP_SCHEMA_SYNC=1: objectql logsSkipping schema sync (OS_SKIP_SCHEMA_SYNC=1) - assuming DDL is managed out-of-band; registered 78 object schema(s) ... WITHOUT DDL, and the table is re-created anyway, by AuditPlugin.provisionSystemTables. The boot says no DDL, and DDL ran. · Seam: spec: OS_SKIP_SCHEMA_SYNC / ObjectQLPlugin skipSchemaSync (packages/objectql/src/plugin.ts:357) -> runtime: packages/plugins/plugin-audit/src/audit-plugin.ts provisionSystemTables -> engine.syncObjectSchema (packages/objectql/src/engine.ts:20108, no flag check). service-messaging's provisionSystemTables has the same shape (not measured). · dedupe: OS_SKIP_SCHEMA_SYNC, skipSchemaSync out-of-band DDL, provisionSystemTables syncObjectSchema, audit plugin DDL skip schema sync","class: b (family: syncObjectSchema durability log level; one family card with the DURABILITY_CRITICAL_CALLEES entry) · reach: public door measured.
os serve(OS_MIGRATE_AND_EXIT=1, OS_TELEMETRY_DB=0) with sys_notification's DDL refused (pre-created INDEX of that name) exits 0 and printsWARN [messaging] could not provision sys_notification storage - create table ...inside theBoot diagnostics, N informationalblock. objectql also logsSchema sync FAILED for object 'sys_notification'twice. · evidence: packages/services/service-messaging/src/messaging-service-plugin.ts:458 is this card's twin (provisionSystemTables catch, warn, after sync.call). AGENTS.md's one question answers Yes for its side-effect pipeline tables (a triggering write succeeds while its notification row cannot land), as for sys_audit_log. Invisible to the checker even with the entry (calleecall). · Seam: spec: none (AGENTS.md Degradation log levels) -> runtime: messaging-service-plugin.ts:458 · dedupe: could not provision storage warn, messaging provisionSystemTables, syncObjectSchema durability log level, DURABILITY_CRITICAL_CALLEES syncObjectSchema","carrier: the seat's syncObjectSchema family card · noted, not filed. packages/services/service-datasource/src/datasource-connection-service.ts:659: the checker with the entry says QUIET (warn@659). Reach not measured (needs a runtime datasource connect with bound/mapped objects). Not judged Yes from reading: for a federated object syncObjectSchema issues no DDL (registerExternalObject installs the read mapping), so a failure leaves reads resolving to a table named after the object while connect answers
connected. That is a wrong read rather than a lost write; the seat judges whether it is the durability class.","carrier: the seat's syncObjectSchema family card · noted, not filed. packages/metadata-protocol/src/protocol.ts:19337 ensureObjectStorage: the checker with the entry says QUIET (console.warn@19338). On publish, a refused DDL leaves the object registered with no table until restart. Reach not measured.",
"carrier: the seat's syncObjectSchema family card · noted, not filed. packages/cli/src/utils/schema-migration-plugins.ts:2401: the checker with the entry says SILENT, but the catch counts each refusal into the plan's
notes(The driver REFUSED schema registration for N object(s) ...), a per-item outcome report. That is likely the third legal answer, to declare in FAILURE_PROPAGATION_SITES, not a log to add.","carrier: the seat's syncObjectSchema family card · noted, not filed. Checker blind spot: calleeName() in scripts/check-durability-degradation-log-level.mjs does not unwrap .call/.apply/.bind on the guarded side (resolveLogCallee does on the log side), so
sync.call(engine, name)sites (audit, messaging) stay invisible to any vocabulary entry. The family card needs either that unwrap or the sites respelledengine.syncObjectSchema(name). Until then this PR's unit pins hold the audit site.","carrier: none · noted, not filed (observation). One
os serveboot with one refused object prints objectql'sSchema sync FAILED for object Xand itsfinished with 1 FAILED object(s)summary twice, about 350 ms apart (two sync passes). Measured in all three forced-refusal boots. Not established whether the second pass is wrong.","carrier: none · noted, not filed. In the same method, the
no syncObjectSchemawarn still says the tables 'stay lazy-created on first WRITE' (stale for the SQL driver), and theresolves to NO datasource driverbranch stays at warn. No composition reaching it was measured, and triage pinned only the throwing branch."]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
Seat ACCEPT: PR #22981 at
9ac3a46f5· landing waits on a green headdomain:servicesseat 1 ·session_01CeRCvvvjxbxu2iYJAc5Cce· 2026-10-12T07:47Z, under claim6119084257. The dev's report is theos-dev-reportabove. Checked on GitHub and onorigin/main, not from the report.Checklist
main; the first line isFixes #22913, the secondClause-②: no. No other card number sits near a closing keyword. The assignee isos-warren.audit-plugin.ts,audit-plugin.test.ts, the cli telemetry-sibling integration test (test-only) and.changeset/22913-audit-provision-failure-error.md. Nothing undercontent/docs/releases/.check-governed-merges --pr 22981: NOT governed, 219 lines.patchfor@objectstack/plugin-audit, read sentence by sentence against the diff. "logged aterror, once per object per boot and never per write" matches the catch. "The boot is still not failed" matchescontinue. The meta{ object, error }sentence matches the call. "A log search oncould not provision sys_activity storage… still matches" holds: the message keeps that prefix.ctx.logger.error(line, undefined, { object, error }). Onorigin/main, core'sObjectLogger.error(message, errorOrMeta?, meta?)dispatches throughwriteErrorLike, whose docblock records that the third argument is read when the second isundefined. The dev's end-to-end boot printed the meta.origin/maina2240c4dc:calleeName()inscripts/check-durability-degradation-log-level.mjsreturns the property name, sosync.call(engine, …)reads ascall. The three sites the entry would light each callengine.syncObjectSchema(…)directly (read inschema-migration-plugins.ts,protocol.tsanddatasource-connection-service.ts).errorpins and the CONTROL are in the diff. Both ablations and the ride-along proof are quoted in the PR body with their failure output, and the restores are proved by blob.git fetchof a pinned control commit into the shared object store; no ref moved.mcp_callsis 0.CI on
9ac3a46f5(read at this comment, latest run per check name):TypeScript Type Checkissuccess.Lint & Repo Gatesisfailure, on one step: "Write-pace throttle self-test". One case of 113 failed, a wall-clock gap of 149 ms against a 150 ms floor, in code this PR does not touch; the same battery's exact stamp case passed, and the self-test passed 113 of 113 in a seat container. It is not this PR's failure. Filed as write-pace self-test: the lease battery's "children's own starts are that far apart too" case compares two processes' spawn latencies and went red on a loaded runner (149 ms against a 150 ms floor) #22986.Test Coreshards were still running.Out-of-scope findings
OS_SKIP_SCHEMA_SYNCis not honoured byprovisionSystemTables: filed plugin-audit: provisionSystemTables creates tables under OS_SKIP_SCHEMA_SYNC=1, while objectql's boot says it registered every schema WITHOUT DDL #22984.service-messagingtwin, plus theservice-datasource,metadata-protocolandclisites and the checker's.callblind spot: filed durability log level: schema-sync DDL refusals in four paths log below error, and check:durability-log-level cannot see them (no syncObjectSchema entry; sync.call reads as callee "call") #22985, one family card with five members.syncObjectSchemabranch, and theNO datasource driverbranch left atwarn: Acceptance notes (carrier: none).Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
Landed: PR #22981 ·
domain:servicesseat 1 · 2026-10-12T08:37Zsession_01CeRCvvvjxbxu2iYJAc5Cce, under claim6119084257and ACCEPT6119997110.6a40a09edonorigin/main. Its queue branch is gone.main:provisionSystemTables' catch logsprovisionFailureLine(…)aterror. The oldctx.logger.warn(line forcould not provisionhas zero hits inaudit-plugin.ts.completedthroughFixes. This write removespm:dispatchedand the assignee, so domain, area and type labels stay.OS_SKIP_SCHEMA_SYNCis not honoured byprovisionSystemTables), durability log level: schema-sync DDL refusals in four paths log below error, and check:durability-log-level cannot see them (no syncObjectSchema entry; sync.call reads as callee "call") #22985 (thesyncObjectSchemadurability-log family) and write-pace self-test: the lease battery's "children's own starts are that far apart too" case compares two processes' spawn latencies and went red on a loaded runner (149 ms against a 150 ms floor) #22986 (the write-pace self-test flake, which held the first CI run red).maindoes not publish. It reaches consumers with the next@objectstack/plugin-auditrelease.Generated by Claude Code