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
beforeInsert overwrites a system-supplied last_update_at, so seeding a stalled task takes two passes instead of one #63
Found while establishing the seed-writes-history answer for #32. Filed unassigned for triage — the two-pass workaround is measured, documented in AGENTS.md and works today, so this is an ergonomics card, not a blocker for #7.
What happens
task.hook.ts's beforeInsert leg stamps unconditionally:
That is exactly what #3 specified and it is right for every ordinary create. But lifecycle hooks do run on the seed path — the platform's SeedLoaderService.SEED_OPTIONS is { isSystem: true, skipTriggers: true, seedReplay: true }, and skipTriggers suppresses record-change automation, not hooks — so a seed's last_update_at is overwritten with the boot clock even under a system context.
Measured on @objectstack/runtime 17.2.0: a system-context insert carrying last_update_at: <45 days ago> stores now. completed_at is unaffected (the hook does not touch it on insert), so the done half of history seeds in one write; only the stalled half needs the extra pass.
Current workaround (shipped, documented)
A second seed dataset on the same object in mode: 'update', matched on externalId, carrying only last_update_at. The beforeUpdate leg deliberately does not stamp on an administrative write, so the value lands. See the "How to write history" section of AGENTS.md and test/seed-history.test.ts.
It works, and it costs every seed / import / fixture author an extra pass plus the knowledge of why.
What could change
The hook can see the context. Measured on the same boot, a beforeInsert context carries:
So the insert leg could stamp only when the caller did not supply a value from a system context — roughly "a trusted writer replaying history may set it; everyone else gets now".
Not done here for two reasons, both worth weighing at triage:
It widens what a system caller may do to the stagnation signal, which is the one column the hook exists to protect. The current shape has the virtue that the ONLY way to move last_update_at backwards is an explicit, separate, visible administrative write. That may be the better design even though it costs a pass.
If the answer is "leave it", this issue should close as by-design with a line in the hook's header saying so, because the next seed author will ask the same question.
Acceptance
Either:
the beforeInsert leg honours a system-context-supplied last_update_at, test/seed-history.test.ts's "an INSERT cannot carry last_update_at" assertion is inverted, and AGENTS.md drops the second pass; or
the hook header records the deliberate refusal, and AGENTS.md keeps the two-pass shape as the permanent answer.
Correctly raised as triage rather than done as a drive-by. The answer is to keep the two passes.
The change would be one line: let beforeInsert honour a system-supplied last_update_at (the hook can see the context — you measured ctx.session = { isSystem: true }), collapsing the seed from two passes to one.
The cost is not local to seeding. It widens what every system caller may do to last_update_at, and the dispatcher is a system caller. A dispatcher that ever wrote that column — by intent or by a future refactor that copies a field list — would reset the stagnation clock on every nightly run. Silently. That is exactly the failure #3's guard was built to prevent, and it is the one with no error attached: the "Not moving" view simply goes quiet, and a quiet view reads as a healthy team.
So the trade is:
one extra pass for a seed author — rare, one place, and now documented in AGENTS.md with a worked example and an ablation proving the shape is real;
against an absolute guard on the production path — constant, everywhere, protecting the signal the product calls its most valuable.
Take the guard.
There is also something worth keeping about the current shape: the two-pass requirement exists because the beforeUpdate leg refuses to stamp on an administrative write. The mechanism that protects the signal in production is the same one that lets a seed set it. Collapsing the passes would remove that symmetry and leave the guard resting on nothing but the hook author's discipline.
Reopen if a real seed or import turns out to need the single pass badly enough to re-argue it.
Found while establishing the seed-writes-history answer for #32. Filed unassigned for triage — the two-pass workaround is measured, documented in
AGENTS.mdand works today, so this is an ergonomics card, not a blocker for #7.What happens
task.hook.ts'sbeforeInsertleg stamps unconditionally:That is exactly what #3 specified and it is right for every ordinary create. But lifecycle hooks do run on the seed path — the platform's
SeedLoaderService.SEED_OPTIONSis{ isSystem: true, skipTriggers: true, seedReplay: true }, andskipTriggerssuppresses record-change automation, not hooks — so a seed'slast_update_atis overwritten with the boot clock even under a system context.Measured on
@objectstack/runtime17.2.0: a system-context insert carryinglast_update_at: <45 days ago>storesnow.completed_atis unaffected (the hook does not touch it on insert), so thedonehalf of history seeds in one write; only the stalled half needs the extra pass.Current workaround (shipped, documented)
A second seed dataset on the same object in
mode: 'update', matched onexternalId, carrying onlylast_update_at. ThebeforeUpdateleg deliberately does not stamp on an administrative write, so the value lands. See the "How to write history" section ofAGENTS.mdandtest/seed-history.test.ts.It works, and it costs every seed / import / fixture author an extra pass plus the knowledge of why.
What could change
The hook can see the context. Measured on the same boot, a
beforeInsertcontext carries:So the insert leg could stamp only when the caller did not supply a value from a system context — roughly "a trusted writer replaying history may set it; everyone else gets
now".Not done here for two reasons, both worth weighing at triage:
src/hooks/was outside A task cannot be created directly indone— decide whether that is the intent #32's file surface, deliberately.last_update_atbackwards is an explicit, separate, visible administrative write. That may be the better design even though it costs a pass.If the answer is "leave it", this issue should close as by-design with a line in the hook's header saying so, because the next seed author will ask the same question.
Acceptance
Either:
beforeInsertleg honours a system-context-suppliedlast_update_at,test/seed-history.test.ts's "an INSERT cannot carry last_update_at" assertion is inverted, andAGENTS.mddrops the second pass; orAGENTS.mdkeeps the two-pass shape as the permanent answer.