os-sdlc: reload a timestamp column after stamping it with the DB clock #557
Labels
No labels
P0
P1
P2
P3
bug
category/code-change
category/cross-repo
category/eval-run
category/investigation
category/no-test-target
category/owner-ruling
category/prose-artifact
category/recurring-chore
category/structure
category/umbrella
crc-refactor
create
delete
enhancement
filed-by/agent
filed-by/user
frozen
lint-rule
needs-info
needs-triage
next
plugin/cc-architect
plugin/os
plugin/os-adr
plugin/os-aidd-lint
plugin/os-backlog
plugin/os-context
plugin/os-doc-hygiene
plugin/os-sdlc
plugin/os-vault
project/cc-os
ready-for-agent
ready-for-human
recurring
review
rule-proposal
update
waiting
wayfinder:grilling
wayfinder:map
wayfinder:map
wayfinder:research
wayfinder:task
wayfinder:task
wontfix
worklist/deviations
worklist/lint-rule
worklist/new-implement-build
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jared/cc-os#557
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
ADR-0165 stamps lifecycle columns with Sequel::CURRENT_TIMESTAMP. Sequel does not reload a row after update, so the in-memory row keeps the Sequel::SQL::Constant object instead of a time. Model#refresh reloads the row but the associations plugin clears the association cache first, which drops the in-memory ADR-0171 commit record. During #552 this crashed DispatchSummary.seconds_between with undefined method 'to_time' for an instance of Sequel::SQL::Constant, and the interim fix reads a fresh row in Implementation#summary. Verified against sequel 5.108.0 source; vault note user-guide/sequel-model-transaction-and-multi-db-gotchas.md section 3.
Observed
Implementation#complete!, fail!, escalate! and the dispatch stamping methods leave the constant in memory. Implementation#summary works around it with Summary.for(Model.for(db).implementation[id]).
22 sites, all one shape (
update(col: Sequel::CURRENT_TIMESTAMP, ...), no reload), under plugins/os-sdlc/lib/os_sdlc/model/:Reproduce
In a test, open an implementation, call complete!(position: "start"), then inspect completed_at on the same object: it is a Sequel::SQL::Constant.
Expected
One helper stamps a column with the DB clock and then reloads only that column (via this.get(column) assigned into values), keeping the association cache. All _at writes use it. Implementation#summary goes back to Summary.for(self). ADR-0165 stays intact; the DB clock is still the writer. Replacing the constant with Time.now was rejected because ADR-0165 chose the DB clock so every writing process shares one clock.
Owner sketch
One module under plugins/os-sdlc/lib/os_sdlc/model/, included once where model/db.rb builds each model class (not per-table). Stamp columns are positional; other attributes pass through unchanged; updated_at is always stamped.
Caller shape:
Class-level sites (implementation/class_methods.rb:20, violations/class_methods.rb:20) update a dataset, not an instance; they keep their dataset update and are out of scope for the reload.
Cases
complete!(position: "start")on an Implementation whosecommit_recordassociation was loaded,completed_aton the same object is aTime, and the association cache still holdscommit_record(no refetch).Implementation#summaryisSummary.for(self)and returns the same result as before the revert.Sequel::CURRENT_TIMESTAMPwrite in plugins/os-sdlc/lib/os_sdlc/model/ goes throughstamp!;grep -rn "Sequel::CURRENT_TIMESTAMP" plugins/os-sdlc/lib/os_sdlc/modelmatches only the helper and the two class-level dataset updates.Tasks
Origin
Session: 5fdd0aa3-8a9c-4c31-a5b3-d575d8e709ab
Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/5fdd0aa3-8a9c-4c31-a5b3-d575d8e709ab.jsonl
Where: plugins/os-sdlc/lib/os_sdlc/model/implementation/instance_methods.rb; plugins/os-sdlc/lib/os_sdlc/model/db.rb
Filed by: agent, from the user's question on 2026-09-11 about replacing CURRENT_TIMESTAMP with Time.now. Parent: #552.
Owner sketch and Cases added 2026-09-22 (session e8c75cf9-9b10-44bc-a4a2-7fc83305328b) after user-accepted triage.
Triage 2026-09-22: still valid, approach accepted
Finding. Every lifecycle writer leaves a
Sequel::CURRENT_TIMESTAMPconstant in memory. Plainupdatesends the constant to SQLite and never reads the row back, so the in-memory column holdsSequel::SQL::Constant, not aTime. A fullrefreshfixes the value but clears every cached association, which drops the ADR-0171 in-memory commit record. This crashed the summary during #552 (undefined method 'to_time' for Sequel::SQL::Constant).Sites (22, all one shape, none reload):
completed_atin memorycomplete!nilupdateSequel::SQL::Constant(:CURRENT_TIMESTAMP)Timefrom DB clock, associations intactAccepted solution. One module in
plugins/os-sdlc/lib/os_sdlc/model/. Stamp columns are positional; other attributes pass through unchanged.updated_atis always stamped. Reload usesthis.get(column), a plain SELECT that bypasses the associations plugin's refresh path.Caller shape:
Decisions (user-accepted 2026-09-22):
model/db.rbbuilds each model class. Not per-table modules.Implementation#summarytoSummary.for(self). Split only if the gate map reports a file with no mapped tests.commit_record, callcomplete!, assertcompleted_atis aTimeand the association cache still holdscommit_recordon the same object. No per-table tests.ADR-0165 stays intact: the DB clock remains the sole writer of these stamps.
Work started 2026-09-22 on branch ticket-557 via /os-sdlc:implement (poodr-ticket-implementation map).
Resolution
Done: TimestampStamping#stamp! on Model::Base stamps named lifecycle columns plus updated_at with Sequel::CURRENT_TIMESTAMP, then reloads only those columns via this.get so the association cache survives. All 20 instance-level CURRENT_TIMESTAMP writes across implementation, dispatch, agent, edge_spend, handoff, objection route through it. Implementation#summary is Summary.for(self) again. Two class-level dataset updates keep their shape by design.
Evidence: Commit
52072ad5on branch ticket-557. Files: plugins/os-sdlc/lib/os_sdlc/model/timestamp_stamping.rb (new), model/base.rb (include), 6 instance_methods.rb files, tests/model/implementation_complete_stamp_test.rb (cases 1-2 through the public complete! seam: completed_at is a Time and commit_record stays cached; summary equals Summary.for(self)). Pipeline implementation 10: 8 dispatches, 0 retries, all pass; suite gate exit 0 (121 s); grep confirms only the helper and two class-level sites still name CURRENT_TIMESTAMP. History row added to docs/implementation-status/os-sdlc.md.Follow-ups: none