Skip to content

service-storage: restore prefix enumeration cursor-shaped — list(prefix, { cursor, limit }) + adapter-conformance cases (cloud is the first-party caller the retirement could not see, twice) #6781

Closed

Description

@os-zhuang

Part of objectstack-ai/cloud#1203 — maintainer ruling 2026-08-08: option B. Filed by the repo:cloud seat per the cross-seat transfer protocol; pm:queue only, routing label left to the triage seat (note the two-surface shape below — this card may want the contract-first split).

Why

#5540 removed list?(prefix) from IStorageService and #5541 removed the adapter implementations, both on the measurement "Nothing in the repo called either". True for this repo; false one repo over: cloud has two production callers the measurement could not see —

  1. packages/service-cloud/src/environment-storage-cleanup.ts:92 — tenant attachment reclamation on environment delete (TS2339 compile-red at any post-retirement pin; cloud#935 is the incident where this sweep silently did nothing, orphaning deleted tenants' uploads forever);
  2. packages/service-cloud/src/storage-service-blob-store.ts:78 — marketplace snapshot GC (structural type, so it breaks at runtime instead: TypeError swallowed into a warning; delisted package detail blobs stay fetchable at the Worker edge).

This blocks cloud#1197 (the .objectstack-sha bump that lands the #5852 cross-org escalation producer fix in deployment): the retirement and the #5852 fix landed the same morning, so no commit on main has one without the other. Full option analysis (A: cloud hand-rolls S3 pagination — recreates the retired defect one repo over; C: tracked-keys sweeps — right end-state for the env sweep but its backfill itself needs enumeration once) is on cloud#1203; the ruling chose B: restore it upstream, correctly shaped.

The shape — already prescribed, not invented here

Both the retirement note and #5266's option 2 reserved this route word for word: "if a first-party caller ever needs real bucket enumeration it returns cursor-shaped — list(prefix, { cursor, limit }) — with adapter-conformance cases proving both backends agree before it ships." Cloud is that first-party caller, twice.

  1. Contract (packages/spec/src/contracts/storage-service.ts — ⚠️ spec surface, one-owner rule applies): list(prefix: string, opts?: { cursor?: string; limit?: number }) returning { items: StorageFileInfo[]; nextCursor?: string }. Cursor-shaped so callers must handle pagination explicitly — the silent-1000-cap and the one-level-vs-recursive dialect split (service-storage: IStorageService.list(prefix) means two different things on the two shipped adapters (local: one level, directories as files; S3: recursive, silently capped at 1000) #5266's two measured defects in the OLD signature) become unrepresentable.
  2. Both adapters, consistent semantics: recursive prefix match, paginated. Local: readdir recursive, directory entries skipped (never stat'd into results); S3: ListObjectsV2 + ContinuationToken loop honoring limit.
  3. Adapter-conformance cases proving Local and S3 agree — at minimum the three service-storage: IStorageService.list(prefix) means two different things on the two shipped adapters (local: one level, directories as files; S3: recursive, silently capped at 1000) #5266 named: nested keys (a/b/c visible under list('a')), directory entries excluded, >1000 objects fully enumerated via cursor.
  4. SwappableStorageService pass-through restored.
  5. storage-adapter-list-retirement.test.ts flips to pin the new contract (both adapters implement it, signatures match the spec) — flipped pins keep bearing load; do not just delete the retirement pins.
  6. Changeset minor (new public API surface).

Consumer side (not this card)

Cloud picks the capability up by moving the pin — cloud#1197 / PR #1205 (parked draft) re-targets to a SHA covering this, and cloud-side behaviour-level tests for both callers are tracked under cloud#1203 (measured there: cloud's deploy pipeline cannot see this class of break — OS_SKIP_DTS=1 skips the only typechecking pass — so type-level red alone is not a regression guard). A follow-on cloud card (option C: tracked-keys reclamation for the environment sweep, using this list once for backfill) is filed after cloud#1197 lands.

Refs: cloud#1203 (ruling + analysis), cloud#935, #5266 (the dialect measurement; tracking), #5540, #5541, cloud#1197 / cloud PR #1205.

Activity

  1. added
    domain:specDomain lane — seat card indexed by label:pm:seat
    on Aug 8, 2026
  2. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    ContributorAuthor

    Triage (routing + designation — pm:queue was set by the filing seat per the transfer protocol): + domain:spec, designated to the spec seat as a single-claimant cross-domain card (rule 4 exception path, precedent #6735 / #6428).

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    Collaborator

    认领 / Claim — dev seat

    • session: session_01F8q5J1MQyocgtNspb15fSn
    • branch: claude/issue-6781-storage-cursor-enumeration
    • base: origin/main

    按分诊评论的单一认领人指派(spec seat,跨域整卡)。声明的文件面(cross-domain,含 domain:cli 的 adapters):

    • packages/spec/src/contracts/storage-service.ts(契约,one-owner)
    • 两个 storage adapter 实现(local / S3)
    • SwappableStorageService 透传
    • storage-adapter-list-retirement.test.ts(翻转为新契约 pin)
    • adapter-conformance 用例 + changeset(minor)

    在途冲突检查:本卡与 cloud#1197 / cloud PR #1205(consumer 侧,明确不在本卡范围)无文件重叠。若有其他会话已在这些文件上在途,请回复本评论。


    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

domain:specDomain lane — seat card indexed by label:pm:seatpm:queueReady for the PM dispatch loop

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions