Uh oh!
There was an error while loading. Please reload this page.
Establish and pin how a seed writes history - #64
Merged
Conversation
A duly_task cannot be created in `done` by an ordinary caller: completed_at is readonly, beforeInsert stamps only last_update_at, so completed_at_required_when_done refuses the row. Correct for the dispatch path, fatal for the seed path that #7 needs. Measured on @objectstack/runtime 17.2.0: `{ context: { isSystem: true } }` exempts the readonly strip and IS the sanctioned way to write history — but it takes two passes. completed_at rides along on the insert. last_update_at cannot: beforeInsert stamps it unconditionally and lifecycle hooks still run on the seed path (skipTriggers suppresses record-change automation, not hooks), so it takes a second seed dataset in `mode: 'update'`. Without that second pass there are no stalled rows and the "Not moving" view is empty on a seeded demo. test/seed-history.test.ts pins both directions against a real booted kernel with the declarative seeder actually running: the system write succeeds, and an ordinary caller's identical write is still refused by completed_at_required_when_done (VALIDATION_FAILED). It also pins that the insert-path readonly strip is a protocol-BOUNDARY guard — engine.insert applies none of it — filed upstream as objectstack-ai/objectstack#14147. AGENTS.md carries the answer next to the product invariants, including that the seed loader resolves duly_task.owner as a natural key against sys_user.name, so a seed must seed its users first or every task row is refused. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
os-warren
marked this pull request as ready for review
September 1, 2026 08:15
Uh oh!
There was an error while loading. Please reload this page.
os-warren added a commit
that referenced
this pull request
Sep 1, 2026
`pnpm dev` on an empty database now opens on a running system rather than five empty grids — a three-level business-unit tree, thirteen people, a twenty-item role catalog across three position codes, thirty-one duties and six months of dispatched history. History is produced by the dispatcher's own planner (`planDispatch`) rather than by a second period walk, so every period key is the engine's spelling by construction and "standing duties hold zero tasks" is structurally impossible to violate rather than merely absent from the fixture. `last_update_at` is written by a second `mode: 'update'` seed pass, per #32 / PR #64 — an insert can never carry it, and without that pass the "Not moving" view is empty while the seed reports success. Fixes#7 Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os-warren added a commit
that referenced
this pull request
Sep 1, 2026
* Seed the demo: the product working on first boot `pnpm dev` on an empty database now opens on a running system rather than five empty grids — a three-level business-unit tree, thirteen people, a twenty-item role catalog across three position codes, thirty-one duties and six months of dispatched history. History is produced by the dispatcher's own planner (`planDispatch`) rather than by a second period walk, so every period key is the engine's spelling by construction and "standing duties hold zero tasks" is structurally impossible to violate rather than merely absent from the fixture. `last_update_at` is written by a second `mode: 'update'` seed pass, per #32 / PR #64 — an insert can never carry it, and without that pass the "Not moving" view is empty while the seed reports success. Fixes#7 Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Spread in-flight touch ages by how long a task has been open Keeps every non-designated row inside the fortnight while putting real values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d tiles read 6 / 3 / 2 rather than 3 / 3 / 2. Also corrects the fan-out comment after #72: the reason a seeded assignment does not fan out is the loader's own skipTriggers, not an unbound trigger. Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#32
The answer is yes, with a second pass nobody would have guessed. No application-level workaround was needed and none was written.
The answer
{ context: { isSystem: true } }is the sanctioned way to write history — the same legsrc/jobs/dispatch.job.tsalready uses, and the leg the platform's own seed loader uses (SeedLoaderService.SEED_OPTIONS = { isSystem: true, skipTriggers: true, seedReplay: true }). It exempts a write from the readonly strip.completed_atlast_update_atmode: 'update'The stalled half is the one that would have bitten #7.
beforeInsertstampslast_update_atunconditionally, and lifecycle hooks do run on the seed path —skipTriggerssuppresses record-change automation, not hooks — so a system insert's value is overwritten with the boot clock. Measured on a real seeded boot: the row seeded withlast_update_at: <45 days ago>came back holding boot time. It takes a second seed dataset on the same object inmode: 'update', matched onexternalId, carrying onlylast_update_at; thebeforeUpdateleg deliberately does not stamp on an administrative write, so that value lands.Skip that pass and the "Not moving" view is empty on a freshly seeded demo, with no error anywhere — the seed reports success and the view is simply blank.
A third finding #7 needs
The seed loader resolves
duly_task.owneras a natural key againstsys_user.name, deferred to a second resolution pass. A bare id string matching nosys_userrow does not resolve, and becauseownerisrequired: truethe whole task row is refused (Owner is required, plus an unresolved-reference error). Measured: without asys_userdataset seeded first, nothing seeds at all —inserted: 0, errored: 4.Both directions are pinned
test/seed-history.test.tsruns against a real booted kernel with the declarative seeder actually running (skipSeedData: false), so the seed path itself is under test rather than a hand-made stand-in.donetask with a completion instant → succeeds, nothing strippedcompleted_at_required_when_done(asserted oncode: VALIDATION_FAILED+name: ValidationError+ the message, not on a baretoThrow), and nothing is writtenisSystem: falsespelled out, so the exemption is pinned to the flag's value and not to the key being absentlast_update_atbackdate → dropped, withdroppedFieldsasserted as well as the unchanged stored valueAblations
Both mutations were confirmed on disk before running (grep for the injected and the deleted text), and both were restored by an
EXIT INT TERMtrap.readonly: truefromcompleted_at(src/objects/task.object.ts)mode: 'update'seed pass from the fixtureexpected '2026-09-01T08:09:31.664Z' to be '2026-07-18T08:09:31.478Z'The second one is the important one: it proves the two-pass shape this PR documents is load-bearing rather than decoration.
Gates
All four green at
4f42406, which is the final commit:pnpm testwas re-run afterpnpm buildwithdist/objectstack.jsonon disk — still 428/428 — confirming the suite'sartifactPathguard holds and the tests report onsrc/, not on the last build.Files changed
test/seed-history.test.ts(new)AGENTS.md— the answer, next to the product invariantssrc/data/was deliberately not touched. The worked example lives in the test fixture and inAGENTS.mdrather than indulySeeds, because rows added there would boot on everypnpm devand collide head-on with #7, which owns the real seed. This keeps the file surface totest/+AGENTS.md. No breach of the declared surface.Filed out of scope
readonlystrip is a protocol-boundary guard only —engine.insertapplies none of it, andcreate_record'sonFieldsDroppedchannel can never fire for a readonly drop objectstack#14147 — the insert-pathreadonlystrip is a protocol-boundary guard only.engine.insertapplies none of it (the update-path strip is in the engine and isisSystem-gated), andcreate_recordwires anonFieldsDroppedchannel that can never fire for a readonly drop on that path. Pinned here as a tripwire written to go red and be deleted when the platform closes it. Not currently reachable in this app —assignment.flow.tsdeclaresrunAs: 'system'.beforeInsertoverwrites a system-suppliedlast_update_at, so seeding a stalled task takes two passes instead of one #63 —beforeInsertoverwrites a system-suppliedlast_update_at, which is what forces the second pass. The hook can see the context (ctx.session = { isSystem: true }, measured), so a one-line change would collapse it to one pass — but it widens what a system caller may do to the stagnation signal, so it is a triage decision, not a drive-by.src/hooks/was outside this card's surface.Generated by Claude Code
Generated by Claude Code