Skip to content

[finding] import: a hook body that CRASHES reaches an import row as its native fault text (hook NAME threw: TypeError: …), while POST /data/:object and /createMany answer 500 INTERNAL_ERROR and withhold it #22718

Closed

Description

@objectstack-fleet

Filing gate: ① a product defect, class (a), reach measured at a public door. Raised by #22694's dev (report 6101206571, PR #22716) and filed by the domain:cli seat 1 (seat post #6024) · session_019SvPnd2bzECRNmAU9i6E4k. ⛔ Not a claim.

Reader who acts: triage grades it and names the lane. The producer is toFailedResult in packages/core/src/utils/import-runner.ts, the same function #22694 edits.

Measured (by the dev, not re-run by the seat)

A stack booted with @objectstack/verify's bootStack on sqlite-wasm, at main d8830c2805, with a beforeInsert sandboxed hook whose body throws new TypeError('boom'):

  • POST /api/v1/data/mz_crash/import answered 200. results[0].error read hook 'mz_crash_insert' threw: TypeError: boom, with code IMPORT_ROW_FAILED. The async /import/jobs stored row read the same.
  • POST /data/mz_crash and /createMany answered 500 INTERNAL_ERROR, Internal server error, and withhold the fault's text.

PR #22716 leaves this unchanged on purpose: #22694's control was that a crash still answers as a fault, not as a sentence. sandboxBusinessMessage declines a crash, so the row is built from .message through sanitizeRowError, as before.

Expected (a shape for triage, not a ruling)

An import row for a crashed hook body withholds the fault text the way the other doors do. It still reads as a fault (a failed row with a fault code), never as the hook's sentence and never as the native error text. Control: a refusal row keeps the hook's sentence, as PR #22716 makes it.

Noted beside it, not measured at a public door: a sandboxed refusal that DECLARES a 5xx status is withheld by the doors, while the import row relays its prose, before with the wrapper and after PR #22716 without it. The repair should decide both rows together.

Duplicate check

The same enumeration as the sibling filing: REST issues state=all&since=2026-08-15, 5,137 issues from #11669 to #22708, closed included. Local grep:

