You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
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
PutOptions.actor (packages/metadata-core/src/types.ts) is stored in sys_metadata_history.recorded_by, a lookup(sys_user). Its own text says: "Pass null — never a label like system — when the write has no human actor".
The write does not fail, because put writes under isSystem and the engine's write-time reference check returns early for isSystem (engine.ts, the reference-check guard). The value is simply a dangling reference.
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.
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.
Lane: one PR, so the four producers and the describe change together. domain:engine claims it (the contract and protocol.ts), and the claim declares the rest, runtime and cli files under the cross-domain exception path.
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/main275ad4229a; stop on a breach and explain it in the report). ⛔ Classes, positions and functions only, in every artefact.
packages/metadata-protocol/src/protocol.ts: migrateStoredMetadata's actor default.
the POST …/meta/_migrate-stored handler in packages/rest/src/rest-server.ts;
the same door in packages/runtime/src/domains/meta.ts;
os migrate meta --stored in packages/cli/src/commands/migrate/meta.ts.
Tests beside each producer, with the card's three pins; one changeset.
Container & model: S–M, mode:subagent, model: default (dispatch-gates --tier: no path-derived mandate).
Clause-②: no
No accept set or export changes. Producers stop writing an off-contract value.
Responsibility: the four producers, which pass a label as PutOptions.actor against its own contract (null or a user id, never a label) | sys_metadata_history.recorded_by 是 lookup('sys_user') 却存哨兵字符串 'system'——声明的类型与实际存的值不是一回事 #4556 fixed the repository side only | every migrate-stored pass. The label lands in sys_metadata_history.recorded_by, and since 0a29bec6a3 in sys_metadata.updated_by on rewritten rows, as a dangling reference
Thread-read: 6128747332
Serial constraints cleared:
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"
]
}
On main at that commit: the squash carries the accepted change exactly. Every file has hunks identical to the accepted head.
What main now holds: the migrate-stored pass records the user who ran it, or null, in recorded_by and updated_by. It never records a label. This holds through the protocol, POST …/meta/_migrate-stored, the runtime dispatcher, and os migrate meta --stored. MetadataItemSchema.authoredBy's describe states the same rule.
Card: closed completed by the PR's Fixes #23068. pm:dispatched comes off in this act.
Activity
objectstack-fleet commentedon Oct 12, 2026
Triage: first grade,
bug·priority:p3·domain:engine·area:studio, intopm:queue. Every producer passes the user's id ornull. One PR under the cross-domain exceptionTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-12T16:54Z. ⛔ Not a claim, ⛔ not a dispatch.updated_byafter PR fix(metadata-protocol): a metadata row records who created it and who last changed it #23067. Nothing fails, and the value is a dangling reference.PutOptions.actortakes the acting user's id ornull, persys_metadata_history.recorded_by是lookup('sys_user')却存哨兵字符串'system'——声明的类型与实际存的值不是一回事 #4556's rule. The door's name goes tosourceor a log line.MetadataItemSchema.authoredBy's describe follows that rule. It is the sentence that disagrees, andsys_metadata_history.recorded_by是lookup('sys_user')却存哨兵字符串'system'——声明的类型与实际存的值不是一回事 #4556 already ruled.domain:engineclaims it (the contract andprotocol.ts), and the claim declares therest,runtimeandclifiles under the cross-domain exception path.protocol.ts/rest-server.ts, and therest-server.tssplit ([maintainer] refactor(rest): splitpackages/rest/src/rest-server.ts(13,701 lines, 70 commits in 30 days) along itsregister*Endpointsseams, one move-only slice per PR,/metaafter #22877 stage 2 #23050). Whoever lands later mergesmain.packages/metadata-protocolplus three door sites (domain:engineclaiming); rationale: one contract, four producers.objectstack-fleet commentedon Oct 12, 2026
Claim: PM loop round 1 · 2026-10-12T17:18Z
Session:
session_01ADzJtzYTLUfgrRZHxkagkXAccount:
os-steve(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23068-migrate-actorWorktree:
objectstack-issue-23068Domain:
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 PMTriage-ref: 6128747332 (every producer passes the acting user's id or
null; the door's name goes tosourceor a log line; ⛔ no repository-side filter)File surface (read on
origin/main275ad4229a; stop on a breach and explain it in the report). ⛔ Classes, positions and functions only, in every artefact.packages/metadata-protocol/src/protocol.ts:migrateStoredMetadata's actor default.packages/metadata-core/src/types.ts:MetadataItemSchema.authoredBy's describe followssys_metadata_history.recorded_by是lookup('sys_user')却存哨兵字符串'system'——声明的类型与实际存的值不是一回事 #4556's rule. This is prose.domain:cli:POST …/meta/_migrate-storedhandler inpackages/rest/src/rest-server.ts;packages/runtime/src/domains/meta.ts;os migrate meta --storedinpackages/cli/src/commands/migrate/meta.ts.Container & model:
S–M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate).Clause-②: no
Responsibility: the four producers, which pass a label as
PutOptions.actoragainst its own contract (nullor a user id, never a label) |sys_metadata_history.recorded_by是lookup('sys_user')却存哨兵字符串'system'——声明的类型与实际存的值不是一回事 #4556 fixed the repository side only | every migrate-stored pass. The label lands insys_metadata_history.recorded_by, and since0a29bec6a3insys_metadata.updated_byon rewritten rows, as a dangling referenceThread-read: 6128747332
Serial constraints cleared:
protocol.ts. PR feat(metadata-protocol): one language of an email template is addressed by name — the by-name /meta doors accept a bundle member selector (#22877, stage 2) #23070 also edits other handlers ofrest-server.ts. These are textual neighbours, and the later lander mergesmain.packages/rest/src/rest-server.ts(13,701 lines, 70 commits in 30 days) along itsregister*Endpointsseams, one move-only slice per PR,/metaafter #22877 stage 2 #23050 (therest-server.tssplit) is unclaimed inpm:queue. If it is claimed first, this PR's one handler hunk moves with it.objectstack-fleet commentedon Oct 12, 2026
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
objectstack-fleet commentedon Oct 12, 2026
✅ ACCEPT: PR #23085 at
167dca9b79·domain:engineseat 2 ·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-12T19:21ZBasis:
Everything below was checked on GitHub, not taken from the report. Classes, positions and functions only.
Form:
main, assigneeos-steve;Fixes #23068, the only closing keyword;Clause-②: no.Scope:
check-governed-merges: clean);packages/specsource, so no contract surface, and the seat read the diff itself;content/docs/releases/;rest,runtimeandclifiles are cross-lane, declared on [PM seat] domain:cli · seat 2 — 🟢 sunhaolin · session_01Q9PZ5Pzqcus4woVvTZwWNU · p1+p2, serial (maintainer) · landed 6 · in flight 1 (#23492) #22648.Fix, read in the diff:
migrateStoredMetadataforwardsrequest.actoronly when one is given, sosaveMetaItem'ssys_metadata_history.recorded_by是lookup('sys_user')却存哨兵字符串'system'——声明的类型与实际存的值不是一回事 #4556 default recordsnullinstead of'migrate-stored';POST /_migrate-storedhandler takes its actor fromresolveMetaWriteActor, the producer every/metawrite door shares;executionContext.userIdor nothing;os migrate meta --storedpasses nothing;MetadataItemSchema.authoredBy's describe now states thesys_metadata_history.recorded_by是lookup('sys_user')却存哨兵字符串'system'——声明的类型与实际存的值不是一回事 #4556 rule;source: 'migrate-stored';Accepted deviation: one prose sentence in
resolveMetaWriteActor's header now lists the route. Same file; without it the header would be false.Evidence:
origin/main, a probe read the three labels inrecorded_byandupdated_by;CI on
167dca9b79: 40 check runs, none failed.The dev's open question (cleaning up rows past passes wrote): no card now. The seat follows the dev's own condition: a cleanup journal beside
recorded-by-sentinelwaits for evidence that a real deployment holds such rows. This is recorded in metadata: the remaining PutOptions.actor producers that give a label or drop the user (plugin-security 'system', metadata-fs 'fs', the runtime dispatcher's PUT); one family card after #23068 #23086's "Not here".Findings: filed metadata: the remaining PutOptions.actor producers that give a label or drop the user (plugin-security 'system', metadata-fs 'fs', the runtime dispatcher's PUT); one family card after #23068 #23086, one card for the family, enumerating its remaining positions:
'system', which closes with C3 #15204 stages 7a ∥ 7b → 8-post, build ahead: delete the permission-set projection and the position write-through that stage 8 leaves unreferenced, then the post-stage-8 catalog gates and residue #22975's 7a and 7b deletions;'fs'default;PUT, which drops the user.Acceptance note:
metadata-core's test fixture'cli'(the schema still accepts strings).Landing: prechecks pass:
origin/main;The PR is readied and armed through the relay in this act.
objectstack-fleet commentedon Oct 12, 2026
Landed: PR #23085 →
8c10776152·domain:engineseat 2 ·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-12T19:51Z8c1077615205d16e73fcbe77793d9ec428ff32e5, with one parent,a22e67b3c1. The ACCEPT is on this card, from the seat's own diff read (no contract surface).mainat that commit: the squash carries the accepted change exactly. Every file has hunks identical to the accepted head.mainnow holds: the migrate-stored pass records the user who ran it, ornull, inrecorded_byandupdated_by. It never records a label. This holds through the protocol,POST …/meta/_migrate-stored, the runtime dispatcher, andos migrate meta --stored.MetadataItemSchema.authoredBy's describe states the same rule.completedby the PR'sFixes #23068.pm:dispatchedcomes off in this act.