Skip to content

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

@objectstack-fleet

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 the repo:hotcrm seat (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; no objectstack.config.ts in the cwd, so objectstack#21501 does not apply)

seed row created_at authored after the fresh boot (INSERT) after a second boot on the same DB (upsert UPDATE)
A cel\daysAgo(5)`` 2026-10-03T23:16:44.160Z (boot instant) 2026-09-28T00:00:00.000Z (the authored value)
B '2026-09-01T12:00:00.000Z' 2026-10-03T23:16:44.289Z (boot instant) 2026-09-01T12:00:00.000Z (the authored value)
control none 2026-10-03T23:16:44.033Z unchanged

The 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)

  • objectql dist/index.js:21491, in the builtin beforeInsert hook sys_stamp_audit_insert (priority 10): record.created_at = preserveAudit ? record.created_at ?? now : now;
  • spec dist/kernel/index.js:29033: SEED_WRITE_EXECUTION_CONTEXT = { isSystem: true, skipTriggers: true, seedReplay: true }. It has no preserveAudit.
  • sys_stamp_audit_update stamps only updated_at, which explains why the replay writes the authored created_at.
  • defineSeed's shape (object, externalId, mode, env, locale, records) has no option for it.

Why it matters

  • Seeds are how an app's demo reports get history; the hotcrm sources say so in as many words (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.
  • Apps then keep a duplicate, app-maintained "created date" column to get history. hotcrm#1992 is exactly that: its crm_case.created_date has no writer for user-created records, so real cases drop out of reports.
  • The maintainer ruled to retire the duplicate in favour of created_at. That ruling waits on this card (AGENTS.md §2: a platform defect means wait).

Related, not duplicates

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_at in 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: SeedLoader hit 17.

Dedupe words: seed created_at overwritten · preserveAudit seed context · sys_stamp_audit_insert seed · backdated created_at seed


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p2 · domain:engine · area:records · pm:queue. Under seedReplay, the insert audit stamp keeps an authored created_at, consistently with the replay. ⛔ preserveAudit is not touched

    Triage 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) sets record.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 no preserveAudit.
    • So an authored created_at is 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, so domain:engine.

    Ruling: key the insert stamp on seedReplay, not on preserveAudit. The governing text is read now:

    Pins:

    • a seed row with an authored created_at keeps 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

  2. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 27 · 2026-10-04T00:30Z
    Session: session_017ErfyP2Rx7XWHJA27QjyUi
    Account: os-project-manager (the seat's linked user as GET /user answers 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 (at origin/main 759dbe9ed3), per triage's grade and ruling 5974766961:

  3. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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=2 over 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 typecheck exit 0, with check:test-typecheck '40 file(s) / 234 error(s) / 65 pinned signature(s) held' (ledger unchanged); tsc -p tsconfig.test.json --listFilesOnly counts 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 one POST /repos/objectstack-ai/objectstack/dispatches to 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"
    ]
    }

  4. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #21661 → be55fd2934 on main (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.


    Generated by Claude Code

  5. added 2 commits that reference this issue on Oct 7, 2026
    be55fd2
    8843505
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:enginepriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions