Repository navigation
A seed row's explicit created_at is overwritten with the boot instant on INSERT (seed context sets no preserveAudit), yet written on the upsert UPDATE of a later boot — seeds cannot backdate creation time consistently #21646
Description
Activity
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p2·domain:engine·area:records·pm:queue. UnderseedReplay, the insert audit stamp keeps an authoredcreated_at, consistently with the replay. ⛔preserveAuditis not touchedTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-03T23:53Z. ⛔ Not a claim, ⛔ not a dispatch.Measured now on
main:packages/objectql/src/plugin.ts:1150(sys_stamp_audit_insert, registered about:1207) setsrecord.created_at = preserveAudit ? (record.created_at ?? now) : now.SEED_WRITE_EXECUTION_CONTEXT(packages/spec/src/kernel/execution-context.zod.ts:519) is{ isSystem, skipTriggers, seedReplay }, with nopreserveAudit.- So an authored
created_atis overwritten on insert and written on the replay update, as measured.
Why p2. A real app's history depends on it: hotcrm#1992's maintainer-ruled retirement of a duplicate date column waits on this card. Every fresh or reset database shows all seeded rows as created at boot.
Routing: the builtin audit hook in
packages/objectql, sodomain:engine.Ruling: key the insert stamp on
seedReplay, not onpreserveAudit. The governing text is read now:preserveAuditis "UPDATE-only … never when it is created" (maintainer, 2026-08-08;preserveAuditIgnoredOnInsertWarning,rule-validator.ts). ⛔ So the fix does not addpreserveAuditto the seed context. That would also print the "IGNORED on this INSERT" warning for every seed row.- The same warning names the sanctioned route for archival facts on INSERT: a system context.
seedReplay's own rationale (rule-validator.ts, about:459, seed replay vs state_machine initialStates: mid-lifecycle fixture rows are silently rejected on INSERT (showcase kanban can never repopulate) #3433) is "a seed is a snapshot of established facts". - So under
seedReplaythe insert stamp keeps an authoredcreated_at(?? now), as the replay already does.- ⛔ Not under bare
isSystem: a system clone could carry a source row'screated_at. - ⛔ No change for REST or any other caller. #15395's engine-side readonly strip runs AFTER the audit binder, so a plain REST caller's created_at survives on an object that declares it readonly #15964's rule stands.
- ⛔ Not under bare
- Measured first:
created_byunder the same path (keep the authored value or stamp, stated either way);- each of the context's three readers (
SeedLoaderService,AppPlugin's stackdata[]replay,@objectstack/verify'sseed()), which passseedReplayalike.
Pins:
- a seed row with an authored
created_atkeeps it on a fresh boot and on replay; - a seed row without one is stamped at boot;
- a non-seed system insert and a REST insert are both stamped now;
preserveAudit's insert warning is unchanged.
Generated by Claude Code
- addedarea:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Oct 3, 2026 objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsClaim: PM loop round 27 · 2026-10-04T00:30Z
Session:session_017ErfyP2Rx7XWHJA27QjyUi
Account:os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-21646-seed-created-at
Worktree:objectstack-issue-21646
Domain:domain:engine
Seat:domain:engine#1
File surface (atorigin/main759dbe9ed3), per triage's grade and ruling 5974766961:packages/objectql/src/plugin.ts: the builtinsys_stamp_audit_inserthook (around:1150, registered around:1207). UnderseedReplayit keeps an authoredcreated_at(?? now), as the replay update already does.- ⛔
preserveAuditis not touched, and the seed context gains nopreserveAudit.SEED_WRITE_EXECUTION_CONTEXT(packages/spec) is read, not edited. - ⛔ No change for bare
isSystem, for REST or for any other caller. #15395's engine-side readonly strip runs AFTER the audit binder, so a plain REST caller's created_at survives on an object that declares it readonly #15964's rule stands. - Measured first:
created_byon the same path (kept or stamped, stated either way);- the context's three readers:
SeedLoaderService,AppPlugin's stackdata[]replay, and@objectstack/verify'sseed().
- Tests;
.changeset/21646-*.md.
Container & model:S,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate).
Clause-②: no - An authored seed value that was silently dropped on insert is now kept, as the replay already keeps it.
- No accepted input, key, export or error code is added or removed.
- No contract surface is touched.
Thread-read: 5974766961
Serial constraints cleared: at 2026-10-04T00:30Z: - Of the 8 open PRs, none touches
packages/objectql/src/plugin.ts, a seed loader, orexecution-context.zod.ts. - This lane's in-flight claims hold other regions:
- [Decision] security(objectql): may a hook's
handlername bind to a function another package registered (the engine-wide fallback HookSchema.handler declares), or does name resolution stay inside the hook's own package (#21585 option B) #21604 holdshook-binder.ts'sresolveHandlerandengine.ts's function registry; - Two stored view containers of one object still displace each other's views: a container bound elsewhere or unbound under a sibling's expanded name, and a second container's bare list taking the first's
<object>.default, are accepted with no diagnostic #21639 (PR fix(metadata-protocol): one collision predicate at the save door — a stored view container never takes a name already served from elsewhere (#21639, #21638) #21648) holdsmetadata-protocol's save door.
- [Decision] security(objectql): may a hook's
objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21646,
"status": "done",
"branch": "claude/issue-21646-seed-created-at",
"pr": "#21661",
"session": "session_017ErfyP2Rx7XWHJA27QjyUi — mode:subagent, so this is the dispatching PM's harness-stamped id (the Claude-Session trailer on every commit of the branch)",
"premise_still_valid": true,
"summary": "Under the seed write context (ExecutionContext.seedReplay, set by SEED_WRITE_EXECUTION_CONTEXT), sys_stamp_audit_insert in packages/objectql/src/plugin.ts now keeps an authored created_at with?? now, exactly as it already did under preserveAudit. The fresh-boot INSERT now stores the same value as the replay UPDATE. Bare isSystem, REST and every other caller still stamp now. preserveAudit, its insert warning and SEED_WRITE_EXECUTION_CONTEXT are untouched. H1 measured: the hook session that buildSession builds carries no seedReplay. The hook therefore reads it from the beforeInsert envelope's input.options.context, which the HookContext input PHASE contract defines as the caller's own options bag. Declaring a new HookContext.session key in spec would contradict the claim's Clause-2 line. H2: created_by is never stamped on a seed write, because the seed context has no userId; an authored value is kept on insert and replay. This was already true and is unchanged. H3: all three readers pass the one constant and reach the stamp with seedReplay set, so there is no producer to fix. H4: SeedLoaderService resolves cel envelopes before deciding insert or update, so both paths see the evaluated instant. H5: the warning is emitted only from the non-isSystem strip branch, so it cannot fire for a seed write. Pin 4 holds it unchanged for a non-system preserveAudit create.",
"tests": "Head e5a2555 (after merging origin/main 6ec54f0), unless another commit is named. (1) The new pin file packages/objectql/src/plugin-audit-seed-created-at.test.ts boots ObjectKernel with ObjectQLPlugin and runs the real SeedLoaderService.load() twice over one store: pins 1-4 plus a created_by companion, 6 tests. Pre-fix against the unfixed plugin.ts: 'Tests 2 failed | 4 passed (6)'. Both pin-1 cases failed with "expected '2026-10-03T12:00:00.000Z' to be '2026-09-28T00:00:00.000Z'", which is the card's table. (2)pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2over the pin file and four neighbours (plugin-audit-created-at-create-side, plugin-audit-created-by-create-side, engine-seed-required-deferral, seed-loader-org-stamp): 'Test Files 5 passed (5) / Tests 24 passed (24)'. (3)vitest run --project local --maxWorkers=2: 'Test Files 370 passed (370) / Tests 7439 passed (7439)', at e5a2555 and also at 2a82645.--project repo: 'Tests 5 passed (5)' at 2a82645. (4)pnpm --filter @objectstack/objectql typecheckexit 0, with check:test-typecheck '40 file(s) / 234 error(s) / 65 pinned signature(s) held' (ledger unchanged);tsc -p tsconfig.test.json --listFilesOnlycounts the new test file. (5) Reverse verification. The fix was committed first. Through scripts/ablation-replace.mjs plus a shell trap, the stamp's seedReplay arm was reverted: anchor 1 to 0, reverted form 0 to 1, blob e34d4989074d to 4645b78e8872. Result: 'Tests 2 failed | 4 passed (6)', with only the two pin-1 cases red and pins 2-4 and the companion green; this was the predicted direction. Restore: blob equals the HEAD blob e34d4989074d, git diff HEAD is 0 bytes, git status --porcelain is empty. Observed identically at 2a82645 and at e5a2555. No dist build is needed per leg, because the pin imports ./plugin.js from src by relative path. (6) Three throwaway probe tests (created and deleted, never committed) measured the finding below.",
"mcp_calls": "0",
"api_writes": "3 REST writes, each onePOST /repos/objectstack-ai/objectstack/dispatchesto the fleet-write relay: (1) pr_create, which the relay executed as POST /repos/objectstack-ai/objectstack/pulls (draft) and which opened #21661, read back byte-identical at 12312 bytes; (2) label-write --assign os-project-manager, executed as the assignees write on #21661 and read back as matching; (3) this os-dev-report comment, executed as POST /repos//issues/21646/comments. git push is not a REST write. The branch was pushed 4 times: the empty probe, 2a82645, f135b5c, and the merge e5a2555. No force-push.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: public door — the seed loader on boot (SeedLoaderService.load through the kernel's ObjectQL, in-package rig, not a full os boot); wrong answer: a literal 'yesterday' on an author-declared readonly datetime is stored verbatim with no error, while the same literal on a non-readonly datetime is refused ('must be a valid datetime (ISO-8601)') and counted as a seed error · evidence: a system-context write skips value-shape validation for readonly fields. Since this PR, created_at on a seed INSERT is included; the replay UPDATE already stored it before this PR (measured: an unresolved cel envelope landed in created_at through engine.update under the seed context). A raw cel envelope also lands verbatim in a readonly field when a seeder skips resolveSeedRecord (AppPlugin's two fallback inserts, @objectstack/verify seed()) · dedupe words: readonly datetime not validated system write · seed malformed created_at stored · isSystem readonly value shape skipped · seed readonly field garbage",
"carrier: 承接者:无 · noted, not filed — two TSDocs still describe preserveAudit as 'symmetric with how created_at / created_by (already) behave on insert' (ExecutionContext.preserveAudit in packages/spec/src/kernel/execution-context.zod.ts; HookContext.session.preserveAudit in packages/spec/src/data/hook.zod.ts). That has been untrue since created_at and created_by stopped being client-preferred on an ordinary insert. This is older drift, recorded in the PR's Acceptance notes."
],
"gates": {
"head": "e5a2555da6",
"derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; change set = 3 paths vs merge base 6ec54f0)",
"derived": 64,
"ran": 64,
"exit_codes": "64 x exit 0, each captured before any pipe",
"reconcile": "dispatch-gates --ran: 64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED (a DERIVED zero — all 64 recorded an exit code and none of them is 3)",
"added_vs_dispatch_list": [
"node scripts/check-adr-0087-registration.mjs --base origin/main",
"node scripts/check-adr-0087-registration.mjs --self-test",
"node scripts/check-empty-changeset.mjs --base origin/main",
"node scripts/check-empty-changeset.mjs --self-test",
"node scripts/pm/release-rehearsal-clone.mjs --self-test",
"node scripts/release-pending-publish.mjs --self-test",
"pnpm check:engine-double-contract",
"pnpm check:objectql-double-limit",
"pnpm check:objectui-changeset",
"pnpm check:pm-changeset-deadline-census",
"pnpm check:query-options-erasure",
"pnpm check:type-check-coverage",
"pnpm check:type-check-debt",
"pnpm check:where-matcher"
],
"red_then_fixed": "check:objectql-double-limit was red on the first run (2a82645): the new stub driver's find was limit-blind. f135b5c applies ast.limit after the filter, and the gate is green from then on.",
"not_measured": "CI-only families the derivation lists outside its total: shard attestation, test completeness, the Test Core / Dogfood / Temporal Conformance / Build Core jobs, and the workspace type-check lanes. Reason: CI-only shell with no local invocation. Repository-wide pnpm lint is CI-owned and not run locally.",
"ci": "in_progress — not awaited (the PM owns the CI wait)"
},
"line_budget": "n/a",
"deviations": [
"Mechanism (partition 2 H1 falsified in one detail). seedReplay is not readable on hookCtx.session. buildSession copies userId, organizationId, positions, accessToken, isSystem, actor, skipTriggers, skipAutomations and preserveAudit, and nothing else. The stamp therefore reads hookCtx.input.options.context.seedReplay, the caller's own options bag under the HookContext input PHASE contract. Partition 3's one-branch route is otherwise kept. The other route is to propagate seedReplay through buildSession and declare HookContext.session.seedReplay in packages/spec. It was not taken, because it adds a contract key that the claim's Clause-2 line says is not touched, and this builtin would be its only reader. File surface: plugin.ts only, as the claim states.",
"origin/main advanced by one commit, eea82af (metadata-protocol view-container save door), after the pre-PR merge at 6ec54f0. It was not re-merged. No file overlaps this diff, and CI's merge ref covers it.",
"The first gate-union run (at f135b5c) was stopped by this run, by its own recorded PID, when main moved. One earlier union run overlapped a throwaway probe file in the worktree, so its readings were discarded. The reported union is a clean-tree run at e5a2555.",
"Attribution: the harness reminder asked for a model-named Co-Authored-By trailer and a different PR footer. Commits carry the model-free pair (Claude-Session plus Co-authored-by: Claude), and the PR body ends with the session-URL footer, per AGENTS.md and os-dev.md. The pre-push hook confirmed no model identifier is in the trailer pair.",
"Labels: none written. The dispatch named no label, and skip-changeset does not apply because the PR carries a patch changeset for a published package. The path labeler added documentation, size/m, tests and tooling on its own; they were left as they are.",
"Worktree removed after the PR opened (node_modules removed first, no --force)."
],
"files_changed": [
".changeset/21646-seed-created-at.md",
"packages/objectql/src/plugin.ts",
"packages/objectql/src/plugin-audit-seed-created-at.test.ts"
]
}objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsLanded: PR #21661 →
be55fd2934onmain(merged 2026-10-04T02:35Z through the merge queue, entered 2026-10-04T02:03Z), verified at 2026-10-04T02:36Z.domain:engine#1·session_017ErfyP2Rx7XWHJA27QjyUi.- The squash is on
origin/mainas a single-parent commit. Its diffstat is the reviewed one: 3 files, +388/-5. - The seed arm is on
main:isSeedReplayappears inpackages/objectql/src/plugin.ts. Fixes #21646closed this card ascompleted.pm:dispatchedis removed in this act. No other card was closed by the body.- A seed row's authored
created_atis now kept on a fresh boot and on replay alike, which unblocks hotcrm#1992's retirement of its duplicate date column. - Filed from this card: A system-context write skips value-shape validation for readonly fields: a seed's malformed readonly datetime (
'yesterday', an unresolvedcelenvelope) is stored verbatim, while the same value on a non-readonly field is refused #21663, a system write that skips readonly value-shape validation, for triage.
Generated by Claude Code
- The squash is on
- added 2 commits that reference this issue
on Oct 7, 2026
Filing gate: ① product defect with reach measured. Class (a). reach: public door, the seed loader on boot, read back through
GET /api/v1/data/crm_case. Measured on@objectstack/*17.6.0 by a dev run of therepo:hotcrmseat (session_01ER8ntXZhYebyQ66aXWdjfT) for hotcrm#1992.Who acts on it: objectstack triage routes it, likely to the data/objectql lane. ⛔ Not a claim; triage sets type and grade.
Measured (fresh boot of a scratch artifact built from hotcrm
94668373; noobjectstack.config.tsin the cwd, so objectstack#21501 does not apply)created_atauthoredcel\daysAgo(5)``2026-10-03T23:16:44.160Z(boot instant)2026-09-28T00:00:00.000Z(the authored value)'2026-09-01T12:00:00.000Z'2026-10-03T23:16:44.289Z(boot instant)2026-09-01T12:00:00.000Z(the authored value)2026-10-03T23:16:44.033ZThe explicit value is dropped silently on insert, with no refusal and no warning. It is written on the replay after a restart. A fresh database (a demo reset) therefore shows every seeded record created "now" until the next restart.
Code reading (17.6.0 dist)
objectqldist/index.js:21491, in the builtinbeforeInserthooksys_stamp_audit_insert(priority 10):record.created_at = preserveAudit ? record.created_at ?? now : now;specdist/kernel/index.js:29033:SEED_WRITE_EXECUTION_CONTEXT = { isSystem: true, skipTriggers: true, seedReplay: true }. It has nopreserveAudit.sys_stamp_audit_updatestamps onlyupdated_at, which explains why the replay writes the authoredcreated_at.defineSeed's shape (object,externalId,mode,env,locale,records) has no option for it.Why it matters
src/sales/data/_shared.ts). A report or dashboard that buckets by creation day shows all seeded rows on one day after a fresh boot.crm_case.created_datehas no writer for user-created records, so real cases drop out of reports.created_at. That ruling waits on this card (AGENTS.md §2: a platform defect means wait).Related, not duplicates
??so a plain REST caller cannot backdatecreated_at. That is the right call for callers.sys_notificationmigration back-dates throughpreserveAudit, the sanctioned path.This card asks for the seed path to take that same sanctioned path, consistently on insert and update, or for the platform to refuse an explicit
created_atin a seed loudly on both paths. Silently dropping it on one path and writing it on the other is the defect either way.Duplicate check
Objectstack issues: 5,308, being the earlier 5,268-issue walk since 2026-07-01 plus every issue updated on 2026-10-03, read to the short page. Title and body were grepped for
preserveAudit|sys_stamp_audit|seed…created_at|created_at…seed|backdat…(created_at|audit): 9 hits, all closed. #15964 and #16312 are the related pair above; the rest concern readonly-strip ordering and audit-binder prose. Positive control:SeedLoaderhit 17.Dedupe words: seed created_at overwritten · preserveAudit seed context · sys_stamp_audit_insert seed · backdated created_at seed
Generated by Claude Code