Skip to content

release: session-following location stamps, adoption fencing, app id alignment - #274

Merged
LeXwDeX merged 6 commits into
mainfrom
dev
Aug 15, 2026
Merged

release: session-following location stamps, adoption fencing, app id alignment#274
LeXwDeX merged 6 commits into
mainfrom
dev

Conversation

@LeXwDeX

@LeXwDeX LeXwDeX commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Summary

Promote to main for the next official release. Two trains merged on dev since v1.0.13 (#268):

  1. fix(dag): fix(dag): SessionMoved wake wedge — mixed directory stamps leave workflows with no directory owner #269 + fix(dag): vs-deletion adoption tail — adoption can slip past a workflow's own deletion within the revalidation window #270 — atomic adoption redesign (PR fix(dag): session-following location stamps + adoption-vs-deletion fencing (#269, #270) #272): the workflow execution-location stamp now moves WITH the session (SessionProjector re-stamps inside the same durable publish transaction; DagLoop evicts stale entries and the new owner re-forks drain — a wedged wake delivers immediately instead of never); adoption and spawn re-admission go through a single conditional claim fenced to non-terminal statuses, so a workflow's own deletion can no longer be slipped past. No-ALTER fail-closed convergence migration; R7 two-writer whitelist encoded as an invariant update. Red-first evidence: five mutation reversions (M1-M5), each going RED on its target probe, restores byte-identical; probe C4 flipped from pinning the wedge to pinning the resolution.
  2. fix(app): refactor(app): migrate packages/app/src/utils/id.ts off the legacy 0x1000-shifted 48-bit encoding #271 — app id encoder alignment (PR fix(app): migrate app id helper off the legacy 0x1000-shifted 48-bit encoding (#271) #273): packages/app now uses the shared schema identifier generator (raw 48-bit ms + monotonic latch); 0x1000 arithmetic eliminated from the app package; wrap-boundary/clock-regression suite mirrors schema; app-vs-core same-wall-clock decode pinned (drift 0-5ms).

Issues #269, #270, #271 close with these PRs — the entire v1.0.13 known-limitations ledger is burned down.

Dev validation

  • dev @ 994f0a3 full suite green: Typecheck, CodeQL, full unit + generated-freshness + HttpAPI contract (run 31858614820).
  • Both PRs passed dev gates (CLEAN) and independent verification: design gate ACCEPT → implement → mutation re-proof verify → standards+intent arbitration ACCEPT.

Known residual (documented, non-blocking)

  • Bounded re-stamp-vs-adoption race (tryClaimAdoption bumps time_updated on the wall clock) — no probe pins the window yet; carried for follow-up.
  • Directory-authority asymmetry: create stamps are realpath-normalized, Moved re-stamps are payload-raw; documented at the claim site.

Release plan

On merge: dispatch release-fork from main (official). Release notes carry the three fixed items (#269/#270/#271).

… encoding

Root cause: packages/app/src/utils/id.ts still encoded timestamp * 4096 +
counter into the 48-bit id prefix — the scheme that wrapped at epoch
1786706395136 (2026-08-14 19:19:55.136 +08) and was replaced everywhere else
by the shared fixed generator (PR #265). The app generates message and part
ids client-side, so every app-created id after the wrap carried a restarted
prefix: it no longer decoded back to wall-clock time and sorted against
core-generated ids as if it came from a different era.

Migration: generateID now delegates to the shared fixed generator in
@opencode-ai/schema/identifier (raw 48-bit ms behind a monotonic latch), so
app ids share one encoding with core ids. The Identifier namespace API is
unchanged — ascending/descending with the app prefix map and given-id
passthrough/validation. The legacy module had no timestamp() decode API with
live callers in app/desktop, so nothing needed re-implementing there.

Coverage: id.test.ts pins the app boundary — raw-ms decode of live ids,
prefix/shape for all six prefixes, app-vs-core same-wall-clock sort
consistency, burst and descending ordering, given-id passthrough and
validation; two probes were red against the legacy encoder (decoded prefix
189982101517 vs 1786752772485 wall-clock; app-vs-core drift ~1.6e12 ms) and
are green after. id-wrap-boundary.test.ts mirrors
packages/schema/test/identifier.test.ts through the app's generator seam.
Declared @opencode-ai/schema as an app workspace dependency.

Evidence: app unit 453/453, desktop 49/49, schema 16/16, typecheck green in
packages/app and packages/desktop, rg 0x1000 packages/app clean, root lint
4849 warnings (budget untouched), spot-check app vs core msg_ prefix drift
1 ms.

Refs #271
…onMoved (#269)

Root cause (#269): the execution-location key (WorkflowTable.directory) was
stamped at dag.create and never moved afterwards, while a SessionEvent.Moved
only rewrote SessionTable.directory. ownsSession conjuncts the directory over
ALL of a session's workflow rows, so after a move the rows disagree (the
pre-move workflows keep the old stamp) and the fail-closed ownership policy
leaves EVERY directory with no owner — the session's wakes wedge until restart.
Probe C4 pinned the wedge rather than fixing it (v1.0.13 decision).

Fix (design-gate Model a + b integration, atomic adoption):
- C1: the session projector's Moved projection now re-stamps every workflow row
  of the moved session to the payload-sourced destination directory, inside the
  SAME durable publish transaction as the SessionTable update — the stamp moves
  WITH the session in one transaction, so there is never a mixed-stamp window
  (no SessionTable read, no fs I/O in the projector).
- C5: DagLoop subscribes to SessionEvent.Moved. The instance whose directory no
  longer owns the moved session's workflows evicts its stale runtime entries
  (fail-closed), and the NEW owner re-forks the serialized wake drain so a wake
  that was wedged behind the old stamps delivers immediately (bounded time)
  rather than at the next idle event or restart.
- C6: no-ALTER idempotent convergence migration converges pre-fix divergent
  stamps to the session's current directory, preserving fail-closed NULL rows.

Probes (red-first; each was RED before the corresponding seam landed):
- C4 flips from pinning the wedge to pinning the resolution: exactly one live
  owner = the session's new durable directory, no cross-directory double
  adoption. RED evidence: wf1 stayed "/wtA" pre-C1.
- C8 pins the bounded-time resolution: a moved session's wedged wake reaches the
  new owner's admission seam immediately, delivered exactly once. RED evidence:
  the wake stayed wedged (poll timeout) pre-C5.
- C9 pins the migration convergence (fail-closed NULL preserved). RED evidence:
  a no-op migration leaves the stale stamp divergent.

Invariants preserved: fail-closed NULL, session-sourced create stamp, H1 latch,
R7 negative half. Only the write-once rule is narrowed to a two-writer whitelist
(create INSERT + Moved re-stamp) — encoded in the follow-up R7-ext(a) amendment.
Root cause (#270, shared territory with #269): admission was check-then-act —
an instance re-read the durable row (ownsWorkflow) and then published its
in-memory entry / materialized a child session. A deletion (Session.remove → FK
cascade) that commits between the passed ownership check and the action slipped
past it: the in-flight spawn could create a child for a workflow the cascade had
already removed. SpawnReady eviction was the mitigation, not a fence.

Fix (Model (b) conditional-claim fence of the accepted atomic-adoption design —
status+directory conditionality, no lease column):
- C2: DagStore.tryClaimAdoption — a single conditional UPDATE that matches the
  row only while it STILL EXISTS and is non-terminal, returning whether the
  admission claimed it. It converts the admission from check-then-act into
  act-with-condition; a committed deletion/terminal matches zero rows and the
  admission aborts atomically. Directory ownership is deliberately NOT re-done
  here: ownsWorkflow canonicalizes both sides and a SQL directory comparison
  would diverge (create stamps are realpathed, Moved re-stamps are not).
- C3: recoverWorkflow and the WorkflowStarted handler gate runtimes.set on the
  claim (after the ownsWorkflow directory revalidation), so a cascade in the
  final window aborts the adoption before it ever publishes an entry.
- C4: spawnNode re-admits atomically right before sessions.create — the fence
  that closes the spawn window nodeQueued's guard alone leaves open between its
  read and its publish. A committed deletion matches zero rows and the spawn
  aborts before any child session exists.

Probe C7 (red-first, park-gate style): parks the window-2 claim, commits a
deletion, releases, and asserts no post-deletion child survives.
RED evidence (fence conditionality reverted → always-claim): a child session is
created-then-cancelled for the deleted workflow, so `expect(cancelsA).toEqual([])`
fails (Received +3). Restoring the conditional claim makes the claim return false
on the deleted row and abort before sessions.create (GREEN).

All existing DAG-LOC-01 guards stay green (24 probes).
…ext(a) (#269)

Root cause (#269): the atomic-adoption redesign narrows the DAG-LOC-01
write-once directory invariant into a two-writer whitelist. R7-ext(a) previously
asserted "no UPDATE writes the directory column anywhere in the dag trees," which
held but left the new whitelisted writer (the Moved re-stamp) unpinned.

This is an invariant UPDATE, not a weakening — mandated by #269's own acceptance
("the directory stamp must move WITH the session in one transaction"):
- The ONLY sanctioned directory writers are the dag projector's WorkflowCreated
  INSERT (create-time stamp, onConflictDoNothing) and the session projector's
  SessionEvent.Moved re-stamp (payload-sourced, no SessionTable read).
- R7-ext(a) keeps its negative half (no .set({directory}) in the dag trees) and
  gains a positive whitelist half asserting the Moved re-stamp exists in the
  session projector (outside the dag trees, so R7/R7-ext stay scoped correctly),
  pinning the #269 resolution at the static-contract level.
- WorkflowTable.directory column comment documents the two-writer whitelist.

RED evidence (mutation reversion — Moved re-stamp removed from the session
projector): the whitelist expect fails, Expected true / Received false.

No user-visible semantic change; no runtime behavior altered in this commit.
fix(dag): session-following location stamps + adoption-vs-deletion fencing (#269, #270)
fix(app): migrate app id helper off the legacy 0x1000-shifted 48-bit encoding (#271)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant