Skip to content

fix(service-storage): give failed/expired upload-session statuses a producer (#7667) - #7844

Merged
huangyiirene merged 2 commits into
mainfrom
claude/issue-7667-upload-session-status-liveness
Aug 12, 2026
Merged

fix(service-storage): give failed/expired upload-session statuses a producer (#7667)#7844
huangyiirene merged 2 commits into
mainfrom
claude/issue-7667-upload-session-status-liveness

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes #7667

Premise: verified on origin/main (098f4bb)

All three anchors held. Enum members at system-upload-session.object.ts:104-105, retention onlyWhen at :134, type union at metadata-store.ts:54, and a repo grep confirms no writer for either status anywhere in service-storage.

Branch taken: ENFORCE, not remove

The deciding evidence is one file the issue does not mention: packages/spec/src/api/storage.zod.ts:247 declares UploadProgressSchema.status as z.enum(['in_progress','completing','completed','failed','expired']). Both statuses are published to every client that reads the contract — so they are not merely declared-and-unread; the API promises them. Removing them from the object enum would have forked the object from the spec's progress contract and forced a packages/spec narrowing (the dispatch's STOP condition). Enforcing gives that contract teeth instead.

Both statuses also name real failure states that were previously invisible:

  • failed — a completion whose backend completeChunkedUpload threw left the row at completing. That is a non-terminal status, so the 7d retention backstop ($in ['completed','failed','expired']) never reaped it, and a progress poll reported "still assembling" for a session that had already given up. The completion route now stamps failed on that path. It records an attempt rather than locking the session — nothing reads failed as a refusal, so a retry of the same uploadId runs the happy path and overwrites it with completed (pinned by a test).
  • expired — a session past its own expires_at kept answering in_progress and kept accepting chunks, until the TTL sweep deleted the row out from under the caller. The deadline the init response already announced (expiresAt) bound nothing. A chunk PUT or a complete against an overdue session is now refused 410 UPLOAD_SESSION_EXPIRED and the row is durably stamped expired. GET .../progress reports the status rather than refusing — expired is a declared member of UploadProgressSchema.status, and the SDK's resumeUpload polls progress first.

The retention onlyWhen is now consistent with the enum in the strong direction: every member it names has a writer in storage-routes.ts. The reap guard (createUploadSessionReapGuard) already handled both statuses — it aborts the backend multipart for any non-completed row carrying a backend_upload_id — so this closes that loop rather than opening a new one.

Deliberate non-behaviours, each pinned by a test: a row with no expires_at carries no declared deadline and is left alone (the guard enforces the row's own deadline, it does not invent one); a completed row does not become expired by waiting for the reaper; expiry is checked after the resume-token check, so a caller who cannot prove it owns the session learns nothing about its state.

⚠️ Out-of-surface addition, declared loudly

packages/spec/src/api/error-code-ledger.zod.tsone additive line, registering UPLOAD_SESSION_EXPIRED under @objectstack/service-storage. ErrorCode is the closed union StandardErrorCode ∪ ERROR_CODE_LEDGER, and an unregistered code fails schema parse → fails the envelope conformance suites → fails CI (ADR-0112). The alternative was reusing a semantically wrong existing code (UPLOAD_SESSION_NOT_FOUND on a session that plainly exists), which is precisely the bypass the ledger's own header warns about. Purely additive: the union grows, nothing that validated before stops validating.

The #5536 ride-along clause did not trigger — this diff does not touch storage-service-plugin.ts.

Checklist maintenance

docs/qa/platform-checklist/areas/attachments-storage.jsonattachments-storage.upload-session-abort revision 3, closing the clause-5 finding revision 2 explicitly deferred to this issue: two steps that drive failed and expired, a new acceptance clause for the 410 refusal, and the producers named in source. Transient completing is recorded as a knownGap rather than an unreachable-variant FAIL — the complete route writes it and overwrites it in the same request, so a scan of settled rows will never show it, and that is correct.

Gates (all run locally, all green)

Gate Result
pnpm build (full closure) ✅ 71/71 tasks
service-storage suite ✅ 24 files, 361 tests
specerror-code-ledger + storage ✅ 62 tests
pnpm check:docs-audit-scope
pnpm check:platform-checklist ✅ 15 areas, 182 items
pnpm check:route-envelope ✅ (new 410 branch on the shared sendError)
pnpm typecheck (spec)

service-storage declares no typecheck script (not type-check-covered); an ad-hoc tsc --noEmit over it reports only the pre-existing extension-less test-import noise, none from this diff.

No new test fake with update/delete verbs was added — the tests drive the real StorageMetadataStore on its no-engine Map path, so the assertEngineUpdateDispatch/assertEngineDeleteDispatch gate does not apply.

Follow-up (not taken here — out of this card's file surface)

packages/client's storage.resumeUpload reads the progress body but ignores status, so resuming an expired session now surfaces as a 410 on the first chunk PUT rather than a clean early exit. That is an honest failure, not a regression, but a short-circuit on status === 'expired' would be a better client experience. Worth a separate domain:client card.


Generated by Claude Code

… a producer (#7667)

`sys_upload_session.status` declared `failed` and `expired`, the retention
backstop reaped on both, and `UploadProgressSchema` published both to every
client reading the contract — while nothing in the service ever wrote either.
A scan of every session row could only return `in_progress`/`completed`, so
the retention rule named two states the system could not enter.

ADR-0049 enforce-or-remove, taking the ENFORCE branch: removal would have
forked the object from the spec's progress contract, and both failure states
are real and were previously invisible.

- `failed`: a completion whose backend `completeChunkedUpload` threw left the
  row at `completing` — non-terminal, so the 7d retention backstop never
  reaped it and a progress poll read "still assembling" indefinitely. The
  completion route now stamps `failed` on that path. It records an attempt
  rather than locking the session: a retry runs the happy path and overwrites
  it with `completed`.
- `expired`: a session past its own `expires_at` kept answering `in_progress`
  and kept accepting chunks until the TTL sweep deleted the row out from under
  the caller, so the deadline the init response announced bound nothing. A
  chunk PUT or a complete against an overdue session is now refused 410
  `UPLOAD_SESSION_EXPIRED` (registered under `@objectstack/service-storage` in
  `ERROR_CODE_LEDGER`) and the row is durably stamped `expired`. Progress
  REPORTS the status rather than refusing — `expired` is a declared member of
  `UploadProgressSchema.status` and the SDK's `resumeUpload` polls it first.

A row with no `expires_at` carries no declared deadline and is left alone; a
`completed` row does not become `expired` by waiting for the reaper. The
`failed` stamp is best-effort and loud on failure, so a metadata-store error
never replaces the real backend cause on its way to the 500.

Checklist item `attachments-storage.upload-session-abort` revision 3 records
the producers, adds steps that drive both statuses, and records transient
`completing` as a knownGap rather than an unreachable-variant FAIL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198Jr94CUGy2vDGtT1L8pka
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 11, 2026 10:43pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-storage, @objectstack/spec.

107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/api/plugin-endpoints.mdx (via @objectstack/service-storage)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/service-storage, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/service-storage, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/service-storage, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

PATCH ROUND (services seat, session session_01JVfWSQN9RDDGwdVSmMc29x) — same claim, same branch, one gate to fix. Durable copy of the instruction; the dev session poke follows when the messaging channel recovers.

The red: CI's "TypeScript Type Check" job fails on check:docs (tsx scripts/build-docs.ts --check) — the one-line error-code-ledger.zod.ts addition regenerates the API reference pages, and the generated content/docs/references/api/*.mdx set (11 files, including error-code-ledger.mdx and storage.mdx) was not regenerated and committed. 产物随源走.

The fix, in order (for the original dev on claude/issue-7667-upload-session-status-liveness):

  1. Ensure the worktree has NO uncommitted merge state — commit anything pending first (⛔ never run gen:schema mid-MERGE: it silently regresses the authorable-surface anchor, os-regen 驱动指示的 gen:schema 在 merge 未 commit 时运行,会把 authorable-surface 锚点倒退回旧 merge-base —— 生成器写入、门全绿、静默撤销 main 的锚点推进 #5370).
  2. pnpm --filter @objectstack/spec gen:schema && pnpm --filter @objectstack/spec gen:docs
  3. Commit the regenerated content/docs/references files and nothing else. If gen:schema touched authorable-surface.base.json, verify with pnpm --filter @objectstack/spec check:authorable-surface (⛔ never hand-edit the anchor). If gen:schema's rmSync cleared gen:openapi's output (rest tests would 503), also run pnpm --filter @objectstack/spec gen:openapi (gen:schema rmSync 整个 json-schema/ 会顺手抹掉 gen:openapi 的产物,rest 的 openapi 路由测试随后 503 假红——check:generated 原地跑 build-schemas 也触发 #5371).
  4. Re-run pnpm --filter @objectstack/spec check:docs locally — must be green.
  5. Push to the same branch; post an addendum on upload-session-abort c4: failed/expired sys_upload_session.status have no producer (enforce-or-remove) #7667 (marker line first) with the patch sha and the check:docs rerun result.

⛔ Nothing else on the PR changes; no CI idle-polling — the PM owns the ready-flip and landing.


Generated by Claude Code

…edger entry (#7667)

产物随源走: registering `UPLOAD_SESSION_EXPIRED` in `ERROR_CODE_LEDGER` widens
the `ErrorCode` union every enveloped response references, so all 11
`content/docs/references/api/*.mdx` pages that render it were stale and
`check:docs` (`build-docs.ts --check`) failed the TypeScript Type Check job.

The whole diff is that one addition propagating: a new `UPLOAD_SESSION_EXPIRED`
bullet in `error-code-ledger.mdx`, and the union arity in every rendered `error`
column moving `+260 more` → `+261 more`. No unrelated drift was absorbed.

Generated, not hand-written: `pnpm --filter @objectstack/spec gen:schema && gen:docs`
on a clean tree with no merge in progress (#5370). `authorable-surface.base.json`
was not touched, so no re-anchoring rode along; `json-schema/openapi.json` was not
cleared by the rmSync and was refreshed anyway (#5371, gitignored either way).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198Jr94CUGy2vDGtT1L8pka
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

upload-session-abort c4: failed/expired sys_upload_session.status have no producer (enforce-or-remove)

1 participant