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
driver-sqlite-wasm: a background flush that fails (void this.flush()) is an unhandled rejection — main's Dogfood shard 2/3 went red on it, when a refused boot's never-closed driver flushed into a directory the test had already removed #23635
Filing gate ①, a red on main, measured once. Filed by the triage seat · session_01AavokzJ5DndAwitDXvKy4U. ⛔ Not a claim.
reach:main's CI run 38697814439 at e611e2bdae (07:18Z): Dogfood Regression Gate (2/3) failed, and with it Verify dogfood shard results.
All 634 tests passed (87 files). The job failed on one unhandled rejection.
The next main run (38701251197, f7cd580bff) had shard 2/3 green. It is intermittent: a race, not a flake.
The reading
The red, verbatim from the job log:
Unhandled Rejection
Error: ENOENT: no such file or directory, rename '/tmp/dogfood-ownership-order-AfWd6u/showcase.db.tmp-3627-65' -> '/tmp/dogfood-ownership-order-AfWd6u/showcase.db'
❯ _WasmSqliteConnection.atomicWriteFile ../../drivers/driver-sqlite-wasm/src/wasm-connection.ts:435:7
❯ ../../drivers/driver-sqlite-wasm/src/wasm-connection.ts:398:9
This error originated in "test/organization-ownership-boot-refusal-order.dogfood.test.ts"
The file's own docblock says the refused boot's SQLite driver is never torn down: bootstrap failed before any handle came back.
That driver keeps scheduling flushes as fire-and-forget calls: void this.flush() at wasm-connection.ts:329, :352 and :359.
flush() re-marks dirty and rethrows from the awaited step (:398-:407). At those three call sites nobody awaits or catches it.
afterAll removes the temp directory (rmSync(dir, …)). A flush still in flight then fails its rename, and the rejection reaches the process unhandled. Vitest fails the run.
Ask
At the producer (packages/drivers/driver-sqlite-wasm): a background flush that fails is handled. It is logged once, with the file named, and dirty stays set for a later retry, as flush() already intends. No call site leaves a rejection to the process.
Pin it: a flush that fails in the background (the target's directory removed, or a rename that rejects) produces no unhandled rejection, and the next flush retries. CONTROL: an awaited flush() (the close() path) still throws to its caller.
⛔ No retry, skip or quarantine of the dogfood file, and no removal of its afterAll cleanup.
Read, not required for green: a bootstrap that fails after the driver connected never closes it. That is harmless under os serve, whose process exits, but in-process hosts (tests, embedders) leak the driver.
The seat says on this card whether it folds that in or files it.
Not measured: whether a production os serve that meets a failing background flush (ENOSPC, a removed data directory) crashes on the same unhandled rejection. This seat's search for an unhandledRejection handler was on an older checkout, not main. The seat confirms on main.
Grade p2: an intermittent red on main, seen once. A PR the queue bounces on it can re-enter (a tax, one grade down), and the production half is unmeasured.
Lane domain:engine (packages/drivers/driver-*, by the lane table).
Done:
the pin is green;
a background flush can no longer reject unhandled;
Claim: PM loop round 2 · 2026-10-15T08:07Z · on the maintainer's 「engine 车道还有哪些 p2 的小卡你可以认领的, 20x 周套餐还剩6%,你可以串行派发。」 (this session), which lifts this seat's earlier no-new-dispatch scope for serial p2 work
Session: session_01ADzJtzYTLUfgrRZHxkagkX
Account: os-steve (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-23635-background-flush-rejection
Worktree: objectstack-issue-23635
Domain: domain:engine
Seat: domain:engine#2 (seat post #20966)
File surface: packages/drivers/driver-sqlite-wasm/src/wasm-connection.ts (the three void this.flush() sites at :329, :352, :359 and flush() at :377), a new or existing test beside it in packages/drivers/driver-sqlite-wasm, and one changeset. ⛔ Not organization-ownership-boot-refusal-order.dogfood.test.ts: no retry, skip or quarantine, and its afterAll cleanup stays. Stop on a breach and explain it in the report.
Container & model: S, mode:subagent, model: floor (this run's dispatch-gates.mjs --tier: "no path-derived mandate … floor · default · ceiling"; one file's background-error handling plus a pin, under the maintainer's budget note)
Clause-②: no
Responsibility: the risk is produced by driver-sqlite-wasm's own fire-and-forget void this.flush() calls. No platform path covers it: on main23e689dc7d, git grep -n -E "unhandledRejection|unhandled-rejections" origin/main -- 'packages/cli/**' 'packages/runtime/**' 'packages/core/src/**' finds no handler, so the process default applies. It is reached today by CI (main's Dogfood shard 2/3, intermittent) and by in-process hosts. By the same reading, a production os serve whose background flush fails (ENOSPC, a removed data directory) meets the same unhandled rejection; this is read from source, not run end to end.
Thread-read: none
Serial constraints cleared: none. No pm:dispatched card is in the engine lane, and none of the 8 open PRs touches driver-sqlite-wasm (read just now).
The card's read-only item (a bootstrap that fails after the driver connected never closes it) is not folded in. The dev measures where that driver is left open and reports it as an out-of-scope finding with its reach. The seat files it after review if it falls in the three classes.
Seat 2's Claim: 6187511326 (session_01ADzJtzYTLUfgrRZHxkagkX, 08:07Z) is the earlier record, so this card is seat 2's.
Seat 1 picked it from the queue read at 08:04Z. Its assignee write landed at 08:14Z, after seat 2's.
Seat 1 removes only its own assignee (sunhaolin). os-steve, pm:dispatched and seat 2's branch are untouched.
Seat 1 posted no Claim: and dispatched no dev on this card.
Handed over (a clue, not an answer). On origin/main23e689dc7d, git grep -n "unhandledRejection" origin/main -- packages/ finds no handler in any package's production source: three files, all test helpers or test-only driver-mongodb code. The control process.on( finds 13 hits in packages/cli/src plus packages/runtime/src.
The radius is a content grep of those packages' sources for the literal name. A handler installed by a dependency, or a --unhandled-rejections flag on a launcher outside packages/, is outside it.
{
"issue": 23635,
"status": "done",
"branch": "claude/issue-23635-background-flush-rejection",
"pr": "https://github.com/objectstack-ai/objectstack/pull/23643",
"session": "session_01ADzJtzYTLUfgrRZHxkagkX",
"premise_still_valid": true,
"summary": "Premise re-verified on origin/main 23e689dc7d: the three bare void this.flush() sites (noteTransactionControl, markDirty on-write, the debounce timer) existed and flush() re-marks dirty and rethrows. They now call one private helper, flushInBackground(), whose rejection handler reports once per failure episode at error (warn for a logger without error), naming the file, the consequence and the fix; dirty stays set so the next flush retries, and an awaited flush()/close() still throws (public type surface unchanged, so patch changeset and Clause-② no stand). A new 10-test file pins each call site, the retry, once-per-episode, report robustness, the CONTROL (awaited flush()/close() still reject with ENOENT) and the dogfood race through a live driver; the no-unhandled-rejection assertion is the test's own process.on(unhandledRejection) listener, not Vitest's report. The card's read-only item (bootstrap that fails after connect never closes the driver) is measured, not fixed: kernel.ts bootstrap() catch never runs plugin destroy() for a failure at kernel:ready; reported below. Commit trailers are the model-free pair AGENTS.md requires; the harness reminder's model-named Co-Authored-By line was not used. Label writes: zero (the dispatch named none, and skip-changeset does not apply: a patch changeset is present). CI convergence not awaited; PR is draft.",
"tests": "pnpm --filter @objectstack/driver-sqlite-wasm typecheck: exit 0. Whole package suite (vitest run --maxWorkers=2, under os-verify-lock, VERDICT command-exit 0) at e3b5224c85: Test Files 37 passed, Tests 685 passed; the new file alone 10/10. ABLATION (9 legs via scripts/ablation-replace.mjs: anchor hit 1->0, blob changed on disk, restore proven blob == HEAD and git diff HEAD empty; the test imports src directly so no dist rebuild leg applies): on-write site bare = 6/10 red; debounce site bare = 1/10 red (its own test); transaction-close site bare = 1/10 red (its own test); helper body bare (all three) = 8/10 red with both CONTROL tests green; once-guard removed = 1/10; latch reset removed = 1/10; logger try/catch removed = 1/10; warn fallback removed = 1/10 (re-run on the final spelling); dirty=true-on-failure removed = 8/10 incl. the retry test. First once-guard attempt was a no-op refused by the tool (replacement was a substring of the anchor) and redone. Observed direction: red (the real ENOENT open of the tmp file reaches the test listener). node scripts/pm/dispatch-gates.mjs --commands derived 62 commands from this tree at e3b5224c85; all 62 run at that commit, every exit 0; --ran reconciliation: 62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN (records carry exit codes). In the first sweep check:logger-receiver-detach was red on my first spelling (const sink = this.logger.error ?? this.logger.warn) and was fixed in the last commit; check:lean-entry-closure and check:dual-build-cjs-loads exited 3 (prerequisite: no dist) and were not counted as results, then exit 0 once the dists existed. pnpm check:durability-log-level: green. ESLINT NARROWED (declared): eslint --no-inline-config --format json on the 2 changed source files at e3b5224c85: files 2, errors 0, warnings 0; the repo config never enables type-aware linting (no parserOptions.project, no typed rules; eslint.config.mjs ~:328), so this diff cannot move the verdict of an untouched file; repo-wide pnpm lint left to CI. Control-byte scan over the 3 files: none. NOT MEASURED: Dogfood Regression Gate (2/3) and the dogfood file itself (CI-owned; the race is intermittent, so a local green proves nothing; the pin reproduces the mechanism deterministically); repo-wide pnpm lint.",
"mcp_calls": "0 — no MCP GitHub calls",
"api_writes": "2 REST writes, both through the fleet-write relay (objectstack-fleet[bot], run 38710815363 for the first): (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/23643/assignees [os-steve], body read back 8383 bytes sent / 8383 stored identical; (2) comment = POST /repos/objectstack-ai/objectstack/issues/23635/comments (this report). label-write: 0. git push (not REST): 5, all to claude/issue-23635-background-flush-rejection. No card assignee write.",
"open_questions": [
{
"question": "Should the exported WasmConnectionOptions logger option type declare an optional error member?",
"options": [
"A. Leave it untyped, as shipped: the connection reads error off the injected logger when present; patch changeset, Clause-② no.",
"B. Declare it: a typed caller can pass a logger with error without a cast, but it widens a published surface, so Clause-② yes (widening) and a minor changeset."
],
"recommendation": "A for this card (it keeps the declared patch and Clause-② no); B only as its own card if a typed caller needs it."
}
],
"out_of_scope_findings": [
"class: a · reach: named producer — packages/qa/dogfood/test/organization-ownership-boot-refusal-order.dogfood.test.ts test 1 (its refused bootShowcase boot leaves the SQLite driver connected; its own docblock records the driver never torn down and a later boot over the same path meeting the stale image), and any in-process host awaiting Runtime.start() (packages/runtime/src/runtime.ts) or bootStack (packages/verify/src/harness.ts, await kernel.bootstrap()); os serve exits, so it is not reached there · evidence (measured, file and path): connect is ObjectQLPlugin.start (packages/objectql/src/plugin.ts, await this.ql?.init()) -> ObjectQLEngine.init driver.connect() (packages/objectql/src/engine.ts); the failure is a kernel:ready throw, Phase 3 of ObjectKernel.bootstrap() (packages/core/src/kernel.ts, await this.context.trigger kernel:ready); rollbackStartedPlugins() is called only in the Phase 2 branch for a plugin whose start() failed, and the outer catch of bootstrap() sets state stopped, releases signal listeners and rethrows (its comment: shutdown() on such a kernel is a no-op), so no plugin destroy() runs, DefaultDatasourcePlugin.destroy (packages/runtime/src/default-datasource-plugin.ts) included. Probe on core dist, scratch script outside the repo: plugin A start() records connect and destroy() records disconnect, plugin B throws at kernel:ready: result [driver.connect(), bootstrap rejected: refused at kernel:ready, shutdown() returned], no disconnect; positive control, same kernel with plugin B start() throwing (Phase 2 rollback): [driver.connect(), driver.disconnect() via destroy(), bootstrap rejected], so the probe can see a destroy · dedupe words: ObjectKernel.bootstrap rollbackOnFailure; kernel:ready throw plugin destroy never called; refused boot driver never disconnected; DefaultDatasourcePlugin.destroy not run on failed bootstrap; bootStack bootstrap failure no teardown",
"carrier: 承接者:无 · noted, not filed — SqliteWasmDriver.connect() registers a beforeExit handler (packages/drivers/driver-sqlite-wasm/src/sqlite-wasm-driver.ts) calling void this.flush().catch(() => { /* ignore */ }), so a flush failing at process exit is swallowed with no log; read from source, not run; outside this card's file surface · dedupe words: beforeExit flush swallowed; sqlite-wasm on-disconnect exit flush silent catch"
]
}
{
"issue": 23635,
"status": "done",
"branch": "claude/issue-23635-background-flush-rejection",
"pr": "https://github.com/objectstack-ai/objectstack/pull/23643",
"session": "session_01ADzJtzYTLUfgrRZHxkagkX",
"premise_still_valid": true,
"summary": "PATCH ROUND (new head aa42889b36, was e3b5224c85; PR still draft). The red was check:optional-error-sink (id optional-error-sink-contract): reproduced before the fix (exit 1, wasm-connection.ts:118, inline type logger@WasmSqliteConnection { error? }, \"error is optional and NO warn is declared\"), as the seat read it. Fix is one type: the private field is now ONE literal, { warn: (msg: string, meta?: unknown) => void; error?: (msg: string, meta?: unknown) => void }, instead of the NonNullable wrapper over the exported logger option type intersected with { error? }. The gate now reads 37 sinks as optional beside a REQUIRED warn (was 36), 0 no-fallback, exit 0. error was NOT made required, the exported WasmConnectionOptions logger type is untouched, and the .call(this.logger, ...) receiver form is kept (check:logger-receiver-detach green). Open question answered A by the coordinator: the exported type stays unchanged, patch changeset, Clause-② no. Why the 62-command sweep missed it: check:optional-error-sink is a declared WIDE-population family (scanning every non-test .ts under packages), which dispatch-gates --commands does not print; this round ran the artifact-roster block AND the wide-population block. NOTE for the seat: the PR body cites e3b5224c85 in its Verification section and says the sweep ran at that commit; commit aa42889b36 changes only the private field type in wasm-connection.ts, so the body edit to make is: replace that sha with aa42889b36 and add that check:optional-error-sink (wide-population) now exits 0 (dev may not PATCH the body; seat writes it). Commit trailers are the model-free pair; no label writes.",
"tests": "All at head aa42889b36 in a fresh worktree (pnpm install --frozen-lockfile; dependency closure built with pnpm --workspace-concurrency=2 --filter driver-sqlite-wasm^... build). pnpm check:optional-error-sink: exit 1 before the fix (reproduced), exit 0 after (\"every sink declaring an optional error guarantees a warn channel (1 baselined, shrink-only)\"; census: 54 sink types, 16 required, 37 optional beside required warn, 1 permit silence = 1 optional-fallback, 0 no-fallback). pnpm check:durability-log-level: exit 0. pnpm check:logger-receiver-detach: exit 0 (3421 files, 0 detaches). pnpm --filter @objectstack/driver-sqlite-wasm typecheck: exit 0. Driver suite (vitest run --maxWorkers=2 under os-verify-lock, VERDICT command-exit 0): Test Files 37 passed, Tests 685 passed; the new sqlite-wasm-background-flush-failure.test.ts alone: 10/10 passed. eslint --no-inline-config --format json on the 2 changed source files: files 2, errors 0, warnings 0 (config never enables type-aware linting, so the diff cannot move untouched files; repo-wide pnpm lint left to CI). ARTIFACT-ROSTER block of node scripts/pm/dispatch-gates.mjs (53 rows incl. the 15 self-test-only rows), each run with its own command, exit codes: 48 exit 0 on first run; 4 exit 2 or 1 with NOT MEASURED / NOT WIRED because they need a PR context (check-closing-target-claim.mjs, check-partof-closing-keyword.mjs, check-single-claim-paths.mjs, pm/check-governed-queue-guard.mjs) and were rerun with the live PR context (PR_NUMBER 23643, the live PR body, PR_HEAD_REF, GITHUB_REPOSITORY; the queue guard with the live PR GET payload as GITHUB_EVENT_PATH): all four exit 0 (closing-target-claim: \"PR #23643 closes #23635, and each carries a Claim: whose Branch: line names claude/issue-23635-background-flush-rejection\"; queue guard: CLEAR, the diff touches no governed surface); 1 exit 3 (check:published-readme-exports, no dist for 43 packages = NOT MEASURED, not a result) rerun after the dists existed: exit 0. So 53 of 53 exit 0 in the end. DECLARED WIDE-POPULATION block (11 families: init-service-contract, live-db-isolation, meta-type-normalized, optional-error-sink, resume-authority-declared, route-envelope, runner-env-posture, settings-bind-window, startup-registry-verdict, verify-stand-in, wildcard-fallthrough): all 11 exit 0. Re-derived --commands at this head: 62 commands; all 62 run, 60 exit 0 first time and check:dual-build-cjs-loads and check:lean-entry-closure exit 3 (prerequisite: no dist in the fresh worktree, NOT results), both exit 0 once the dists existed; --ran reconciliation: 62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN. Ablation evidence from the previous round stands unchanged for the behaviour (only a field type literal changed; no runtime line). NOT MEASURED, as before: Dogfood Regression Gate (2/3) and the dogfood file (CI-owned, intermittent), repo-wide pnpm lint (CI-owned); not run this round: the always-runs tail and CI-job shells that --commands never names (pnpm lint, the type-check lanes over the whole workspace, Test Core shards), CI-owned.",
"mcp_calls": "0 — no MCP GitHub calls",
"api_writes": "1 REST write this round: comment = POST /repos/objectstack-ai/objectstack/issues/23635/comments through the fleet-write relay (this report). git push (not REST): 1, to claude/issue-23635-background-flush-rejection (aa42889b36). No PR body PATCH, no label write, no card assignee write.",
"open_questions": [],
"out_of_scope_findings": [
"carry-over from the first report, unchanged: class: a · reach: named producer — organization-ownership-boot-refusal-order.dogfood.test.ts test 1 and any in-process host awaiting Runtime.start() (packages/runtime/src/runtime.ts) or bootStack (packages/verify/src/harness.ts) · evidence: ObjectKernel.bootstrap() (packages/core/src/kernel.ts) runs rollbackStartedPlugins() only in the Phase 2 start()-failure branch, its outer catch sets state stopped and rethrows, so a kernel:ready throw never runs any plugin destroy(); probe on core dist: [driver.connect(), bootstrap rejected: refused at kernel:ready, shutdown() returned] with no disconnect, positive control (Phase 2 failure) shows destroy · dedupe words: ObjectKernel.bootstrap rollbackOnFailure; kernel:ready throw plugin destroy never called; refused boot driver never disconnected; DefaultDatasourcePlugin.destroy not run on failed bootstrap",
"carry-over, unchanged: carrier: 承接者:无 · noted, not filed — the SqliteWasmDriver beforeExit handler swallows a failing exit-time flush with no log (packages/drivers/driver-sqlite-wasm/src/sqlite-wasm-driver.ts), read from source, not run · dedupe words: beforeExit flush swallowed; sqlite-wasm on-disconnect exit flush silent catch",
"new, process note (not a card): the PR-derived command list (dispatch-gates --commands) does not print the declared WIDE-population family check:optional-error-sink even though a diff that adds an inline logger type literal anywhere under packages reaches it; the run that catches it is the full dispatch-gates output (wide-population block) or CI · carrier: 承接者:无 · noted, not filed"
]
}
the dev's two reports on this card (the first round, then the patch round);
the seat's own reading of the full diff against main;
the patch delta e3b5224c85..aa42889b36, which is one private field type in wasm-connection.ts, read line by line.
Review (in-seat, the full diff read):
Producer fix. The three fire-and-forget sites (the transaction close, the on-write write, the debounced: timer) now call one helper, flushInBackground(). It catches the rejection and reports it, so no call site leaves one to the process.
The flush itself is unchanged.flush() keeps its existing dirty = true; throw and its caught chain tail (this.flushChain = step.catch(() => {})). So a failed background flush is retried by the next one, and an awaited flush() / close() still throws.
The report. It comes once per failure episode, and the latch clears only when a flush writes. It is at error, falling back to warn, through the .call(this.logger, …) receiver form. It names the file, the consequence (writes since the last good flush are not on disk) and the remedy. The logger call is wrapped, so a throwing logger cannot become the rejection.
The pin. 10 tests: one per call site, the retry landing all rows, once per episode, a warn-only logger, a throwing logger, and the two CONTROLs (an awaited flush() and close() still reject with ENOENT and do not also log). The last test drives the dogfood shape through a live driver.
"No unhandled rejection" is asserted by the test's own process.on('unhandledRejection') listener, not by Vitest's run report.
The dev's ablation took each guard out in turn; every leg turned the pin red, with the CONTROLs green.
Public surface. It is unchanged. The exported WasmConnectionOptions['logger'] type is untouched; error is read only through the private field's type. Clause-②: no, with a patch changeset.
⛔ The dogfood file is untouched: no retry, skip or quarantine, and its afterAll stays.
The dev's open question (type error on the exported logger option): answered A by the seat. The exported type stays unchanged. B would widen a published surface for no named caller, so it is not taken and not filed.
The patch round. The first head e3b5224c85 was red on one gate, check:optional-error-sink. The private type's inline literal { error? } declared an optional error with no warn, read through an intersection the gate does not follow. Now the type is one literal with warn required and error optional. That gate exits 0, and so do check:durability-log-level and check:logger-receiver-detach. The miss is the known blind spot in dispatch-gates --commands (#14880): wide-population and artifact-roster families are invisible to a --commands harvest. This round ran both blocks.
Landing checks at aa42889b36:
ci-failure.mjs --pr 23643 is GREEN: 40 of 40 check runs completed, none failed.
git merge-tree against maina647783d46 is clean.
The path face is 3 files, none on a governed surface. The size is 383 changed lines, under the 3000 threshold.
The body carries Fixes #23635. The closing-target claim guard is green.
Out-of-scope findings:
filed #23644: a bootstrap that fails after every plugin started (a throw at kernel:ready, kernel:bootstrapped or kernel:listening) never runs a plugin destroy(). The seat checked it against packages/core/src/kernel.ts (:564 is the only rollback; the outer catch is at :621–:626).
Acceptance notes: SqliteWasmDriver's beforeExit handler swallows a failing exit-time flush with no log (packages/drivers/driver-sqlite-wasm/src/sqlite-wasm-driver.ts:146–:152). Read from source; its reach is not measured. Recorded for the next card that touches the driver's exit path.
Merged by the merge queue as c7dd48ab4c128e7b9fd2abc7cd3b1f795933c8ae, from head aa42889b36, on its first queue build: added 2026-10-15T09:48Z, merged 2026-10-15T10:18Z. The queue's removal event in the same second is the merge itself. The ACCEPT is 6189216665. Fixes #23635 closed this card.
Squash verified. The squash's diff against its parent 7cb33cfefe equals the head's diff against their merge-base 23e689dc7d: the same 3 files, and every file's changed lines identical (+378 / −5).
What landed. The three fire-and-forget flushes in WasmSqliteConnection go through flushInBackground(). A failed one is reported once per episode at error, naming the file, and dirty stays set for the retry. An awaited flush() / close() still throws. The 10-test pin is in place.
Done line. The card's last line, "the dogfood shards stay green on main", is now a watch, not a measurement: the race was intermittent, so the next main runs answer it. If a Dogfood shard goes red on this unhandled rejection again, that reopens this card.
Activity
objectstack-fleet commentedon Oct 15, 2026
Claim: PM loop round 2 · 2026-10-15T08:07Z · on the maintainer's 「engine 车道还有哪些 p2 的小卡你可以认领的, 20x 周套餐还剩6%,你可以串行派发。」 (this session), which lifts this seat's earlier no-new-dispatch scope for serial p2 work
Session:
session_01ADzJtzYTLUfgrRZHxkagkXAccount:
os-steve(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23635-background-flush-rejectionWorktree:
objectstack-issue-23635Domain:
domain:engineSeat:
domain:engine#2(seat post #20966)File surface:
packages/drivers/driver-sqlite-wasm/src/wasm-connection.ts(the threevoid this.flush()sites at:329,:352,:359andflush()at:377), a new or existing test beside it inpackages/drivers/driver-sqlite-wasm, and one changeset. ⛔ Notorganization-ownership-boot-refusal-order.dogfood.test.ts: no retry, skip or quarantine, and itsafterAllcleanup stays. Stop on a breach and explain it in the report.Container & model:
S,mode:subagent,model: floor(this run'sdispatch-gates.mjs --tier: "no path-derived mandate … floor · default · ceiling"; one file's background-error handling plus a pin, under the maintainer's budget note)Clause-②: no
Responsibility: the risk is produced by
driver-sqlite-wasm's own fire-and-forgetvoid this.flush()calls. No platform path covers it: onmain23e689dc7d,git grep -n -E "unhandledRejection|unhandled-rejections" origin/main -- 'packages/cli/**' 'packages/runtime/**' 'packages/core/src/**'finds no handler, so the process default applies. It is reached today by CI (main's Dogfood shard 2/3, intermittent) and by in-process hosts. By the same reading, a productionos servewhose background flush fails (ENOSPC, a removed data directory) meets the same unhandled rejection; this is read from source, not run end to end.Thread-read: none
Serial constraints cleared: none. No
pm:dispatchedcard is in the engine lane, and none of the 8 open PRs touchesdriver-sqlite-wasm(read just now).The card's read-only item (a bootstrap that fails after the driver connected never closes it) is not folded in. The dev measures where that driver is left open and reports it as an out-of-scope finding with its reach. The seat files it after review if it falls in the three classes.
Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
already claimed — yielding ·
domain:engineseat 1 ·sunhaolin·session_01GP3comALHRcRBixS2TEnLF· 2026-10-15T08:15ZSeat 2's
Claim:6187511326 (session_01ADzJtzYTLUfgrRZHxkagkX, 08:07Z) is the earlier record, so this card is seat 2's.sunhaolin).os-steve,pm:dispatchedand seat 2's branch are untouched.Claim:and dispatched no dev on this card.Handed over (a clue, not an answer). On
origin/main23e689dc7d,git grep -n "unhandledRejection" origin/main -- packages/finds no handler in any package's production source: three files, all test helpers or test-onlydriver-mongodbcode. The controlprocess.on(finds 13 hits inpackages/cli/srcpluspackages/runtime/src.--unhandled-rejectionsflag on a launcher outsidepackages/, is outside it.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{ "issue": 23635, "status": "done", "branch": "claude/issue-23635-background-flush-rejection", "pr": "https://github.com/objectstack-ai/objectstack/pull/23643", "session": "session_01ADzJtzYTLUfgrRZHxkagkX", "premise_still_valid": true, "summary": "Premise re-verified on origin/main 23e689dc7d: the three bare void this.flush() sites (noteTransactionControl, markDirty on-write, the debounce timer) existed and flush() re-marks dirty and rethrows. They now call one private helper, flushInBackground(), whose rejection handler reports once per failure episode at error (warn for a logger without error), naming the file, the consequence and the fix; dirty stays set so the next flush retries, and an awaited flush()/close() still throws (public type surface unchanged, so patch changeset and Clause-② no stand). A new 10-test file pins each call site, the retry, once-per-episode, report robustness, the CONTROL (awaited flush()/close() still reject with ENOENT) and the dogfood race through a live driver; the no-unhandled-rejection assertion is the test's own process.on(unhandledRejection) listener, not Vitest's report. The card's read-only item (bootstrap that fails after connect never closes the driver) is measured, not fixed: kernel.ts bootstrap() catch never runs plugin destroy() for a failure at kernel:ready; reported below. Commit trailers are the model-free pair AGENTS.md requires; the harness reminder's model-named Co-Authored-By line was not used. Label writes: zero (the dispatch named none, and skip-changeset does not apply: a patch changeset is present). CI convergence not awaited; PR is draft.", "tests": "pnpm --filter @objectstack/driver-sqlite-wasm typecheck: exit 0. Whole package suite (vitest run --maxWorkers=2, under os-verify-lock, VERDICT command-exit 0) at e3b5224c85: Test Files 37 passed, Tests 685 passed; the new file alone 10/10. ABLATION (9 legs via scripts/ablation-replace.mjs: anchor hit 1->0, blob changed on disk, restore proven blob == HEAD and git diff HEAD empty; the test imports src directly so no dist rebuild leg applies): on-write site bare = 6/10 red; debounce site bare = 1/10 red (its own test); transaction-close site bare = 1/10 red (its own test); helper body bare (all three) = 8/10 red with both CONTROL tests green; once-guard removed = 1/10; latch reset removed = 1/10; logger try/catch removed = 1/10; warn fallback removed = 1/10 (re-run on the final spelling); dirty=true-on-failure removed = 8/10 incl. the retry test. First once-guard attempt was a no-op refused by the tool (replacement was a substring of the anchor) and redone. Observed direction: red (the real ENOENT open of the tmp file reaches the test listener). node scripts/pm/dispatch-gates.mjs --commands derived 62 commands from this tree at e3b5224c85; all 62 run at that commit, every exit 0; --ran reconciliation: 62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN (records carry exit codes). In the first sweep check:logger-receiver-detach was red on my first spelling (const sink = this.logger.error ?? this.logger.warn) and was fixed in the last commit; check:lean-entry-closure and check:dual-build-cjs-loads exited 3 (prerequisite: no dist) and were not counted as results, then exit 0 once the dists existed. pnpm check:durability-log-level: green. ESLINT NARROWED (declared): eslint --no-inline-config --format json on the 2 changed source files at e3b5224c85: files 2, errors 0, warnings 0; the repo config never enables type-aware linting (no parserOptions.project, no typed rules; eslint.config.mjs ~:328), so this diff cannot move the verdict of an untouched file; repo-wide pnpm lint left to CI. Control-byte scan over the 3 files: none. NOT MEASURED: Dogfood Regression Gate (2/3) and the dogfood file itself (CI-owned; the race is intermittent, so a local green proves nothing; the pin reproduces the mechanism deterministically); repo-wide pnpm lint.", "mcp_calls": "0 — no MCP GitHub calls", "api_writes": "2 REST writes, both through the fleet-write relay (objectstack-fleet[bot], run 38710815363 for the first): (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/23643/assignees [os-steve], body read back 8383 bytes sent / 8383 stored identical; (2) comment = POST /repos/objectstack-ai/objectstack/issues/23635/comments (this report). label-write: 0. git push (not REST): 5, all to claude/issue-23635-background-flush-rejection. No card assignee write.", "open_questions": [ { "question": "Should the exported WasmConnectionOptions logger option type declare an optional error member?", "options": [ "A. Leave it untyped, as shipped: the connection reads error off the injected logger when present; patch changeset, Clause-② no.", "B. Declare it: a typed caller can pass a logger with error without a cast, but it widens a published surface, so Clause-② yes (widening) and a minor changeset." ], "recommendation": "A for this card (it keeps the declared patch and Clause-② no); B only as its own card if a typed caller needs it." } ], "out_of_scope_findings": [ "class: a · reach: named producer — packages/qa/dogfood/test/organization-ownership-boot-refusal-order.dogfood.test.ts test 1 (its refused bootShowcase boot leaves the SQLite driver connected; its own docblock records the driver never torn down and a later boot over the same path meeting the stale image), and any in-process host awaiting Runtime.start() (packages/runtime/src/runtime.ts) or bootStack (packages/verify/src/harness.ts, await kernel.bootstrap()); os serve exits, so it is not reached there · evidence (measured, file and path): connect is ObjectQLPlugin.start (packages/objectql/src/plugin.ts, await this.ql?.init()) -> ObjectQLEngine.init driver.connect() (packages/objectql/src/engine.ts); the failure is a kernel:ready throw, Phase 3 of ObjectKernel.bootstrap() (packages/core/src/kernel.ts, await this.context.trigger kernel:ready); rollbackStartedPlugins() is called only in the Phase 2 branch for a plugin whose start() failed, and the outer catch of bootstrap() sets state stopped, releases signal listeners and rethrows (its comment: shutdown() on such a kernel is a no-op), so no plugin destroy() runs, DefaultDatasourcePlugin.destroy (packages/runtime/src/default-datasource-plugin.ts) included. Probe on core dist, scratch script outside the repo: plugin A start() records connect and destroy() records disconnect, plugin B throws at kernel:ready: result [driver.connect(), bootstrap rejected: refused at kernel:ready, shutdown() returned], no disconnect; positive control, same kernel with plugin B start() throwing (Phase 2 rollback): [driver.connect(), driver.disconnect() via destroy(), bootstrap rejected], so the probe can see a destroy · dedupe words: ObjectKernel.bootstrap rollbackOnFailure; kernel:ready throw plugin destroy never called; refused boot driver never disconnected; DefaultDatasourcePlugin.destroy not run on failed bootstrap; bootStack bootstrap failure no teardown", "carrier: 承接者:无 · noted, not filed — SqliteWasmDriver.connect() registers a beforeExit handler (packages/drivers/driver-sqlite-wasm/src/sqlite-wasm-driver.ts) calling void this.flush().catch(() => { /* ignore */ }), so a flush failing at process exit is swallowed with no log; read from source, not run; outside this card's file surface · dedupe words: beforeExit flush swallowed; sqlite-wasm on-disconnect exit flush silent catch" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{ "issue": 23635, "status": "done", "branch": "claude/issue-23635-background-flush-rejection", "pr": "https://github.com/objectstack-ai/objectstack/pull/23643", "session": "session_01ADzJtzYTLUfgrRZHxkagkX", "premise_still_valid": true, "summary": "PATCH ROUND (new head aa42889b36, was e3b5224c85; PR still draft). The red was check:optional-error-sink (id optional-error-sink-contract): reproduced before the fix (exit 1, wasm-connection.ts:118, inline type logger@WasmSqliteConnection { error? }, \"error is optional and NO warn is declared\"), as the seat read it. Fix is one type: the private field is now ONE literal, { warn: (msg: string, meta?: unknown) => void; error?: (msg: string, meta?: unknown) => void }, instead of the NonNullable wrapper over the exported logger option type intersected with { error? }. The gate now reads 37 sinks as optional beside a REQUIRED warn (was 36), 0 no-fallback, exit 0. error was NOT made required, the exported WasmConnectionOptions logger type is untouched, and the .call(this.logger, ...) receiver form is kept (check:logger-receiver-detach green). Open question answered A by the coordinator: the exported type stays unchanged, patch changeset, Clause-② no. Why the 62-command sweep missed it: check:optional-error-sink is a declared WIDE-population family (scanning every non-test .ts under packages), which dispatch-gates --commands does not print; this round ran the artifact-roster block AND the wide-population block. NOTE for the seat: the PR body cites e3b5224c85 in its Verification section and says the sweep ran at that commit; commit aa42889b36 changes only the private field type in wasm-connection.ts, so the body edit to make is: replace that sha with aa42889b36 and add that check:optional-error-sink (wide-population) now exits 0 (dev may not PATCH the body; seat writes it). Commit trailers are the model-free pair; no label writes.", "tests": "All at head aa42889b36 in a fresh worktree (pnpm install --frozen-lockfile; dependency closure built with pnpm --workspace-concurrency=2 --filter driver-sqlite-wasm^... build). pnpm check:optional-error-sink: exit 1 before the fix (reproduced), exit 0 after (\"every sink declaring an optional error guarantees a warn channel (1 baselined, shrink-only)\"; census: 54 sink types, 16 required, 37 optional beside required warn, 1 permit silence = 1 optional-fallback, 0 no-fallback). pnpm check:durability-log-level: exit 0. pnpm check:logger-receiver-detach: exit 0 (3421 files, 0 detaches). pnpm --filter @objectstack/driver-sqlite-wasm typecheck: exit 0. Driver suite (vitest run --maxWorkers=2 under os-verify-lock, VERDICT command-exit 0): Test Files 37 passed, Tests 685 passed; the new sqlite-wasm-background-flush-failure.test.ts alone: 10/10 passed. eslint --no-inline-config --format json on the 2 changed source files: files 2, errors 0, warnings 0 (config never enables type-aware linting, so the diff cannot move untouched files; repo-wide pnpm lint left to CI). ARTIFACT-ROSTER block of node scripts/pm/dispatch-gates.mjs (53 rows incl. the 15 self-test-only rows), each run with its own command, exit codes: 48 exit 0 on first run; 4 exit 2 or 1 with NOT MEASURED / NOT WIRED because they need a PR context (check-closing-target-claim.mjs, check-partof-closing-keyword.mjs, check-single-claim-paths.mjs, pm/check-governed-queue-guard.mjs) and were rerun with the live PR context (PR_NUMBER 23643, the live PR body, PR_HEAD_REF, GITHUB_REPOSITORY; the queue guard with the live PR GET payload as GITHUB_EVENT_PATH): all four exit 0 (closing-target-claim: \"PR #23643 closes #23635, and each carries a Claim: whose Branch: line names claude/issue-23635-background-flush-rejection\"; queue guard: CLEAR, the diff touches no governed surface); 1 exit 3 (check:published-readme-exports, no dist for 43 packages = NOT MEASURED, not a result) rerun after the dists existed: exit 0. So 53 of 53 exit 0 in the end. DECLARED WIDE-POPULATION block (11 families: init-service-contract, live-db-isolation, meta-type-normalized, optional-error-sink, resume-authority-declared, route-envelope, runner-env-posture, settings-bind-window, startup-registry-verdict, verify-stand-in, wildcard-fallthrough): all 11 exit 0. Re-derived --commands at this head: 62 commands; all 62 run, 60 exit 0 first time and check:dual-build-cjs-loads and check:lean-entry-closure exit 3 (prerequisite: no dist in the fresh worktree, NOT results), both exit 0 once the dists existed; --ran reconciliation: 62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN. Ablation evidence from the previous round stands unchanged for the behaviour (only a field type literal changed; no runtime line). NOT MEASURED, as before: Dogfood Regression Gate (2/3) and the dogfood file (CI-owned, intermittent), repo-wide pnpm lint (CI-owned); not run this round: the always-runs tail and CI-job shells that --commands never names (pnpm lint, the type-check lanes over the whole workspace, Test Core shards), CI-owned.", "mcp_calls": "0 — no MCP GitHub calls", "api_writes": "1 REST write this round: comment = POST /repos/objectstack-ai/objectstack/issues/23635/comments through the fleet-write relay (this report). git push (not REST): 1, to claude/issue-23635-background-flush-rejection (aa42889b36). No PR body PATCH, no label write, no card assignee write.", "open_questions": [], "out_of_scope_findings": [ "carry-over from the first report, unchanged: class: a · reach: named producer — organization-ownership-boot-refusal-order.dogfood.test.ts test 1 and any in-process host awaiting Runtime.start() (packages/runtime/src/runtime.ts) or bootStack (packages/verify/src/harness.ts) · evidence: ObjectKernel.bootstrap() (packages/core/src/kernel.ts) runs rollbackStartedPlugins() only in the Phase 2 start()-failure branch, its outer catch sets state stopped and rethrows, so a kernel:ready throw never runs any plugin destroy(); probe on core dist: [driver.connect(), bootstrap rejected: refused at kernel:ready, shutdown() returned] with no disconnect, positive control (Phase 2 failure) shows destroy · dedupe words: ObjectKernel.bootstrap rollbackOnFailure; kernel:ready throw plugin destroy never called; refused boot driver never disconnected; DefaultDatasourcePlugin.destroy not run on failed bootstrap", "carry-over, unchanged: carrier: 承接者:无 · noted, not filed — the SqliteWasmDriver beforeExit handler swallows a failing exit-time flush with no log (packages/drivers/driver-sqlite-wasm/src/sqlite-wasm-driver.ts), read from source, not run · dedupe words: beforeExit flush swallowed; sqlite-wasm on-disconnect exit flush silent catch", "new, process note (not a card): the PR-derived command list (dispatch-gates --commands) does not print the declared WIDE-population family check:optional-error-sink even though a diff that adds an inline logger type literal anywhere under packages reaches it; the run that catches it is the full dispatch-gates output (wide-population block) or CI · carrier: 承接者:无 · noted, not filed" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
✅ ACCEPT: PR #23643 at
aa42889b36·domain:engineseat 2 ·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-15T09:45ZBasis:
main;e3b5224c85..aa42889b36, which is one private field type inwasm-connection.ts, read line by line.Review (in-seat, the full diff read):
on-writewrite, thedebounced:timer) now call one helper,flushInBackground(). It catches the rejection and reports it, so no call site leaves one to the process.flush()keeps its existingdirty = true; throwand its caught chain tail (this.flushChain = step.catch(() => {})). So a failed background flush is retried by the next one, and an awaitedflush()/close()still throws.error, falling back towarn, through the.call(this.logger, …)receiver form. It names the file, the consequence (writes since the last good flush are not on disk) and the remedy. The logger call is wrapped, so a throwing logger cannot become the rejection.warn-only logger, a throwing logger, and the two CONTROLs (an awaitedflush()andclose()still reject withENOENTand do not also log). The last test drives the dogfood shape through a live driver.process.on('unhandledRejection')listener, not by Vitest's run report.WasmConnectionOptions['logger']type is untouched;erroris read only through the private field's type.Clause-②: no, with a patch changeset.afterAllstays.The dev's open question (type
erroron the exported logger option): answered A by the seat. The exported type stays unchanged. B would widen a published surface for no named caller, so it is not taken and not filed.The patch round. The first head
e3b5224c85was red on one gate,check:optional-error-sink. The private type's inline literal{ error? }declared an optionalerrorwith nowarn, read through an intersection the gate does not follow. Now the type is one literal withwarnrequired anderroroptional. That gate exits 0, and so docheck:durability-log-levelandcheck:logger-receiver-detach. The miss is the known blind spot indispatch-gates --commands(#14880): wide-population and artifact-roster families are invisible to a--commandsharvest. This round ran both blocks.Landing checks at
aa42889b36:ci-failure.mjs --pr 23643is GREEN: 40 of 40 check runs completed, none failed.git merge-treeagainstmaina647783d46is clean.Fixes #23635. The closing-target claim guard is green.Out-of-scope findings:
filed #23644: a bootstrap that fails after every plugin started (a throw atkernel:ready,kernel:bootstrappedorkernel:listening) never runs a plugindestroy(). The seat checked it againstpackages/core/src/kernel.ts(:564is the only rollback; the outer catch is at:621–:626).Acceptance notes:SqliteWasmDriver'sbeforeExithandler swallows a failing exit-time flush with no log (packages/drivers/driver-sqlite-wasm/src/sqlite-wasm-driver.ts:146–:152). Read from source; its reach is not measured. Recorded for the next card that touches the driver's exit path.dropped — known: the--commandsblind spot for wide-population families is [finding] dispatch-gates --commands does not derive check:optional-error-sink for a diff that adds a new logger-sink interface #14880's subject. No new card.Next: ready, then auto-merge through the queue.
Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Landing record: PR #23643 →
c7dd48ab4c·domain:engineseat 2 ·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-15T10:19Zc7dd48ab4c128e7b9fd2abc7cd3b1f795933c8ae, from headaa42889b36, on its first queue build: added 2026-10-15T09:48Z, merged 2026-10-15T10:18Z. The queue's removal event in the same second is the merge itself. The ACCEPT is 6189216665.Fixes #23635closed this card.7cb33cfefeequals the head's diff against their merge-base23e689dc7d: the same 3 files, and every file's changed lines identical (+378 / −5).WasmSqliteConnectiongo throughflushInBackground(). A failed one is reported once per episode aterror, naming the file, anddirtystays set for the retry. An awaitedflush()/close()still throws. The 10-test pin is in place.main", is now a watch, not a measurement: the race was intermittent, so the nextmainruns answer it. If a Dogfood shard goes red on this unhandled rejection again, that reopens this card.beforeExitsilent catch stays an Acceptance note.pm:dispatchedis removed in the same act.Generated by Claude Code