Skip to content

metadata-protocol: the migrate-stored pass and its REST, runtime and CLI doors pass a label as PutOptions.actor; the contract says null, and the label lands in recorded_by (and updated_by after PR #23067) #23068

Closed

Description

@objectstack-fleet

Filing gate ①, class (b): a declared contract that the named producers break. reach: four named producers:

  • ObjectStackProtocolImplementation.migrateStoredMetadata (packages/metadata-protocol/src/protocol.ts) defaults to actor: request.actor ?? 'migrate-stored';
  • the REST handler POST …/meta/_migrate-stored (packages/rest/src/rest-server.ts) passes 'USER (POST …/_migrate-stored)' or 'POST …/_migrate-stored';
  • packages/runtime/src/domains/meta.ts uses the same shape;
  • os migrate meta --stored (packages/cli/src/commands/migrate/meta.ts) passes 'os migrate meta --stored'.

Filed by domain:engine seat 2 (#20966) · os-steve · session_01ADzJtzYTLUfgrRZHxkagkX · 2026-10-12T15:39Z. It comes from #23033's dev (os-dev-report 6127496314, open question 1 and out-of-scope finding 1). The seat answered A (a card of its own), so it does not ride PR #23067. ⛔ Not a claim. Lane and grade are triage's. Its files sit in domain:engine (protocol.ts), domain:cli (rest, runtime, cli).

The contract it breaks

What lands today, and after PR #23067

Wanted

  • Each producer passes the acting user's id when one exists, and null otherwise.
  • The door's name moves to where it belongs: the source field (source: 'migrate-stored' is already stated) or a log line, not the actor.
  • Pins:
    • a migrate pass run by a user records that user's id;
    • one with no user records null in recorded_by and updated_by;
    • no label string reaches either column.
  • ⛔ Not a repository-side filter of non-id values: the producer is off-contract, so the repository is not the place to fix it.

Sequencing

protocol.ts and rest-server.ts are held at this stamp by #22877 stage 2's by-name doors (other functions), and runtime by #23027 (PR #23045, in the merge queue). Neither holds migrateStoredMetadata or its handlers, so these are textual neighbours. The later lander merges main.

Dedupe: a search of the issues for "migrate-stored actor", "recorded_by label", "PutOptions.actor label" found #4556 (closed; it retired the 'system' sentinel at the repository) and #7749 (closed; the bearer /meta PUT). Neither covers these producers.

Activity

  1. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p3 · domain:engine · area:studio, into pm:queue. Every producer passes the user's id or null. One PR under the cross-domain exception

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

  2. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-12T17:18Z
    Session: session_01ADzJtzYTLUfgrRZHxkagkX
    Account: os-steve (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-23068-migrate-actor
    Worktree: objectstack-issue-23068
    Domain: domain:engine (claiming; one PR under the cross-domain exception path the triage named)
    Seat: domain:engine#2 (seat post #20966); the lane's only seated PM
    Triage-ref: 6128747332 (every producer passes the acting user's id or null; the door's name goes to source or a log line; ⛔ no repository-side filter)
    File surface (read on origin/main 275ad4229a; stop on a breach and explain it in the report). ⛔ Classes, positions and functions only, in every artefact.

  3. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 23068,
    "status": "done",
    "branch": "claude/issue-23068-migrate-actor",
    "pr": "#23085",
    "session": "session_01ADzJtzYTLUfgrRZHxkagkX (subagent: the dispatching PM session id)",
    "premise_still_valid": true,
    "summary": "The four producers now pass the acting user's id or nothing: migrateStoredMetadata forwards request.actor only when one is given, so saveMetaItem's #4556 default records null (it used to default to 'migrate-stored'); the REST POST /meta/_migrate-stored handler takes its actor from resolveMetaWriteActor, the producer every /meta write door shares; the runtime dispatcher passes executionContext.userId or nothing; and os migrate meta --stored passes no actor, because it has no user. MetadataItemSchema.authoredBy's describe now gives PutOptions.actor's rule (a user id, or null for a system-initiated write, never a label such as 'system' or 'cli'). No reader parses authoredBy for those labels. The pass still names itself on source 'migrate-stored', so no log line was added and there is no repository-side filter. Reproduced first on origin/main 275ad42: a probe over the built protocol read recorded_by and updated_by equal to 'migrate-stored', 'admin (POST /api/v1/meta/_migrate-stored)' and 'os migrate meta --stored', and the new pins were red on the unfixed source at every door.",
    "tests": "Red before the fix (pins committed at c126984, unfixed source): protocol 'expected [ migrate-stored, migrate-stored ] to deeply equal [ null, null ]'; REST and runtime 'expected admin (POST /meta/_migrate-stored) to be admin' (runtime: /metadata), with isSystem still sending an actor; CLI: --apply and the preview both sent an actor. Green at df2f5e3 (after merging origin/main 038d4c9 and rebuilding the closure turbo build --filter=@objectstack/cli..., 59/59): metadata-protocol pnpm test 228 files passed, 3 skipped, 28205 tests passed, exit 0; metadata-core 18 files / 685 tests, exit 0; rest (--project local) 274 files / 5752 passed, exit 0; runtime (--project local) 359 files / 5154 passed, exit 0; cli --project unit 284 files / 4272 tests, exit 0; cli --project integration on the one integration file edited, 3/3, exit 0 (the rest of the integration tier is declared to CI); typecheck for metadata-core, metadata-protocol, rest and cli exit 0. runtime typecheck went red at df2f5e3: the new pin grew the shrink-only test-typecheck ledger by one TS18048 (res.response possibly undefined). Fixed at 167dca9 (optional chaining, one line); after that, runtime typecheck exits 0 and the pin file passes 14/14. Ablation (fix committed first; scripts/ablation-replace.mjs in wrap mode inside a trap-guarded script; every pin subject resolves from source, so no rebuild): leg A put the protocol default back to request.actor ?? 'migrate-stored' (anchor 1 to 0, blob 799733c6b23f to 9290dc96e3ad), and the protocol #23068 block went 1 failed / 1 passed while REST 16/16, runtime 14/14 and CLI 2/2 stayed green. Leg B put the REST route-name label back (anchor 1 to 0, blob 712ca3eb781b to 79e1b90a562a), and REST went 2 failed / 14 passed while the protocol pins (2/2), runtime and CLI stayed green. Restore: both blobs equal to HEAD, git diff HEAD empty, git status --porcelain empty. Narrowed lint: eslint --no-inline-config --format json over the 10 changed .ts files gives 10 files, 0 errors and 0 warnings (an ignored file would have shown an ignore warning). eslint.config.mjs never enables type-aware linting, so verdicts on untouched files cannot move. Repository-wide pnpm lint is left to CI.",
    "gates": "At the final head 167dca9: the lead list of 59 plus the 11 that re-derivation on the real diff added (the changeset family: check-adr-0087-registration x2, check-changeset-no-major x2, check-empty-changeset x2, release-rehearsal-clone --self-test, release-pending-publish --self-test, check:changeset-gate-self-tests, check:objectui-changeset, check:pm-changeset-deadline-census), 70 in all. Each exit was captured before any pipe, and all 70 exited 0. dispatch-gates --ran, given the exit codes: 70 derived, 70 run, 0 NOT-MEASURED (a derived zero), 0 UNRUN. check:dual-build-cjs-loads and check:i18n-coverage measured real built output in this run (i18n-coverage: 13 configs, 621 baselined, none new). An earlier run at df2f5e3 had both at exit 3 (PREREQUISITE NOT MET); that run is superseded by this one. CI convergence is not awaited: in_progress.",
    "line_budget": "11 files, +232 / -23 (255 changed lines, against the 3000 human-merge threshold). No skills/** and no governed surface, so no SKILL.md line budget and no maintainer quick-read section.",
    "files_changed": [
    "packages/metadata-protocol/src/protocol.ts (+17/-3: migrateStoredMetadata forwards actor only when given; actor field doc; header source bullet)",
    "packages/rest/src/rest-server.ts (+12/-7: _migrate-stored handler uses resolveMetaWriteActor; that helper's header lists the route)",
    "packages/runtime/src/domains/meta.ts (+8/-4: _migrate-stored passes userId or nothing)",
    "packages/cli/src/commands/migrate/meta.ts (+6/-1: runStored passes no actor)",
    "packages/metadata-core/src/types.ts (+1/-1: MetadataItemSchema.authoredBy describe)",
    "packages/metadata-protocol/src/protocol.stored-migration.test.ts (+35: #23068 block on the existing stub engine)",
    "packages/rest/src/rest-meta-migrate-stored.test.ts (+13/-2: toBe(admin); isSystem sends no actor)",
    "packages/runtime/src/domains/meta-migrate-stored.test.ts (+15/-2: same two pins)",
    "packages/cli/src/commands/migrate/meta.stored-actor.test.ts (new, 105, unit tier: real MigrateMeta, boot mocked, no actor on --apply or the preview)",
    "packages/cli/src/commands/migrate/meta.stored-flow-resolution.integration.test.ts (+4/-3: mirrors the command's call without actor; the probe actor is user-id shaped)",
    ".changeset/23068-migrate-actor.md (new: patch for metadata-protocol, metadata-core, rest, runtime and cli; Clause-②: no)"
    ],
    "mcp_calls": "0",
    "api_writes": "2 relay strokes, both as objectstack-fleet[bot]: (1) pr_create through scripts/pm/fleet-write/dispatch.mjs, which sent POST /repos/objectstack-ai/objectstack/dispatches, and the relay ran POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/23085/assignees (os-steve). The read-back showed 11149 bytes sent and stored, identical, assignee os-steve, draft true. (2) This os-dev-report comment through scripts/pm/post-stamped.mjs, to POST /repos//issues/23068/comments. git push is not REST and was not relayed. No label writes, no PATCH of the PR body, and no card writes.",
    "deviations": [
    "rest-server.ts: besides the claimed handler, one sentence in resolveMetaWriteActor's header now lists POST /_migrate-stored among the doors it serves. Same file, prose only; without it the header would have been false once the handler started using the helper.",
    "No log line was added for the door's name. The triage allowed source or a log line. source 'migrate-stored' was already on every history row, the two HTTP doors are one operation, and recorded_by now tells a user-run pass from a userless one.",
    "Conflict named: the harness asked for a 'Co-Authored-By: Claude' trailer that carries a model name, and a PR footer in its own wording. AGENTS.md (which wins) requires the model-free trailer pair and the session-URL footer block, so I followed AGENTS.md on all three commits and on the PR body. The merge commit df2f5e3 carries git's default message.",
    "One extra commit (167dca9) after the first full run, to keep the runtime test-typecheck ledger from growing. The gates were re-run at that head, and the runtime typecheck and pin file were re-run too. The other suites ran at df2f5e3; the commit changes one line of one runtime test file.",
    "Cleanup is done: the worktree node_modules were removed and git worktree remove succeeded without --force. No background process I started is still running."
    ],
    "open_questions": [
    {
    "question": "Rows that past migrate-stored passes rewrote keep their label in sys_metadata_history.recorded_by and sys_metadata.updated_by. The recorded-by-sentinel journal plan rewrites only 'system', and its header claim that the column 'has only ever held one sentinel' does not cover these labels. Should a cleanup follow? (Not measured: how many deployments hold such rows, and whether the dangling-reference audit, which #4743/#5719 re-scoped, reports them.)",
    "options": [
    "A: a follow-up card for a journal plan beside recorded-by-sentinel. It would set label values to NULL, or take the user id out of the REST form 'ID (POST ...)'. Work: S-M. It needs a closed list of the exact labels the four producers wrote.",
    "B: no cleanup. The rows stay dangling, and the write path is correct from this release on. Work: none. The cost is permanent dangling-reference noise on deployments that ran the pass with --apply."
    ],
    "recommendation": "A, but only once the dangling-reference audit (or a direct query) shows such rows on a real deployment. On the four axes: real need is unmeasured today; long-term, A matches the #4556 precedent of fixing the write path and the stored rows together; for preventing AI mistakes, a reference column that only ever holds an id or NULL is the strict contract; and for startup focus, nothing should be filed before that evidence exists."
    }
    ],
    "out_of_scope_findings": [
    "class: b · reach: named producers: reconcilePermissionSetProjection (packages/plugins/plugin-security/src/permission-set-projection.ts, called from security-plugin.ts reconcile) and backfillRowOnlyPositions (packages/plugins/plugin-security/src/position-environment-backfill.ts) · evidence: both call saveMetaItem with actor 'system', the sentinel #4556 retired. It lands in recorded_by, created_by and updated_by (lookup sys_user). Contract: PutOptions.actor says to pass null, never a label like 'system'. Seam: spec:PutOptions.actor (metadata-core types.ts) to runtime:saveMetaItem to SysMetadataRepository.put · Same family as this card (a producer passes a label as PutOptions.actor): name it under this family's closing card or as a sub-issue of #23068, not as a single-point card · dedupe words: "actor system saveMetaItem", "permission-set backfill recorded_by", "position backfill actor", "PutOptions.actor sentinel"",
    "class: b · reach: named producer: MetadataPlugin (packages/metadata/src/plugin.ts, the FileSystemRepository attach) passes no fsActor, so FileSystemRepository defaults it to 'fs' (packages/metadata-fs/src/repository.ts: the constructor default and every authoredBy/event actor fallback) · evidence: MetadataEventSchema.actor's describe says 'Never a sentinel string', and MetadataItemSchema.authoredBy's describe now says the same. Seam: spec:MetadataEventSchema.actor / MetadataItemSchema.authoredBy (metadata-core types.ts) to runtime:FileSystemRepository; consumer: none measured · dedupe words: "fsActor fs actor", "FileSystemRepository authoredBy", "MetadataEvent actor sentinel"",
    "class: b · reach: named producer: the runtime HttpDispatcher's PUT /metadata/:type/:name branch (packages/runtime/src/domains/meta.ts, protocol.saveMetaItem with writeFace 'meta-dispatch') passes no actor, although executionContext.userId is in hand, so a signed-in user's save records recorded_by/updated_by null. The REST twin passes the user id through resolveMetaWriteActor (#7749/#7941) · evidence: the call site carries no actor key, and metaSaveRequestOptions (packages/rest/src/meta-save-request.ts) adds none. Contract: PutOptions.actor is the identity of the writer, and null is for a write with no human actor. Seam: spec:PutOptions.actor to runtime:HttpDispatcher meta PUT · not measured through a live door · dedupe words: "runtime PUT metadata actor", "meta-dispatch recorded_by null", "HttpDispatcher saveMetaItem actor"",
    "carrier: the seat (open question 1) · past rows holding migrate-stored labels; noted, not filed",
    "carrier: none · packages/metadata-core/test/types.test.ts uses 'cli' as an authoredBy/actor fixture, which the schema still accepts; noted in the PR's Acceptance notes only"
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR #23085 at 167dca9b79 · domain:engine seat 2 · session_01ADzJtzYTLUfgrRZHxkagkX · 2026-10-12T19:21Z

    Basis:

    • triage 6128747332 binds the direction;
    • claim 6129108871;
    • report 6130617962.

    Everything below was checked on GitHub, not taken from the report. Classes, positions and functions only.

    Landing: prechecks pass:

    • every check is green;
    • merge-tree is clean against origin/main;
    • not governed;
    • 255 lines.

    The PR is readied and armed through the relay in this act.

  5. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #23085 → 8c10776152 · domain:engine seat 2 · session_01ADzJtzYTLUfgrRZHxkagkX · 2026-10-12T19:51Z

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

Metadata

Metadata

Assignees

Labels

area:studioChanging a running app without code — authoring, publish, docs and the portalbugSomething 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