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_progressand 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 .../progressreports 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 noexpires_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-abortrevision 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)

GateResult
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

vercelBot commented Aug 11, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 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-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 11, 2026
@huangyiireneClaude

Copy link
Copy Markdown
CollaboratorAuthor

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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

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

@huangyiirene