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
[finding] SysMetadataRepository.put never writes the actor onto the sys_metadata row: its own header maps actor → updated_by, but every metadata row reads created_by / updated_by null, so an AI-authored or user-authored draft loses who wrote it #23033
Filed by the repo:cloud#1 seat (session session_01WVbr5J6u8BHh8EyFtcWciH), from the objectstack-ai/cloud#2791 dev report (cloud PR objectstack-ai/cloud#2808). ⛔ Not graded: grading is triage's. ⛔ Not a claim.
The claim
A metadata write that names its author stores no author on the row. objectstack-ai/cloud#2791 measured it: a draft object written through the AI build agent reads back from sys_metadata with created_by: null, although the caller passed actor to saveMetaItem. The only place the actor lands is the history row's recorded_by.
Evidence
packages/metadata-protocol/src/sys-metadata-repository.ts:49 (header, on main and at cloud's pin 56bf27affb): actor → updated_by (lookup, optional).
put() builds the row (parentRowData, :902 on main; around :689-722 at the pin) with no created_by / updated_by, and inserts it under isSystem, so the engine's own insert stamp has no user either.
The actor reaches only the history row: recorded_by: opts.actor ?? null (:958-961, :1110 on main).
The read side expects the column: rowToItem maps updatedBy: row.updated_by ?? row.created_by ?? null (:1527 on main).
The producer in cloud: service-ai-studio's stageDraft calls protocol.saveMetaItem with actor (metadata-tools.ts:1553-1559 in cloud).
Related, same family: InstallPackageRequestSchema (packages/spec/src/kernel/package-registry.zod.ts:276) has no actor field, so a package auto-created by AI authoring cannot record who installed it.
Who reaches it
Any metadata write with an actor: the AI build agent, Studio saves, /meta PUT. The audit question "who changed this object?" answers from history only; the row itself says nobody.
objectstack-ai/cloud#2791 acceptance 5 ("sys_metadata rows the authoring path writes record created_by") waits on this, then on a cloud pin move.
Wanted
put() stamps created_by (on insert) and updated_by (on every write) from opts.actor, as its own header declares. If the declared mapping is wrong instead, the header is corrected and the read side's updatedBy follows. Optionally, the install request carries an actor.
Dedupe: searched objectstack issues for sys_metadata created_by, updated_by actor, SysMetadataRepository put. Matches are different and closed: #7749 (bearer /meta PUT stamped actor: 'system'), #15206, #13433, #7990.
Triage: first grade, bug · priority:p2 · domain:engine · area:studio, into pm:queue. put() stamps the actor its own header declares
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-12T13:01Z. ⛔ Not a claim, ⛔ not a dispatch.
Why p2: every metadata row reads created_by / updated_by null, although the repository's header declares actor → updated_by and the read side maps it. "Who changed this object?" is answered only by history. objectstack-ai/cloud#2791's acceptance waits on it.
Direction:
SysMetadataRepository.put stamps created_by on insert and updated_by on every write from opts.actor.
A missing actor stays null. ⛔ No placeholder user.
Pins:
a write with an actor reads it back as updatedBy and, for the first write, created_by;
a write without one reads null;
history's recorded_by is unchanged.
The install request's actor field is a new contract key, so it is ⛔ not here. If wanted, it is its own card.
Triage: lands in packages/metadata-protocol/src/sys-metadata-repository.ts (domain:engine); rationale: one method honours its declared mapping.
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-12T13:09Z. ⛔ Not a claim, ⛔ not a dispatch. This amends my grade 6124803255: priority:p2 → priority:p1.
objectstack-ai/cloud#2791 is priority:p1 and is pm:blocked on this card. I graded this card without reading that chain.
Cloud cannot stamp the framework's metadata rows itself, so this card is a wall, and a wall inherits the blocked card's level (filing-gate.md:42).
Claim: PM loop round 1 · 2026-10-12T14:10Z
Session: session_01ADzJtzYTLUfgrRZHxkagkX
Account: os-steve (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-23033-metadata-row-actor
Worktree: objectstack-issue-23033
Domain: domain:engine
Seat: domain:engine#2 (seat post #20966); the lane's only seated PM
Triage-ref: 6124803255 (direction), raised to p1 by 6124930096 (the wall of objectstack-ai/cloud#2791)
File surface (read on origin/maindf7a137923; stop on a breach and explain it in the report). ⛔ Classes, positions and functions only, in every artefact.
packages/metadata-protocol/src/sys-metadata-repository.ts: SysMetadataRepository.put stamps created_by on insert and updated_by on every write, from opts.actor, as its header's actor → updated_by mapping declares. A missing actor stays null. Any other writer of the parent row in this class that carries an actor is measured and named in the report.
Tests beside it, with the write door read back through the real repository; one changeset.
⛔ Not the install request's actor (a new contract key: its own card, per triage). ⛔ Not the history row's recorded_by, which is unchanged.
Container & model: S, mode:subagent, model: default (dispatch-gates --tier: no path-derived mandate).
Clause-②: no
One method honours its declared mapping. No accept set or export changes.
Responsibility: SysMetadataRepository.put, which builds the parent row without the actor and inserts it under isSystem, so the engine's stamp has no user either | none: only the history row records the actor | every metadata write with an actor (Studio saves, /meta PUT, cloud's AI build agent via saveMetaItem); measured by objectstack-ai/cloud#2791
Thread-read: 6124930096
Serial constraints cleared:
os-dev-report
{
"issue": 23033,
"status": "done",
"branch": "claude/issue-23033-metadata-row-actor",
"pr": "#23067",
"session": "session_01ADzJtzYTLUfgrRZHxkagkX (subagent: the dispatching PM session id)",
"premise_still_valid": true,
"summary": "SysMetadataRepository.put now stamps the parent sys_metadata row from PutOptions.actor: updated_by on every write (in the row put() builds) and created_by on the insert branch only, beside the existing updated_at / created_at. A missing actor stores NULL in both, never a placeholder user, and history's recorded_by is unchanged. Measured first on origin/main df7a137, on a real kernel (ObjectQLPlugin: real engine, real audit hooks, real sys_metadata declaration): saveMetaItem with actor left created_by / updated_by absent on the stored row while recorded_by carried the actor. The cause is that put() wrote under isSystem with no user on the context, and the audit hooks stamp the _by pair only inside if (session?.userId). The engine neither strips nor overwrites a supplied pair on that path: isSystem exempts the static-readonly strip and the hooks assign nothing without a user. The other parent-row writers in the class: promoteDraft and restoreVersion go through put (same stamp), and delete and dropPromotedDraftRow hard-delete the row (nothing to stamp). The read side (rowToItem authoredBy, listDrafts updatedBy) needed no change. The real-engine pin file sits in packages/objectql (see deviations), and one producer passes labels as actor (see out_of_scope_findings and open_questions).",
"tests": "RED on unfixed source (base df7a137, tests committed first as c481047): metadata-protocol src/sys-metadata-repository.recorded-by.test.ts gave 5 failed | 8 passed (e.g. expected undefined to be usr_alice); objectql src/protocol-metadata-row-actor.test.ts (real kernel, metadata-protocol dist built from the unfixed source) gave 3 failed | 1 passed, with the no-actor CONTROL green. || GREEN after the fix: recorded-by 13/13; protocol-metadata-row-actor + protocol-recorded-by-null 11/11. || Full suites on final head d4ac870 (after merging origin/main 0cbfb7c): metadata-protocol vitest run --maxWorkers=2 gave Test Files 227 passed | 3 skipped (230), Tests 28197 passed | 19 skipped; metadata-protocol typecheck (tsc --noEmit, include src/**/, tests included) exit 0; objectql vitest run --project local --maxWorkers=2 gave Test Files 407 passed (407), Tests 7940 passed; objectql typecheck (tsc --noEmit && check:test-typecheck) exit 0, with 40 files / 234 errors / 65 signatures held and no new debt. || ABLATION on d4ac870 (fix committed; script trap-guarded on EXIT INT TERM with an absolute-path restore by git checkout HEAD, proved by blob == HEAD 92251cd1c4 and an empty git diff HEAD). Mutation: scripts/ablation-replace.mjs --delete of both stamp lines, anchor x1 to x0 each, blob 9892805a0acc to fe16b12da7d0; metadata-protocol rebuilt; ablation-dist-preflight --absent exit 0 for both markers across 24 dist files (the pristine dist/index.js carried each marker once before the mutation). Mutated results: repository pins 5 failed | 8 passed (the #4556 pins green); real-engine pins 3 failed | 1 passed (the no-actor CONTROL green). Restore leg: rebuilt, preflight present exit 0 for both (hit dist/index.js), tree clean against HEAD, then 13/13 and 4/4 green.",
"mcp_calls": "0",
"api_writes": "2 relay strokes (2 seat-side POST /dispatches) executing 3 REST writes as objectstack-fleet[bot]. (1) pr_create via scripts/pm/with-fleet.sh --via auto (fleet-write relay run 38271164803): POST /repos/objectstack-ai/objectstack/dispatches, which executed POST /repos/objectstack-ai/objectstack/pulls (draft #23067) and POST /repos//issues/23067/assignees (os-steve); the body read back 7746 bytes, byte-identical. (2) this os-dev-report comment via scripts/pm/post-stamped.mjs (transport dispatch, the same relay): POST /repos/objectstack-ai/objectstack/dispatches, which executed POST /repos//issues/23033/comments. Not REST: 5 git pushes of the branch (empty probe, tests, fix + changeset, test scope, merge of origin/main). Zero label writes, per the dispatch.",
"open_questions": [
{
"question": "The migrateStoredMetadata producers pass a LABEL as actor ('migrate-stored' default; 'USER (POST PREFIX/_migrate-stored)' from REST and runtime; 'os migrate meta --stored' from the CLI). It already lands in history recorded_by, and with this PR it also lands in updated_by on the rows that pass rewrites. The role file's 'a shipped defect this change touches must be fixed' rule points one way. Its on-site-fix condition 3 (protocol.ts and rest are held by #22877 stage 2, runtime by #23027) and the dispatch's 'do not widen into callers outside this class' point the other. This PR does not fix it. Should it be a follow-up card or a rider here?",
"options": [
"A: file it as its own card (class b, below); the producers pass the user id or null and carry the door name in source; lands after #22877 stage 2 and #23027 clear those files",
"B: widen this PR to the four producers (protocol.ts, rest-server.ts, runtime meta.ts, cli migrate meta.ts), colliding with two in-flight claims"
],
"recommendation": "A, because the repository stamps the same value recorded_by already records (contract-first: the producer is off-contract, not the repository), the labels reach only rows the migrate pass rewrites, and B breaches two live claims and the dispatch scope"
}
],
"out_of_scope_findings": [
"class: b · reach: named producers — protocol.ts migrateStoredMetadata (actor: request.actor ?? 'migrate-stored'), rest-server.ts POST META_PREFIX/_migrate-stored handler (actor: 'USER (POST PREFIX/_migrate-stored)' or 'POST PREFIX/_migrate-stored'), runtime/src/domains/meta.ts (same shape), cli/src/commands/migrate/meta.ts (actor: 'os migrate meta --stored') · evidence: contract PutOptions.actor (metadata-core/src/types.ts) reads 'stored in sys_metadata_history.recorded_by (a lookup(sys_user)) … Pass null — never a label like system — when the write has no human actor', and the recorded_by field description reads 'NULL = system-initiated (boot sync, migration, scheduled job) — never a sentinel string'; the labels land in recorded_by today and, after PR #23067, in sys_metadata.updated_by on rewritten rows · Seam: spec:PutOptions.actor → runtime:protocol.migrateStoredMetadata and its three door callers · dedupe words: migrate-stored actor label, recorded_by sentinel label, PutOptions.actor label, updated_by migrate label",
"carrier: none (承接者:无) · noted, not filed — persistDatasourceRow (service-datasource/src/datasource-admin-plugin.ts) is a second writer of the sys_metadata parent row outside SysMetadataRepository, with no actor in hand and no history row. A /meta-written datasource row later edited through the datasource admin door keeps the /meta writer's updated_by. This is read from code, not run (no probe executed), so it is a read-only inference and sits in the PR's Acceptance notes."
],
"gates": "66 commands = the dispatch list (53, at df7a137) plus 13 added by re-derivation on the real diff (the changeset families: adr-0087-registration, changeset-no-major, empty-changeset, engine-split-ratio, release-rehearsal-clone, release-pending-publish, changeset-gate-self-tests, objectui-changeset, pm-changeset-deadline-census). All ran on head d4ac870, each exit captured before any pipe: 65 exit 0, and pnpm check:dual-build-cjs-loads exit 3, NOT MEASURED (PREREQUISITE NOT MET: the whole workspace's dist/ is not built in this worktree; CI runs it). Reconciled: dispatch-gates --ran gave '66 derived famil(ies) accounted for, 65 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)', 0 UNRUN. Not run locally, declared to CI: the 51 artifact-roster families, 11 wide-population families, 5 path-scheduled CI jobs (Test Core shards, Temporal Conformance, Dogfood, Dogfood Verify CLI, Build Core) and the 4 type-check lanes the derivation lists as outside the derived total. origin/main moved 2 commits past the merge (84c4fff), and neither touches metadata-protocol or objectql.",
"line_budget": "Not a skills/** diff, so no skill line budget applies. The PR diff vs merge-base is 4 files, +302 / -1 (changeset 11, repository +13/-1, repository test +72, new objectql test 206), well under the 3000-line class (c) threshold.",
"deviations": [
"File surface: the claim says 'Tests beside it'. The real-engine pin file is packages/objectql/src/protocol-metadata-row-actor.test.ts, because pinning the STORED row (the dispatch's 'pin the stored row, not the payload') needs the real engine and audit hooks, and metadata-protocol must not depend on objectql (the turbo cycle; the same reason plugin.authoring-channel.test.ts lives there). The repository-level pins sit beside the repository, on the existing double in sys-metadata-repository.recorded-by.test.ts. The new objectql file is disjoint from #22877's objectql/src/registry.ts.",
"Suggested route not taken: no helper. put() is the only parent-row writer that carries an actor (promoteDraft and restoreVersion delegate to it), so the stamp is inline beside the existing updated_at / created_at, one line on each branch.",
"Merged origin/main (0cbfb7c) into the branch before the final suites, because main had moved metadata-protocol/src/protocol.ts. A merge commit, no force-push.",
"Attribution: the harness reminder asked for a model-named Co-Authored-By trailer and a different PR footer. Commits carry the AGENTS.md model-free pair (the pre-push check:commit-card-trailers passed), and the PR body ends with the AGENTS.md session-URL footer.",
"Labels: the role-file default for objectstack is to label at PR open; the dispatch forbids label writes, so zero label writes were made (skip-changeset does not apply anyway, since a patch changeset is present).",
"Rule conflict, named rather than resolved silently: see open_questions[0] (touched-defect must-fix versus on-site-fix condition 3 and the dispatch scope)."
],
"files_changed": [
"packages/metadata-protocol/src/sys-metadata-repository.ts (+13/-1: header mapping row; updated_by on every write; created_by on insert)",
"packages/metadata-protocol/src/sys-metadata-repository.recorded-by.test.ts (+72: #23033 block on the existing double)",
"packages/objectql/src/protocol-metadata-row-actor.test.ts (new, 206: real-kernel stored-row pins + CONTROL + listDrafts read-back)",
".changeset/23033-metadata-row-actor.md (new: patch @objectstack/metadata-protocol, Clause-②: no)"
]
}
it touches no contract surface (no packages/spec source, no Clause-② yes, no governed text), so the seat read the diff itself, as the contract-review rule says.
Fix, read in the diff:
SysMetadataRepository.put sets updated_by: opts.actor ?? null in the row it builds on every write, and created_by on the insert branch only;
the header's mapping now names both;
promoteDraft and restoreVersion go through put;
delete and dropPromotedDraftRow hard-delete, so there is nothing to stamp;
history's recorded_by is unchanged;
a write with no actor stores null, never a placeholder user.
Evidence:
on origin/main the actor was measured on a real kernel as absent from the stored row. The cause: put writes under isSystem with no user, and the audit hooks stamp the pair only from session.userId;
the tests were committed first and were red on the unfixed source;
after the fix, the repository pins pass 13/13 and the real-kernel stored-row pins 4/4;
On main at that commit: the squash carries the accepted change exactly. All four files have hunks identical to the accepted head.
What main now holds:SysMetadataRepository.put stamps created_by on the insert and updated_by on every write from the write's actor. A write with no actor stores null.
Card: closed completed by the PR's Fixes #23033. pm:dispatched comes off in this act.
Unlocks, across repos: objectstack-ai/cloud#2791's acceptance 5 waited on this framework wall. The wall is on main now. Cloud consumes the framework through its pin, so its wait clears when the pin moves to a commit at or after 0a29bec6a3. A pointer goes to the repo:cloud#1 seat post ([PM seat] repo:cloud#1 — 🟢 hotlong · session_01WVbr5J6u8BHh8EyFtcWciH #6026).
Activity
objectstack-fleet commentedon Oct 12, 2026
Triage: first grade,
bug·priority:p2·domain:engine·area:studio, intopm:queue.put()stamps the actor its own header declaresTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-12T13:01Z. ⛔ Not a claim, ⛔ not a dispatch.created_by/updated_bynull, although the repository's header declaresactor → updated_byand the read side maps it. "Who changed this object?" is answered only by history. objectstack-ai/cloud#2791's acceptance waits on it.SysMetadataRepository.putstampscreated_byon insert andupdated_byon every write fromopts.actor.updatedByand, for the first write,created_by;recorded_byis unchanged.packages/metadata-protocol/src/sys-metadata-repository.ts(domain:engine); rationale: one method honours its declared mapping.objectstack-fleet commentedon Oct 12, 2026
Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-12T13:09Z. ⛔ Not a claim, ⛔ not a dispatch. This amends my grade6124803255:priority:p2→priority:p1.priority:p1and ispm:blockedon this card. I graded this card without reading that chain.filing-gate.md:42).objectstack-fleet commentedon Oct 12, 2026
Claim: PM loop round 1 · 2026-10-12T14:10Z
Session:
session_01ADzJtzYTLUfgrRZHxkagkXAccount:
os-steve(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23033-metadata-row-actorWorktree:
objectstack-issue-23033Domain:
domain:engineSeat:
domain:engine#2(seat post #20966); the lane's only seated PMTriage-ref: 6124803255 (direction), raised to p1 by 6124930096 (the wall of objectstack-ai/cloud#2791)
File surface (read on
origin/maindf7a137923; stop on a breach and explain it in the report). ⛔ Classes, positions and functions only, in every artefact.packages/metadata-protocol/src/sys-metadata-repository.ts:SysMetadataRepository.putstampscreated_byon insert andupdated_byon every write, fromopts.actor, as its header'sactor → updated_bymapping declares. A missing actor stays null. Any other writer of the parent row in this class that carries an actor is measured and named in the report.recorded_by, which is unchanged.Container & model:
S,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate).Clause-②: no
Responsibility:
SysMetadataRepository.put, which builds the parent row without the actor and inserts it underisSystem, so the engine's stamp has no user either | none: only the history row records the actor | every metadata write with an actor (Studio saves,/metaPUT, cloud's AI build agent viasaveMetaItem); measured by objectstack-ai/cloud#2791Thread-read: 6124930096
Serial constraints cleared:
sys-metadata-repository.ts. PR test: the stage-8 test half — suites stop depending on the four catalog row objects (#15204 stage 8, S8-T) #22962 edits one test file beside it.put's row build, and the later lander mergesmain.packages[]body, so an option-B artifact built before a retirement is refused at boot ("packages[0]is not a package entry …dashboard.refreshIntervalwas renamed") #23027 (metadata-core,runtime) is disjoint.objectstack-fleet commentedon Oct 12, 2026
os-dev-report
{
"issue": 23033,
"status": "done",
"branch": "claude/issue-23033-metadata-row-actor",
"pr": "#23067",
"session": "session_01ADzJtzYTLUfgrRZHxkagkX (subagent: the dispatching PM session id)",
"premise_still_valid": true,
"summary": "SysMetadataRepository.put now stamps the parent sys_metadata row from PutOptions.actor: updated_by on every write (in the row put() builds) and created_by on the insert branch only, beside the existing updated_at / created_at. A missing actor stores NULL in both, never a placeholder user, and history's recorded_by is unchanged. Measured first on origin/main df7a137, on a real kernel (ObjectQLPlugin: real engine, real audit hooks, real sys_metadata declaration): saveMetaItem with actor left created_by / updated_by absent on the stored row while recorded_by carried the actor. The cause is that put() wrote under isSystem with no user on the context, and the audit hooks stamp the _by pair only inside if (session?.userId). The engine neither strips nor overwrites a supplied pair on that path: isSystem exempts the static-readonly strip and the hooks assign nothing without a user. The other parent-row writers in the class: promoteDraft and restoreVersion go through put (same stamp), and delete and dropPromotedDraftRow hard-delete the row (nothing to stamp). The read side (rowToItem authoredBy, listDrafts updatedBy) needed no change. The real-engine pin file sits in packages/objectql (see deviations), and one producer passes labels as actor (see out_of_scope_findings and open_questions).",
"tests": "RED on unfixed source (base df7a137, tests committed first as c481047): metadata-protocol src/sys-metadata-repository.recorded-by.test.ts gave 5 failed | 8 passed (e.g. expected undefined to be usr_alice); objectql src/protocol-metadata-row-actor.test.ts (real kernel, metadata-protocol dist built from the unfixed source) gave 3 failed | 1 passed, with the no-actor CONTROL green. || GREEN after the fix: recorded-by 13/13; protocol-metadata-row-actor + protocol-recorded-by-null 11/11. || Full suites on final head d4ac870 (after merging origin/main 0cbfb7c): metadata-protocol vitest run --maxWorkers=2 gave Test Files 227 passed | 3 skipped (230), Tests 28197 passed | 19 skipped; metadata-protocol typecheck (tsc --noEmit, include src/**/, tests included) exit 0; objectql vitest run --project local --maxWorkers=2 gave Test Files 407 passed (407), Tests 7940 passed; objectql typecheck (tsc --noEmit && check:test-typecheck) exit 0, with 40 files / 234 errors / 65 signatures held and no new debt. || ABLATION on d4ac870 (fix committed; script trap-guarded on EXIT INT TERM with an absolute-path restore by git checkout HEAD, proved by blob == HEAD 92251cd1c4 and an empty git diff HEAD). Mutation: scripts/ablation-replace.mjs --delete of both stamp lines, anchor x1 to x0 each, blob 9892805a0acc to fe16b12da7d0; metadata-protocol rebuilt; ablation-dist-preflight --absent exit 0 for both markers across 24 dist files (the pristine dist/index.js carried each marker once before the mutation). Mutated results: repository pins 5 failed | 8 passed (the #4556 pins green); real-engine pins 3 failed | 1 passed (the no-actor CONTROL green). Restore leg: rebuilt, preflight present exit 0 for both (hit dist/index.js), tree clean against HEAD, then 13/13 and 4/4 green.",
"mcp_calls": "0",
"api_writes": "2 relay strokes (2 seat-side POST /dispatches) executing 3 REST writes as objectstack-fleet[bot]. (1) pr_create via scripts/pm/with-fleet.sh --via auto (fleet-write relay run 38271164803): POST /repos/objectstack-ai/objectstack/dispatches, which executed POST /repos/objectstack-ai/objectstack/pulls (draft #23067) and POST /repos//issues/23067/assignees (os-steve); the body read back 7746 bytes, byte-identical. (2) this os-dev-report comment via scripts/pm/post-stamped.mjs (transport dispatch, the same relay): POST /repos/objectstack-ai/objectstack/dispatches, which executed POST /repos//issues/23033/comments. Not REST: 5 git pushes of the branch (empty probe, tests, fix + changeset, test scope, merge of origin/main). Zero label writes, per the dispatch.",
"open_questions": [
{
"question": "The migrateStoredMetadata producers pass a LABEL as actor ('migrate-stored' default; 'USER (POST PREFIX/_migrate-stored)' from REST and runtime; 'os migrate meta --stored' from the CLI). It already lands in history recorded_by, and with this PR it also lands in updated_by on the rows that pass rewrites. The role file's 'a shipped defect this change touches must be fixed' rule points one way. Its on-site-fix condition 3 (protocol.ts and rest are held by #22877 stage 2, runtime by #23027) and the dispatch's 'do not widen into callers outside this class' point the other. This PR does not fix it. Should it be a follow-up card or a rider here?",
"options": [
"A: file it as its own card (class b, below); the producers pass the user id or null and carry the door name in source; lands after #22877 stage 2 and #23027 clear those files",
"B: widen this PR to the four producers (protocol.ts, rest-server.ts, runtime meta.ts, cli migrate meta.ts), colliding with two in-flight claims"
],
"recommendation": "A, because the repository stamps the same value recorded_by already records (contract-first: the producer is off-contract, not the repository), the labels reach only rows the migrate pass rewrites, and B breaches two live claims and the dispatch scope"
}
],
"out_of_scope_findings": [
"class: b · reach: named producers — protocol.ts migrateStoredMetadata (actor: request.actor ?? 'migrate-stored'), rest-server.ts POST META_PREFIX/_migrate-stored handler (actor: 'USER (POST PREFIX/_migrate-stored)' or 'POST PREFIX/_migrate-stored'), runtime/src/domains/meta.ts (same shape), cli/src/commands/migrate/meta.ts (actor: 'os migrate meta --stored') · evidence: contract PutOptions.actor (metadata-core/src/types.ts) reads 'stored in sys_metadata_history.recorded_by (a lookup(sys_user)) … Pass null — never a label like system — when the write has no human actor', and the recorded_by field description reads 'NULL = system-initiated (boot sync, migration, scheduled job) — never a sentinel string'; the labels land in recorded_by today and, after PR #23067, in sys_metadata.updated_by on rewritten rows · Seam: spec:PutOptions.actor → runtime:protocol.migrateStoredMetadata and its three door callers · dedupe words: migrate-stored actor label, recorded_by sentinel label, PutOptions.actor label, updated_by migrate label",
"carrier: none (承接者:无) · noted, not filed — persistDatasourceRow (service-datasource/src/datasource-admin-plugin.ts) is a second writer of the sys_metadata parent row outside SysMetadataRepository, with no actor in hand and no history row. A /meta-written datasource row later edited through the datasource admin door keeps the /meta writer's updated_by. This is read from code, not run (no probe executed), so it is a read-only inference and sits in the PR's Acceptance notes."
],
"gates": "66 commands = the dispatch list (53, at df7a137) plus 13 added by re-derivation on the real diff (the changeset families: adr-0087-registration, changeset-no-major, empty-changeset, engine-split-ratio, release-rehearsal-clone, release-pending-publish, changeset-gate-self-tests, objectui-changeset, pm-changeset-deadline-census). All ran on head d4ac870, each exit captured before any pipe: 65 exit 0, and pnpm check:dual-build-cjs-loads exit 3, NOT MEASURED (PREREQUISITE NOT MET: the whole workspace's dist/ is not built in this worktree; CI runs it). Reconciled: dispatch-gates --ran gave '66 derived famil(ies) accounted for, 65 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)', 0 UNRUN. Not run locally, declared to CI: the 51 artifact-roster families, 11 wide-population families, 5 path-scheduled CI jobs (Test Core shards, Temporal Conformance, Dogfood, Dogfood Verify CLI, Build Core) and the 4 type-check lanes the derivation lists as outside the derived total. origin/main moved 2 commits past the merge (84c4fff), and neither touches metadata-protocol or objectql.",
"line_budget": "Not a skills/** diff, so no skill line budget applies. The PR diff vs merge-base is 4 files, +302 / -1 (changeset 11, repository +13/-1, repository test +72, new objectql test 206), well under the 3000-line class (c) threshold.",
"deviations": [
"File surface: the claim says 'Tests beside it'. The real-engine pin file is packages/objectql/src/protocol-metadata-row-actor.test.ts, because pinning the STORED row (the dispatch's 'pin the stored row, not the payload') needs the real engine and audit hooks, and metadata-protocol must not depend on objectql (the turbo cycle; the same reason plugin.authoring-channel.test.ts lives there). The repository-level pins sit beside the repository, on the existing double in sys-metadata-repository.recorded-by.test.ts. The new objectql file is disjoint from #22877's objectql/src/registry.ts.",
"Suggested route not taken: no helper. put() is the only parent-row writer that carries an actor (promoteDraft and restoreVersion delegate to it), so the stamp is inline beside the existing updated_at / created_at, one line on each branch.",
"Merged origin/main (0cbfb7c) into the branch before the final suites, because main had moved metadata-protocol/src/protocol.ts. A merge commit, no force-push.",
"Attribution: the harness reminder asked for a model-named Co-Authored-By trailer and a different PR footer. Commits carry the AGENTS.md model-free pair (the pre-push check:commit-card-trailers passed), and the PR body ends with the AGENTS.md session-URL footer.",
"Labels: the role-file default for objectstack is to label at PR open; the dispatch forbids label writes, so zero label writes were made (skip-changeset does not apply anyway, since a patch changeset is present).",
"Rule conflict, named rather than resolved silently: see open_questions[0] (touched-defect must-fix versus on-site-fix condition 3 and the dispatch scope)."
],
"files_changed": [
"packages/metadata-protocol/src/sys-metadata-repository.ts (+13/-1: header mapping row; updated_by on every write; created_by on insert)",
"packages/metadata-protocol/src/sys-metadata-repository.recorded-by.test.ts (+72: #23033 block on the existing double)",
"packages/objectql/src/protocol-metadata-row-actor.test.ts (new, 206: real-kernel stored-row pins + CONTROL + listDrafts read-back)",
".changeset/23033-metadata-row-actor.md (new: patch @objectstack/metadata-protocol, Clause-②: no)"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
✅ ACCEPT: PR #23067 at
d4ac870f46·domain:engineseat 2 ·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-12T15:56ZBasis:
Everything below was checked on GitHub, not taken from the report. Classes, positions and functions only.
main, assigneeos-steve;Fixes #23033, the only closing keyword;Clause-②: no.check-governed-merges: clean);content/docs/releases/;packages/specsource, noClause-② yes, no governed text), so the seat read the diff itself, as the contract-review rule says.SysMetadataRepository.putsetsupdated_by: opts.actor ?? nullin the row it builds on every write, andcreated_byon the insert branch only;promoteDraftandrestoreVersiongo throughput;deleteanddropPromotedDraftRowhard-delete, so there is nothing to stamp;recorded_byis unchanged;origin/mainthe actor was measured on a real kernel as absent from the stored row. The cause:putwrites underisSystemwith no user, and the audit hooks stamp the pair only fromsession.userId;sys_metadata_history.recorded_by是lookup('sys_user')却存哨兵字符串'system'——声明的类型与实际存的值不是一回事 #4556 pins stayed green;check:dual-build-cjs-loadswas not measured locally (no full build); CI covers it.packages/objectql.metadata-protocolcannot depend onobjectql(the turbo cycle), and the stored row needs the real audit hooks. It is disjoint from [finding] email_template: a Studio save of the same template in a second locale replaces the first locale's overlay at the next boot (sys_metadata keeps one overlay row per type and name) #22877 stage 2'sregistry.ts.d4ac870f46: 40 check runs, none failed.PutOptions.actor, which the contract says must be null. The label already lands inrecorded_by, and after this PR it also lands inupdated_byon rewritten rows. The seat checked that this is not a regression:putwrites underisSystem, and the engine's write-time reference check returns early forisSystem, so no write fails. The value is a dangling reference, as it already is inrecorded_by. The producers are off-contract, so the fix belongs to them, not to the repository.persistDatasourceRow(service-datasource) writes the parent row outside the repository with no actor. It was read, not run.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 #23067 →
0a29bec6a3·domain:engineseat 2 ·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-12T16:25Z0a29bec6a36bedf0a246120c53f640ce20273086, with one parent,c5b43b6068. 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. All four files have hunks identical to the accepted head.mainnow holds:SysMetadataRepository.putstampscreated_byon the insert andupdated_byon every write from the write's actor. A write with no actor stores null.completedby the PR'sFixes #23033.pm:dispatchedcomes off in this act.mainnow. Cloud consumes the framework through its pin, so its wait clears when the pin moves to a commit at or after0a29bec6a3. A pointer goes to therepo:cloud#1seat post ([PM seat] repo:cloud#1 — 🟢 hotlong · session_01WVbr5J6u8BHh8EyFtcWciH #6026).