Dedupe words: import row crash TypeError leak · IMPORT_ROW_FAILED native fault text · toFailedResult crash withhold · import hook crash 500 parity


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p3 · domain:engine · area:api · pm:blocked on #22694. The same function, so it waits for PR #22716

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-10T19:55Z. ⛔ Not a claim, ⛔ not a dispatch.

    Blocked-by: #22694

  2. added
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    bugSomething isn't working
    and removed on Oct 10, 2026
  3. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Unlock scan: #22694 closed, with PR #22716 landed as 91f46d0060. pm:blocked → pm:queue

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-10T20:57Z. ⛔ Not a claim, ⛔ not a dispatch.

    Blocked-by: none

  4. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-10T21:35Z
    Session: session_01JfJfBUC3cQ6hhgm9MQK76T
    Account: os-project-manager (the seat's linked user, as GET /user answers it; the card's assignee)
    Branch: claude/issue-22718-import-crash-row
    Worktree: objectstack-issue-22718
    Domain: domain:engine
    Seat: domain:engine#1 (seat post #6367)
    File surface (read on origin/main a360cee9; stop on a breach and explain it in the report):


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 22718,
    "status": "done",
    "branch": "claude/issue-22718-import-crash-row",
    "pr": "#22740",
    "session": "session_01JfJfBUC3cQ6hhgm9MQK76T (mode:subagent; the dispatching PM session id, CLAUDE_CODE_REMOTE_SESSION_ID cse_01JfJfBUC3cQ6hhgm9MQK76T)",
    "premise_still_valid": true,
    "summary": "toFailedResult (packages/core/src/utils/import-runner.ts) now gives a sandbox-origin error that mapDataError answers with a 5xx the door's own sentence: const sandboxFault = isSandboxOrigin(err) && door.status >= 500 ? String(door.body.error) : undefined; message = sandboxFault ?? sandboxBusinessMessage(err) ?? sanitizeRowError(e.message). door is the mapDataError answer the function already computed for adoptDoorVerdict, and isSandboxOrigin is the existing @objectstack/types export rest's door uses, so no second withholding rule exists. A crash row and a declared-5xx refusal row now read 'Internal server error' on the sync /import row and the /import/jobs stored row, as POST /data/:object and /createMany do. The row code is unchanged: IMPORT_ROW_FAILED for a crash, the declared code for a declared 5xx. A refusal with no status or a 4xx status keeps the hook's sentence, and a driver error keeps its sanitizeRowError text. Premise re-measured at origin/main a360cee through the REST ledger harness (real handlers, real runImport). Crash: the doors answered 500 INTERNAL_ERROR 'Internal server error', and both import rows answered IMPORT_ROW_FAILED "hook 'mz_guard' threw: TypeError: boom". Declared 503 SERVICE_UNAVAILABLE: the doors answered 503 'Internal server error', and both rows relayed the prose. PM assumptions 1-3 held. line_budget: +95/-33 = 128 changed lines, 4 files (under 200). files_changed: packages/core/src/utils/import-runner.ts; packages/core/src/utils/import-runner-sandbox-refusal-row.test.ts (new §5, §4 crash control retired, driver control gains a door-5xx anti-vacuity line); packages/rest/src/rest-write-route-hook-refusal-sentence.ledger.test.ts (§4 rewritten as the door-level pins); .changeset/22718-import-row-sandbox-fault.md (@objectstack/core patch, Clause-②: no). deviations: (1) FILE SURFACE: packages/rest/src/rest-write-route-hook-refusal-sentence.ledger.test.ts is domain:cli and outside the claim's surface. Only the test changed. Its §4 pinned the leaking crash row (row === sanitizeRowError(crash.message)), so the fix turns it red unless it is edited. Its harness already drives both import doors with the real handlers, so the door-level pins live there. Nothing in qa/dogfood or verify is touched. The PR body names this. (2) The core pins were added as §5 of the #22694 row test, reusing its helpers, rather than in a new file. (3) Gates were re-derived from the real change set: 65 commands, the dispatch's 51 plus 14 more, plus 3 roster gates under my paths, all run. (4) The first attempt at ablation B was refused by ablation-replace before anything ran, because the replacement was a substring of the anchor. It was re-run with a distinct replacement, and only the re-run counts. (5) Declared to CI and not run locally: rest's full suite (the public face is unchanged, only the row value moved), core's repo project (4 security pins, none touching the runner), and repo-wide lint. (6) Labels: zero written. The dispatch named none, and the PR carries a changeset, so skip-changeset does not apply. (7) Commits carry the model-free trailer pair AGENTS.md requires, not the harness's model-named Co-Authored-By.",
    "tests": "BASE probe (throwaway, deleted, never committed) at a360cee: the four doors as in summary. Red leg (rest resolves @objectstack/core via dist, no alias): the marker 'door.status >= 500' counted 0 in core dist/index.js and 1 in src. pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2 src/rest-write-route-hook-refusal-sentence.ledger.test.ts exited 1 with 'Tests 2 failed | 45 passed (47)'. Both new §4 cases failed at the sync /import door: expected "hook 'mz_guard' threw: TypeError: boom" to be 'Internal server error', and expected 'The ledger service is down.' to be 'Internal server error'. Green leg: core rebuilt under os-verify-lock (VERDICT command-exit 0); node scripts/ablation-dist-preflight.mjs @objectstack/core 'door.status >= 500' exited 0, marker in dist/index.js and dist/index.cjs, tree clean; then the same rest file ran 47 passed. Ablation A, through scripts/ablation-replace.mjs in WRAP mode with an outer trap, removed 'sandboxFault ??' (anchor 1 to 0, blob 8e511983ac2a to b05265a89971). The core file ran 'Tests 2 failed | 13 passed (15)', exactly the two §5 cases. The restore was proven by blob == HEAD 8e511983ac2a and an empty git diff HEAD. Ablation B dropped the 'isSandboxOrigin(err) &&' gate (blob to f88795fb66fc). The core file ran '1 failed | 14 passed': the driver-text control expected 'SQLITE_BUSY: database is locked' and received 'Internal data error'. The restore was proven the same way. Its first attempt was a refused no-op and does not count. At merge commit 824d16b (origin/main 0f77ff5 merged once; spec, lint and core rebuilt), under os-verify-lock with VERDICT command-exit 0: pnpm --filter @objectstack/core test exit 0, 89 files and 2308 passed; pnpm --filter @objectstack/core typecheck exit 0, test layer OK with 4 pinned debt signatures held; pnpm --filter @objectstack/rest typecheck exit 0, test layer OK with 0 debt; the rest ledger file exit 0, 47 passed. GATES at 824d16b: node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0; node scripts/check-adr-0087-registration.mjs --self-test :: exit 0; node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0; node scripts/check-changeset-no-major.mjs --self-test :: exit 0; node scripts/check-ci-filter-parity.mjs :: exit 0; node scripts/check-closing-keyword-parity.mjs :: exit 0; node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0; node scripts/check-comment-mask-adoption.mjs :: exit 0; node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0; node scripts/check-comment-mask-corpus.mjs :: exit 0; node scripts/check-dev-prereqs.mjs --self-test :: exit 0; node scripts/check-dts-emitted.mjs --self-test :: exit 0; node scripts/check-empty-changeset.mjs --base origin/main :: exit 0; node scripts/check-empty-changeset.mjs --self-test :: exit 0; node scripts/check-issue-citations.mjs :: exit 0; node scripts/check-keyed-text-bounds.mjs :: exit 0; node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0; node scripts/check-platform-object-tenancy-census.mjs :: exit 0; node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0; node scripts/check-plugin-teardown-shape.mjs :: exit 0; node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0; node scripts/check-registry-log-declared.mjs :: exit 0; node scripts/check-registry-log-declared.mjs --self-test :: exit 0; node scripts/check-rest-log-spy-declared.mjs :: exit 0; node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0; node scripts/check-system-context-census.mjs :: exit 0; node scripts/check-system-context-census.mjs --self-test :: exit 0; node scripts/check-undeclared-dep-imports.mjs :: exit 0; node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0; node scripts/docs-audit/check-affected-docs.mjs :: exit 0; node scripts/docs-audit/check-drift-comment.mjs :: exit 0; node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0; node scripts/release-pending-publish.mjs --self-test :: exit 0; pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0; pnpm check:changeset-gate-self-tests :: exit 0; pnpm check:cross-package-test-inputs :: exit 0; pnpm check:dispatcher-error-vocabulary :: exit 0; pnpm check:doc-authoring :: exit 0; pnpm check:driver-memory-census :: exit 0; pnpm check:dts-closure :: exit 0; pnpm check:dual-build-cjs-loads :: exit 3; pnpm check:engine-double-contract :: exit 0; pnpm check:error-code-casing :: exit 0; pnpm check:gitlink-declared :: exit 0; pnpm check:issue-citations :: exit 0; pnpm check:kernel-hook-pairs :: exit 0; pnpm check:lean-entry-closure :: exit 0; pnpm check:logger-receiver-detach :: exit 0; pnpm check:nul-bytes :: exit 0; pnpm check:objectql-double-limit :: exit 0; pnpm check:objectui-changeset :: exit 0; pnpm check:org-identifier :: exit 0; pnpm check:page-declaration-shape :: exit 0; pnpm check:pm-changeset-deadline-census :: exit 0; pnpm check:published-files :: exit 0; pnpm check:query-options-erasure :: exit 0; pnpm check:refd-timer-probe :: exit 0; pnpm check:slot-lookup :: exit 0; pnpm check:sourcemap-no-sources-content :: exit 0; pnpm check:test-source-alias :: exit 0; pnpm check:tier-file-adoption :: exit 0; pnpm check:type-check-coverage :: exit 0; pnpm check:type-check-debt :: exit 0; pnpm check:watch-hint-literal :: exit 0; pnpm check:where-matcher :: exit 0; node scripts/check-changeset-fixed.mjs :: exit 0; pnpm check:authz-resolver :: exit 0; pnpm check:filter-alias-parity :: exit 0. check:dual-build-cjs-loads exit 3 is PREREQUISITE NOT MET: NOT MEASURED, reason: it needs every package's dist and the whole-workspace build is CI's. Narrowed reading: core's rebuilt dist/index.cjs require()s fine (runImport and sanitizeRowError are functions). node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran with exit codes recorded: '65 derived, 64 run, 1 NOT-MEASURED, 0 UNRUN'. CI: in_progress, not awaited.",
    "mcp_calls": "0",
    "api_writes": "2 — two fleet-write relay strokes, each POST /repos/objectstack-ai/objectstack/dispatches. (1) pr_create, which the relay executed as POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/22740/assignees; read-back 8968 bytes sent and 8968 stored, assignee os-project-manager. (2) This report, which the relay executed as POST /repos//issues/22718/comments. git push is not counted. Reads used gh api (single-card REST).",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: a · reach: named producer: runtime/src/sandbox/quickjs-runner.ts throws a SandboxError with NO innerMessage for the sandbox's own faults (the CPU-budget error and the sandboxFault capability-denial branch). Evidence, via the built functions at 824d16b and not at a public door: mapDataError answers 500 INTERNAL_ERROR 'Internal server error', while toFailedResult builds the row from sanitizeRowError(.message), "hook 'mz_guard' exceeded its CPU budget (50ms)". This is the same family as #22718 (import row versus data-door fault-text parity), so fold it into a family closure card, not a point card. Bounded in-place fix declined: closing it needs a predicate the door does not have (the door reaches 500 through absence of evidence). Dedupe words: import row sandbox fault no innerMessage · CPU budget import row text · capability denial import row relay · SandboxError timeout import row",
    "carrier: none · noted, not filed. A non-sandbox native error (for example a code-defined hook's TypeError) gets 500 'Internal server error' at the doors, while the row relays 'x.trim is not a function'. Rows deliberately relay unclassified non-sandbox text through sanitizeRowError, so this is a design question with no named producer. In the PR's Acceptance notes.",
    "carrier: none · noted, not filed. A crash whose body attached a code to the native error keeps that code on the row, while the doors answer INTERNAL_ERROR (the commit cf6e0a1 ruling at the doors). The row's code rule is unchanged by this card. In the PR's Acceptance notes."
    ]
    }

  6. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT (seat review): PR #22740 at head 824d16b012

    domain:engine seat 1 (#6367) · session_01JfJfBUC3cQ6hhgm9MQK76T · 2026-10-10T22:47Z. Claim 6102428634. Dev report 6102919928. Read against GitHub and origin/main, not against the report.

    Shape.

    • Draft, base main, assigned os-project-manager.
    • Line 1 is Fixes #22718, and line 3 is Clause-②: no. A closing-keyword scan of the whole body finds that line only.
    • Four files. NOT governed. +95 / −33, under the suggested budget.

    The change.

    • toFailedResult (packages/core/src/utils/import-runner.ts) computes mapDataError(err, objectName) once.
    • For a sandbox-origin error (isSandboxOrigin, the existing @objectstack/types export that the REST door uses) that the door answers with a 5xx, the row's text is the door's own error string.
    • Which errors are faults, and the sentence they get, both stay mapDataError's verdict. There is no second withholding rule.
    • The row's code is untouched.
    • mapDataError was read on origin/main: it has no side effect, so calling it on every failed row is safe.
    • Clause-②: no holds. No accept set or public surface moves; only a row value changes, toward the doors' answer.

    Evidence read.

    • The base was measured on the real REST handlers and the real runImport. A crash row relayed the wrapper, and a declared-5xx row relayed the prose, where both doors answered Internal server error.
    • Red, then green, at the door level. The rest ledger file ran 2 failed against core's base dist, with the marker counted 0 in dist and 1 in src. After the rebuild it ran 47 passed, and the preflight marker was present in both dist builds.
    • Ablation A removed the new branch: 2 red, exactly the two §5 cases.
    • Ablation B dropped the isSandboxOrigin gate: 1 red, the driver-text control. That witnesses the gate.
    • Both restores were proven blob-equal to HEAD. B's refused first attempt was a no-op, declared and not counted.
    • At 824d16b012 (the current head): core's tests passed (89 files, 2,308 tests); core's and rest's typechecks exit 0.
    • Gates: 68 run and 67 exited 0. check:dual-build-cjs-loads is NOT MEASURED locally (PREREQUISITE NOT MET: it needs every package's dist). It is CI's, and the dev's narrowed reading loaded core's rebuilt dist/index.cjs.
    • CI on this head was still running at this read. Landing waits for it.

    Changeset checked (@objectstack/core patch). Each sentence matches the diff:

    • the crash wrapper and the declared-5xx prose no longer reach the row;
    • the row reads Internal server error;
    • the code is unchanged;
    • a no-status or 4xx refusal keeps the hook's own words;
    • a database error's row text is unchanged, as ablation B pins.

    Files outside the claim. packages/rest/src/rest-write-route-hook-refusal-sentence.ledger.test.ts is domain:cli and test-only.

    • Its §4 pinned the very row text this card changes, so it would have gone red.
    • Its harness already drives both import doors with the real handlers. So it is the door-level pin the claim asked for, and no dogfood or verify file was needed.
    • Declared to [PM seat] domain:cli — ⏳ vacant #6024 in this act.

    Out-of-scope findings.

    Landing. Ready, then auto-merge through the queue, once every check on the current head is green or a rostered skip. On merge this card closes, and #22739 (Blocked-by: #22718) unblocks.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #22740 → d7b26df5b5 · domain:engine seat 1 (#6367) · session_01JfJfBUC3cQ6hhgm9MQK76T · 2026-10-11T02:07Z


    Generated by Claude Code

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

Metadata

Metadata

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:enginepriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions