@astrosheep/keiyaku 2.9.11 → 2.9.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +79 -85
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/call-terms.js +1 -0
  4. package/build/agents/providers/claude-agent-sdk/adapter.js +17 -4
  5. package/build/cli/commands/akuma/akuma/handler.js +2 -1
  6. package/build/cli/commands/akuma/akuma/meta.js +4 -3
  7. package/build/cli/commands/akuma/catalog.js +2 -0
  8. package/build/cli/commands/akuma/list/handler.js +1 -1
  9. package/build/cli/commands/akuma/list/meta-list.js +12 -0
  10. package/build/cli/commands/akuma/list/meta-ls.js +2 -2
  11. package/build/cli/commands/akuma.js +5 -0
  12. package/build/cli/commands/contract/amend/meta.js +4 -3
  13. package/build/cli/commands/contract/arc/meta.js +2 -0
  14. package/build/cli/commands/contract/audit/handler.js +28 -2
  15. package/build/cli/commands/contract/audit/meta.js +4 -3
  16. package/build/cli/commands/contract/bind/meta.js +4 -4
  17. package/build/cli/commands/contract/petition/handler.js +2 -1
  18. package/build/cli/commands/contract/petition/meta.js +2 -2
  19. package/build/cli/commands/contract/renew/handler.js +44 -3
  20. package/build/cli/commands/contract/renew/meta.js +5 -4
  21. package/build/cli/commands/metadata.js +14 -10
  22. package/build/cli/commands/projection/call/handler.js +1 -1
  23. package/build/cli/commands/projection/call/meta.js +1 -1
  24. package/build/cli/commands/projection/catalog.js +2 -2
  25. package/build/cli/commands/projection/{revive → fork}/handler.js +10 -5
  26. package/build/cli/commands/projection/fork/meta.js +12 -0
  27. package/build/cli/commands/projection/history/handler.js +46 -2
  28. package/build/cli/commands/projection/history/meta.js +3 -3
  29. package/build/cli/commands/projection/kill/handler.js +4 -5
  30. package/build/cli/commands/projection/kill/meta.js +2 -2
  31. package/build/cli/commands/projection/status/handler.js +29 -13
  32. package/build/cli/commands/projection/status/meta.js +14 -3
  33. package/build/cli/commands/projection/tell/handler.js +8 -4
  34. package/build/cli/commands/projection/tell/meta.js +1 -1
  35. package/build/cli/commands/projection/wait/handler.js +45 -20
  36. package/build/cli/commands/projection/wait/meta.js +1 -1
  37. package/build/cli/commands/shared.js +2 -2
  38. package/build/cli/commands/system/catalog.js +2 -0
  39. package/build/cli/commands/task/add/handler.js +28 -12
  40. package/build/cli/commands/task/add/meta.js +18 -6
  41. package/build/cli/commands/task/catalog.js +4 -0
  42. package/build/cli/commands/task/compose/handler.js +18 -0
  43. package/build/cli/commands/task/compose/meta.js +10 -0
  44. package/build/cli/commands/task/doctor/handler.js +2 -3
  45. package/build/cli/commands/task/doctor/meta.js +2 -2
  46. package/build/cli/commands/task/done/meta.js +2 -2
  47. package/build/cli/commands/task/drop/handler.js +7 -2
  48. package/build/cli/commands/task/drop/meta.js +6 -3
  49. package/build/cli/commands/task/hold/meta.js +2 -2
  50. package/build/cli/commands/task/log/meta.js +2 -2
  51. package/build/cli/commands/task/ls/meta.js +2 -2
  52. package/build/cli/commands/task/note/handler.js +6 -0
  53. package/build/cli/commands/task/note/meta.js +11 -0
  54. package/build/cli/commands/task/resume/meta.js +2 -2
  55. package/build/cli/commands/task/shared.js +29 -15
  56. package/build/cli/commands/task/show/meta.js +2 -2
  57. package/build/cli/commands/task/start/meta.js +2 -2
  58. package/build/cli/commands/task/stop/meta.js +2 -2
  59. package/build/cli/commands/task/update/meta.js +3 -2
  60. package/build/cli/commands/verification/handler.js +43 -0
  61. package/build/cli/commands/verification/meta.js +13 -0
  62. package/build/cli/completion.js +16 -13
  63. package/build/cli/flags.js +48 -12
  64. package/build/cli/help.js +29 -30
  65. package/build/cli/index.js +14 -8
  66. package/build/cli/parse-flags.js +60 -21
  67. package/build/cli/parse-metadata.js +10 -4
  68. package/build/cli/parse-selectors.js +22 -4
  69. package/build/cli/parse.js +43 -30
  70. package/build/cli/projection-address.js +7 -7
  71. package/build/cli/render/arc.js +12 -3
  72. package/build/cli/render/audit.js +2 -2
  73. package/build/cli/render/call.js +4 -4
  74. package/build/cli/render/kanshi.js +122 -85
  75. package/build/cli/render/misc.js +2 -2
  76. package/build/cli/render/petition.js +16 -1
  77. package/build/cli/render/projection-history.js +17 -4
  78. package/build/cli/render/shared.js +5 -5
  79. package/build/cli/render/status-indicator.js +47 -0
  80. package/build/cli/render/status.js +106 -76
  81. package/build/cli/render/success-response.js +20 -5
  82. package/build/cli/render/task.js +33 -5
  83. package/build/cli/render/tell.js +10 -4
  84. package/build/cli/render/tool-command-normalization.js +137 -0
  85. package/build/cli/render/tool-presentation.js +2 -1
  86. package/build/cli/render/verification.js +23 -0
  87. package/build/cli/render/wait.js +101 -33
  88. package/build/cli/types.js +2 -1
  89. package/build/config/akuma-loader.js +21 -1
  90. package/build/config/settings/knobs.js +7 -1
  91. package/build/config/settings/schema.js +10 -5
  92. package/build/core/addressing.js +3 -3
  93. package/build/core/amend.js +102 -17
  94. package/build/core/arc.js +4 -4
  95. package/build/core/audit/candidate.js +10 -18
  96. package/build/core/audit/coordinates.js +12 -17
  97. package/build/core/audit/evidence.js +6 -4
  98. package/build/core/audit/facade.js +3 -3
  99. package/build/core/audit/report.js +1 -1
  100. package/build/core/bind-reconciliation.js +42 -23
  101. package/build/core/bind.js +182 -81
  102. package/build/core/call/call.js +6 -7
  103. package/build/core/call/context.js +46 -33
  104. package/build/core/call/execution.js +21 -6
  105. package/build/core/call/prompt.js +11 -42
  106. package/build/core/contract-carrier-runtime.js +209 -105
  107. package/build/core/contract-carrier.js +156 -73
  108. package/build/core/contract-view.js +43 -6
  109. package/build/core/contract.js +2 -1
  110. package/build/core/derived-replay.js +168 -35
  111. package/build/core/draft.js +60 -27
  112. package/build/core/forfeit.js +1 -1
  113. package/build/core/lifecycle-history.js +13 -0
  114. package/build/core/lifecycle-recovery.js +32 -27
  115. package/build/core/lifecycle-runner.js +14 -14
  116. package/build/core/log.js +1 -1
  117. package/build/core/outcome-base.js +2 -2
  118. package/build/core/projection/akuma-name.js +43 -0
  119. package/build/core/projection/generation/database.js +141 -79
  120. package/build/core/projection/generation/ledger.js +91 -0
  121. package/build/core/projection/generation/model.js +19 -14
  122. package/build/core/projection/generation/projection-generation-continuation.js +1 -1
  123. package/build/core/projection/generation/projection-generation-doorbell.js +88 -0
  124. package/build/core/projection/generation/projection-generation-execution.js +24 -0
  125. package/build/core/projection/generation/projection-generation-identity.js +4 -0
  126. package/build/core/projection/generation/projection-generation-launcher.js +72 -138
  127. package/build/core/projection/generation/projection-generation-process.js +16 -15
  128. package/build/core/projection/generation/projection-generation-runner.js +132 -76
  129. package/build/core/projection/generation/projection-generation-runtime.js +10 -61
  130. package/build/core/projection/generation/protocol.js +68 -0
  131. package/build/core/projection/generation/store.js +17 -7
  132. package/build/core/projection/generation/transitions.js +24 -14
  133. package/build/core/projection/index.js +5 -5
  134. package/build/core/projection/projection-core.js +6 -2
  135. package/build/core/projection/projection-execution-observer.js +1 -1
  136. package/build/core/projection/projection-history.js +8 -8
  137. package/build/core/projection/projection-kill.js +20 -10
  138. package/build/core/projection/projection-life-observer.js +8 -8
  139. package/build/core/projection/projection-mint.js +6 -8
  140. package/build/core/projection/projection-status-observation.js +77 -8
  141. package/build/core/projection/projection-status.js +151 -4
  142. package/build/core/projection/projection-wait.js +48 -29
  143. package/build/core/projection/projection-wake.js +28 -88
  144. package/build/core/projection/tell/launch-store.js +1 -1
  145. package/build/core/projection/tell/store.js +1 -1
  146. package/build/core/renew-build.js +204 -74
  147. package/build/core/renew-plan.js +34 -9
  148. package/build/core/renew-rewrite.js +56 -15
  149. package/build/core/renew.js +39 -35
  150. package/build/core/repository-ledger/accepted-fold-read.js +6 -3
  151. package/build/core/repository-ledger/claim-evidence.js +1 -1
  152. package/build/core/repository-ledger/codec.js +11 -5
  153. package/build/core/repository-ledger/current-state-store.js +464 -349
  154. package/build/core/repository-ledger/fold-repository.js +5 -6
  155. package/build/core/repository-ledger/identity.js +60 -0
  156. package/build/core/repository-ledger/inventory.js +3 -3
  157. package/build/core/repository-ledger/read-model.js +57 -9
  158. package/build/core/repository-ledger/write-transaction.js +24 -17
  159. package/build/core/run-control/connection-bound-close.js +41 -0
  160. package/build/core/run-control/detached-lease-bootstrap.js +208 -0
  161. package/build/core/run-control/detached-process.js +14 -0
  162. package/build/core/run-control/index.js +6 -0
  163. package/build/core/{process-group.js → run-control/process-tree.js} +1 -1
  164. package/build/core/run-control/runner-lease.js +172 -0
  165. package/build/core/run-control/sqlite-process-lifetime-lock.js +174 -0
  166. package/build/core/seal.js +44 -32
  167. package/build/core/settlement/claim-delivery.js +22 -9
  168. package/build/core/settlement/claim.js +8 -6
  169. package/build/core/settlement/petition-claim-gates.js +9 -8
  170. package/build/core/settlement/petition-head-guard.js +5 -6
  171. package/build/core/settlement/petition-preview.js +21 -5
  172. package/build/core/settlement/petition.js +51 -17
  173. package/build/core/settlement/settlement.js +20 -15
  174. package/build/core/settlement/verdict.js +2 -2
  175. package/build/core/settlement/verification.js +492 -364
  176. package/build/core/status/board.js +184 -75
  177. package/build/core/status/drift.js +8 -10
  178. package/build/core/task/board.js +1 -0
  179. package/build/core/task/commands.js +25 -0
  180. package/build/core/task/compose.js +328 -0
  181. package/build/core/task/document.js +102 -39
  182. package/build/core/task/index.js +4 -3
  183. package/build/core/task/settlement-git.js +70 -30
  184. package/build/core/task/settlement-policy.js +27 -14
  185. package/build/core/task/source-board.js +1 -0
  186. package/build/core/task/task-bind-preparation.js +59 -9
  187. package/build/core/task/task-contract.js +2 -2
  188. package/build/core/task/task-git-store.js +1 -0
  189. package/build/core/task/task-store-repository.js +2 -0
  190. package/build/core/task/task.js +2 -2
  191. package/build/core/transcripts.js +4 -4
  192. package/build/core/verification-declaration.js +95 -0
  193. package/build/core/worktree-bootstrap.js +1 -1
  194. package/build/core/worktree-path.js +7 -11
  195. package/build/flow-error.js +15 -2
  196. package/build/generated/version.js +2 -2
  197. package/build/git/branches.js +41 -70
  198. package/build/git/commits.js +92 -21
  199. package/build/git/core.js +9 -161
  200. package/build/git/diff/preview.js +4 -4
  201. package/build/git/diff/read.js +4 -4
  202. package/build/git/diff/structured.js +5 -5
  203. package/build/git/process.js +229 -0
  204. package/build/git/refs.js +30 -5
  205. package/build/git/staging.js +39 -115
  206. package/build/git/streaming-batch.js +9 -16
  207. package/build/git/worktree.js +104 -91
  208. package/build/index.js +7 -0
  209. package/package.json +2 -2
  210. package/skills/keiyaku-akuma/SKILL.md +27 -21
  211. package/skills/keiyaku-task/SKILL.md +61 -5
  212. package/skills/keiyaku-workflow/SKILL.md +81 -26
  213. package/build/cli/commands/projection/revive/meta.js +0 -12
  214. package/build/core/projection/projection-life-protocol.js +0 -115
  215. package/build/core/projection/projection-runner-lock.js +0 -382
  216. package/build/core/projection/tell/database.js +0 -127
  217. package/build/core/settlement/verification-coordination.js +0 -305
  218. package/build/core/settlement/verification-supervisor.js +0 -275
@@ -1,9 +1,11 @@
1
1
  import { openProjectionGenerationStore, } from "./store.js";
2
- import { acquireProjectionRunnerLock, observeProjectionRunnerLock, } from "../projection-runner-lock.js";
3
- import { createProjectionExecutionId } from "./projection-generation-identity.js";
2
+ import { acquireRunnerLease, observeRunnerLease, } from "../../run-control/runner-lease.js";
3
+ import { beginConnectionBoundRunnerClose } from "../../run-control/connection-bound-close.js";
4
+ import { createProjectionExecutionId, projectionRunnerLeasePath } from "./projection-generation-identity.js";
4
5
  import { applyContinuationEffort, } from "./projection-generation-continuation.js";
5
- import { openProjectionTellObserver } from "../tell/database.js";
6
+ import { openProjectionTellObserver } from "./database.js";
6
7
  import { OUTCOME_ERROR_VERSION } from "../../../agents/types.js";
8
+ import { openProjectionGenerationDoorbell, } from "./projection-generation-doorbell.js";
7
9
  /** Private runner-side kill grace. Not a setting, env, parameter, or export. */
8
10
  const KILL_GRACE_MS = 5000;
9
11
  function successorLaunchFacts(launch, completionFacts, effort, now) {
@@ -51,10 +53,10 @@ function readOutcomeState(state) {
51
53
  }
52
54
  function acquireRunnerLockOrObserveDuplicate(projectionDirectory) {
53
55
  try {
54
- return acquireProjectionRunnerLock(projectionDirectory);
56
+ return acquireRunnerLease(projectionRunnerLeasePath(projectionDirectory));
55
57
  }
56
58
  catch (error) {
57
- if (observeProjectionRunnerLock(projectionDirectory).state === "held") {
59
+ if (observeRunnerLease(projectionRunnerLeasePath(projectionDirectory)).state === "held") {
58
60
  return undefined;
59
61
  }
60
62
  throw error;
@@ -75,9 +77,38 @@ export async function runProjectionGeneration(input) {
75
77
  let adopted = false;
76
78
  let settled = false;
77
79
  let tellObserver;
80
+ let doorbell;
78
81
  let graceTimer;
79
- let intentTimer;
80
82
  let handle;
83
+ let gracefulAbortInvoked = false;
84
+ let forceAbortInvoked = false;
85
+ let sawKillIntent = false;
86
+ let sawInterruptIntent = false;
87
+ let wakeAfterLockRelease = false;
88
+ let pullStopIntent;
89
+ let notifyMainWait;
90
+ const commitTerminal = (stage) => {
91
+ if (!runnerLock)
92
+ throw new Error("runner lease is not held");
93
+ const close = beginConnectionBoundRunnerClose(store.connectionForRunnerClose(), runnerLock);
94
+ try {
95
+ const transition = stage();
96
+ if (transition.status !== "committed") {
97
+ close.rollback();
98
+ return transition;
99
+ }
100
+ close.commit();
101
+ runnerLock = undefined;
102
+ return transition;
103
+ }
104
+ catch (error) {
105
+ if (close.leaseOwnership === "retained")
106
+ close.rollback();
107
+ else
108
+ runnerLock = undefined;
109
+ throw error;
110
+ }
111
+ };
81
112
  const clearGraceTimer = () => {
82
113
  if (graceTimer === undefined)
83
114
  return;
@@ -89,15 +120,50 @@ export async function runProjectionGeneration(input) {
89
120
  ?? acquireRunnerLockOrObserveDuplicate(input.projectionDirectory);
90
121
  if (!runnerLock)
91
122
  return { status: "duplicate" };
92
- const initial = store.readCurrentGeneration();
123
+ tellObserver = openProjectionTellObserver(input.projectionDirectory);
124
+ // Establish the version fence before the listener becomes reachable. The
125
+ // forced pull below still sees intent committed before listener startup.
126
+ tellObserver.hasDataVersionChanged();
127
+ pullStopIntent = (force = false) => {
128
+ try {
129
+ if (!force && !tellObserver.hasDataVersionChanged())
130
+ return undefined;
131
+ const live = store.readCurrentGeneration();
132
+ if (live?.launch.executionId !== input.executionId || live?.verdict)
133
+ return live;
134
+ sawKillIntent = live.killIntent !== undefined;
135
+ sawInterruptIntent = live.interruptIntent !== undefined;
136
+ if (!sawKillIntent && !sawInterruptIntent)
137
+ return live;
138
+ if (gracefulAbortInvoked || !handle)
139
+ return live;
140
+ gracefulAbortInvoked = true;
141
+ void handle.abort("graceful").catch(() => {
142
+ // A failed native stop does not alter durable intent truth.
143
+ });
144
+ return live;
145
+ }
146
+ catch {
147
+ // Observation failure is unknown, never evidence about the provider.
148
+ return undefined;
149
+ }
150
+ };
151
+ doorbell = await openProjectionGenerationDoorbell({
152
+ identity: store.readIdentity(),
153
+ executionId: input.executionId,
154
+ onRing: () => {
155
+ pullStopIntent?.(true);
156
+ notifyMainWait?.();
157
+ },
158
+ });
159
+ // This is the one baseline heart pull after listener registration. It
160
+ // covers intent committed while the listener did not yet exist.
161
+ const initial = pullStopIntent(true);
93
162
  if (initial?.launch.executionId !== input.executionId
94
163
  || initial.adoption
95
164
  || initial.verdict) {
96
165
  return { status: "rejected" };
97
166
  }
98
- tellObserver = openProjectionTellObserver(input.projectionDirectory);
99
- // Prime before adoption so a concurrent stop write cannot become the baseline.
100
- tellObserver.hasDataVersionChanged();
101
167
  const adoption = store.adoptionIfCurrent({
102
168
  executionId: input.executionId,
103
169
  facts: { adoptedAt: new Date(now()).toISOString() },
@@ -112,7 +178,10 @@ export async function runProjectionGeneration(input) {
112
178
  // Adoption is the provider-admission boundary. A kill that committed first
113
179
  // settles without constructing a provider execution.
114
180
  if (current.killIntent) {
115
- const killed = store.runnerKilledIfRequested({ executionId: input.executionId });
181
+ const killed = commitTerminal(() => store.runnerKilledIfRequested({
182
+ executionId: input.executionId,
183
+ terminalAt: new Date(now()).toISOString(),
184
+ }));
116
185
  if (killed.status !== "committed")
117
186
  return { status: "rejected" };
118
187
  settled = true;
@@ -126,7 +195,10 @@ export async function runProjectionGeneration(input) {
126
195
  }
127
196
  if (current.killIntent) {
128
197
  await prepared.dispose();
129
- const killed = store.runnerKilledIfRequested({ executionId: input.executionId });
198
+ const killed = commitTerminal(() => store.runnerKilledIfRequested({
199
+ executionId: input.executionId,
200
+ terminalAt: new Date(now()).toISOString(),
201
+ }));
130
202
  if (killed.status !== "committed")
131
203
  return { status: "rejected" };
132
204
  settled = true;
@@ -134,68 +206,35 @@ export async function runProjectionGeneration(input) {
134
206
  }
135
207
  handle = prepared.start();
136
208
  const settleInterrupted = (facts = {}) => {
137
- const transition = store.runnerInterruptedIfRequested({
209
+ pullStopIntent?.(true);
210
+ const transition = commitTerminal(() => store.runnerInterruptedIfRequested({
138
211
  executionId: input.executionId,
139
212
  facts,
140
213
  terminalAt: new Date(now()).toISOString(),
141
- });
214
+ }));
142
215
  if (transition.status !== "committed")
143
216
  return { status: "rejected" };
144
217
  settled = true;
145
- runnerLock.close();
146
- runnerLock = undefined;
147
- void input.wakeAfterInterrupt?.().catch(() => {
148
- // The ordinary wake path persists its typed failure. A later tell can retry.
149
- });
218
+ wakeAfterLockRelease = true;
150
219
  return { status: "completed" };
151
220
  };
152
221
  const settleRunnerOutcome = (execution) => {
153
- const transition = store.runnerVerdictIfOpen({
222
+ pullStopIntent?.(true);
223
+ const transition = commitTerminal(() => store.runnerVerdictIfOpen({
154
224
  executionId: input.executionId,
155
225
  verdict: execution.verdict,
156
226
  facts: execution.facts,
157
227
  terminalAt: new Date(now()).toISOString(),
158
- });
228
+ }));
159
229
  if (transition.status !== "committed")
160
230
  return { status: "rejected" };
161
231
  settled = true;
162
232
  if (transition.records[0]?.facts.state === "interrupted") {
163
- runnerLock.close();
164
- runnerLock = undefined;
165
- void input.wakeAfterInterrupt?.().catch(() => {
166
- // The ordinary wake path persists its typed failure. A later observer retries.
167
- });
233
+ wakeAfterLockRelease = true;
168
234
  }
169
235
  return { status: "completed" };
170
236
  };
171
- let gracefulAbortInvoked = false;
172
- let forceAbortInvoked = false;
173
- let sawKillIntent = false;
174
- let sawInterruptIntent = false;
175
- const observeStopBoundary = () => {
176
- try {
177
- if (!tellObserver.hasDataVersionChanged())
178
- return;
179
- const live = store.readCurrentGeneration();
180
- if (live?.launch.executionId !== input.executionId)
181
- return;
182
- if (live.verdict)
183
- return;
184
- sawKillIntent = live.killIntent !== undefined;
185
- sawInterruptIntent = live.interruptIntent !== undefined;
186
- if (!sawKillIntent && !sawInterruptIntent)
187
- return;
188
- if (gracefulAbortInvoked)
189
- return;
190
- gracefulAbortInvoked = true;
191
- void handle.abort("graceful").catch(() => {
192
- // Abort observation failures fail closed for this boundary.
193
- });
194
- }
195
- catch {
196
- // Observation failures fail closed and must not change provider outcome.
197
- }
198
- };
237
+ const observeStopBoundary = () => { pullStopIntent?.(); };
199
238
  // Mutable box so async settlement is readable without control-flow narrowing traps.
200
239
  const outcomeBox = { status: "pending" };
201
240
  let resolveActivity;
@@ -207,6 +246,7 @@ export async function runProjectionGeneration(input) {
207
246
  resolveActivity = undefined;
208
247
  resolve?.();
209
248
  };
249
+ notifyMainWait = bumpActivity;
210
250
  const outcomeWatch = handle.outcome.then((execution) => {
211
251
  outcomeBox.status = "fulfilled";
212
252
  outcomeBox.execution = execution;
@@ -220,6 +260,7 @@ export async function runProjectionGeneration(input) {
220
260
  });
221
261
  // Always rejection-observe so abandon paths never surface unhandled rejections.
222
262
  observeAbandoned(outcomeWatch);
263
+ const eventDrainState = { closed: false };
223
264
  const eventDrain = (async () => {
224
265
  try {
225
266
  for await (const _event of handle.events) {
@@ -230,20 +271,20 @@ export async function runProjectionGeneration(input) {
230
271
  }
231
272
  }
232
273
  catch {
233
- // Event-stream failures fail closed for observation; abandon quietly.
274
+ // Public event delivery is observation-only; outcome owns terminal truth.
275
+ }
276
+ finally {
277
+ eventDrainState.closed = true;
278
+ bumpActivity();
234
279
  }
235
- bumpActivity();
236
280
  })();
237
281
  observeAbandoned(eventDrain);
238
- // A provider may be silently executing when an operator interrupts it.
239
- // The durable observer is the sole stop-intent authority in that interval.
240
- intentTimer = setInterval(() => {
241
- observeStopBoundary();
242
- bumpActivity();
243
- }, 100);
244
- intentTimer.unref?.();
245
282
  while (outcomeBox.status === "pending" && !sawKillIntent && !sawInterruptIntent) {
246
- await activityChanged();
283
+ const wakeSources = [outcomeWatch, activityChanged()];
284
+ if (!eventDrainState.closed)
285
+ wakeSources.push(eventDrain);
286
+ await Promise.race(wakeSources);
287
+ observeStopBoundary();
247
288
  }
248
289
  if (sawKillIntent || sawInterruptIntent) {
249
290
  // Grace race: outcome settlement inside the window is stop proof.
@@ -275,10 +316,11 @@ export async function runProjectionGeneration(input) {
275
316
  throw killOutcome.error;
276
317
  throw new Error("interrupt outcome did not establish provider stop");
277
318
  }
278
- const killed = store.runnerKilledIfRequested({
319
+ const killed = commitTerminal(() => store.runnerKilledIfRequested({
279
320
  executionId: input.executionId,
280
321
  ...(facts && Object.keys(facts).length > 0 ? { facts } : {}),
281
- });
322
+ terminalAt: new Date(now()).toISOString(),
323
+ }));
282
324
  if (killed.status !== "committed")
283
325
  return { status: "rejected" };
284
326
  settled = true;
@@ -290,14 +332,17 @@ export async function runProjectionGeneration(input) {
290
332
  await handle.abort("force");
291
333
  if (sawInterruptIntent)
292
334
  return settleInterrupted();
293
- const killed = store.runnerKilledIfRequested({ executionId: input.executionId });
335
+ const killed = commitTerminal(() => store.runnerKilledIfRequested({
336
+ executionId: input.executionId,
337
+ terminalAt: new Date(now()).toISOString(),
338
+ }));
294
339
  if (killed.status !== "committed")
295
340
  return { status: "rejected" };
296
341
  settled = true;
297
342
  return { status: "completed" };
298
343
  }
299
344
  catch (error) {
300
- const failed = store.verdictIfOpen({
345
+ const failed = commitTerminal(() => store.verdictIfOpen({
301
346
  executionId: input.executionId,
302
347
  verdict: "failed",
303
348
  facts: {
@@ -313,7 +358,7 @@ export async function runProjectionGeneration(input) {
313
358
  exitCode: null,
314
359
  },
315
360
  },
316
- });
361
+ }));
317
362
  // Durably completed after the failed verdict; outer catch must not overwrite.
318
363
  settled = true;
319
364
  if (failed.status !== "committed")
@@ -344,6 +389,7 @@ export async function runProjectionGeneration(input) {
344
389
  try {
345
390
  const successorOwnership = runnerLock;
346
391
  await input.startSuccessor(successorExecutionId, () => {
392
+ pullStopIntent?.(true);
347
393
  const transition = store.handoff({
348
394
  executionId: input.executionId,
349
395
  completionFacts: execution.facts,
@@ -377,7 +423,7 @@ export async function runProjectionGeneration(input) {
377
423
  return settleRunnerOutcome(execution);
378
424
  }
379
425
  catch (error) {
380
- if (adopted && !settled) {
426
+ if (adopted && !settled && runnerLock) {
381
427
  let cleanupError;
382
428
  if (handle) {
383
429
  try {
@@ -387,9 +433,10 @@ export async function runProjectionGeneration(input) {
387
433
  cleanupError = failure;
388
434
  }
389
435
  }
436
+ pullStopIntent?.(true);
390
437
  const live = store.readCurrentGeneration();
391
438
  const detail = error instanceof Error ? error.message : String(error);
392
- store.runnerVerdictIfOpen({
439
+ const failed = commitTerminal(() => store.runnerVerdictIfOpen({
393
440
  executionId: input.executionId,
394
441
  verdict: "failed",
395
442
  facts: {
@@ -409,14 +456,13 @@ export async function runProjectionGeneration(input) {
409
456
  },
410
457
  },
411
458
  terminalAt: new Date(now()).toISOString(),
412
- });
459
+ }));
460
+ settled = failed.status === "committed";
413
461
  }
414
462
  throw error;
415
463
  }
416
464
  finally {
417
465
  clearGraceTimer();
418
- if (intentTimer !== undefined)
419
- clearInterval(intentTimer);
420
466
  try {
421
467
  tellObserver?.close();
422
468
  }
@@ -424,10 +470,20 @@ export async function runProjectionGeneration(input) {
424
470
  // Observer cleanup cannot reopen life or product authority.
425
471
  }
426
472
  try {
427
- runnerLock?.close();
473
+ await doorbell?.close();
428
474
  }
429
475
  finally {
430
- store.close();
476
+ try {
477
+ runnerLock?.close();
478
+ }
479
+ finally {
480
+ store.close();
481
+ }
482
+ }
483
+ if (wakeAfterLockRelease) {
484
+ void input.wakeAfterInterrupt?.().catch(() => {
485
+ // A later tell retries the ordinary wake path after its durable failure.
486
+ });
431
487
  }
432
488
  }
433
489
  }
@@ -1,47 +1,12 @@
1
1
  import { prepareProjectionGenerationExecution } from "./projection-generation-execution.js";
2
- import { PROJECTION_RUNNER_BOOTSTRAP_ENV, } from "./projection-generation-process.js";
2
+ import { receiveDetachedLease, } from "../../run-control/index.js";
3
3
  import { runProjectionGeneration } from "./projection-generation-runner.js";
4
- import { PROJECTION_ADOPTION_TIMEOUT_MS } from "./projection-generation-launcher.js";
5
4
  import { startProjectionGeneration } from "./projection-generation-launcher.js";
6
5
  import { snapshotPendingTells } from "./projection-generation-continuation.js";
7
6
  import { persistInterruptWakeFailure, tellProjection } from "../projection-wake.js";
8
7
  import { readProjectionIdentity } from "../projection-identity.js";
9
8
  import * as path from "node:path";
10
- import { acquireProjectionRunnerTransferTarget } from "../projection-runner-lock.js";
11
- function sendBootstrapMessage(message) {
12
- return new Promise((resolve, reject) => {
13
- if (!process.send || !process.connected) {
14
- reject(new Error("projection runner bootstrap channel is unavailable"));
15
- return;
16
- }
17
- process.send(message, (error) => error ? reject(error) : resolve());
18
- });
19
- }
20
- function waitForLaunchCommand(executionId, expected) {
21
- return new Promise((resolve, reject) => {
22
- const cleanup = () => {
23
- process.off("message", onMessage);
24
- process.off("disconnect", onDisconnect);
25
- };
26
- const onDisconnect = () => {
27
- cleanup();
28
- reject(new Error("projection runner bootstrap channel disconnected before launch authorization"));
29
- };
30
- const onMessage = (value) => {
31
- const command = value;
32
- if (command.executionId !== executionId)
33
- return;
34
- if (expected === "promote-leash" && command.kind !== "promote-leash" && command.kind !== "launch-rejected")
35
- return;
36
- if (expected === "launch-authority" && command.kind !== "launch-committed" && command.kind !== "launch-rejected")
37
- return;
38
- cleanup();
39
- resolve(command);
40
- };
41
- process.on("message", onMessage);
42
- process.on("disconnect", onDisconnect);
43
- });
44
- }
9
+ import { projectionRunnerLeasePath } from "./projection-generation-identity.js";
45
10
  export async function wakeProjectionAfterInterrupt(projectionDirectory, wake = tellProjection) {
46
11
  const pact = readProjectionIdentity(projectionDirectory);
47
12
  try {
@@ -53,30 +18,17 @@ export async function wakeProjectionAfterInterrupt(projectionDirectory, wake = t
53
18
  }
54
19
  }
55
20
  export async function runProjectionGenerationRuntime(projectionDirectory, executionId) {
56
- if (process.env[PROJECTION_RUNNER_BOOTSTRAP_ENV] !== "1") {
57
- throw new Error("projection runner was not spawned with inherited leash coordination");
58
- }
59
- let runnerLock;
60
- let transferTarget;
21
+ const transfer = await receiveDetachedLease({
22
+ coordinate: executionId,
23
+ leasePath: projectionRunnerLeasePath(projectionDirectory),
24
+ });
25
+ if (transfer.status === "rejected")
26
+ return;
61
27
  try {
62
- transferTarget = acquireProjectionRunnerTransferTarget(projectionDirectory, process.platform, PROJECTION_ADOPTION_TIMEOUT_MS);
63
- await sendBootstrapMessage({ kind: "leash-inherited", executionId });
64
- const promotion = await waitForLaunchCommand(executionId, "promote-leash");
65
- if (promotion.kind === "launch-rejected")
66
- return;
67
- runnerLock = transferTarget.promote();
68
- transferTarget = undefined;
69
- await sendBootstrapMessage({ kind: "leash-acquired", executionId });
70
- const command = await waitForLaunchCommand(executionId, "launch-authority");
71
- if (command.kind === "launch-rejected")
72
- return;
73
- process.disconnect?.();
74
- const ownedLock = runnerLock;
75
- runnerLock = undefined;
76
28
  await runProjectionGeneration({
77
29
  projectionDirectory,
78
30
  executionId,
79
- runnerLock: ownedLock,
31
+ runnerLock: transfer.lease,
80
32
  prepareExecution: (launch) => prepareProjectionGenerationExecution(projectionDirectory, launch),
81
33
  snapshotPendingTells: () => snapshotPendingTells(projectionDirectory),
82
34
  startSuccessor: async (successorExecutionId, commitLaunch, launchOwnership) => {
@@ -90,8 +42,5 @@ export async function runProjectionGenerationRuntime(projectionDirectory, execut
90
42
  wakeAfterInterrupt: () => wakeProjectionAfterInterrupt(projectionDirectory),
91
43
  });
92
44
  }
93
- finally {
94
- runnerLock?.close();
95
- transferTarget?.close();
96
- }
45
+ finally { /* execution owns and closes the transferred lease */ }
97
46
  }
@@ -0,0 +1,68 @@
1
+ import { GENERATION_VERDICT_STATES, } from "./model.js";
2
+ function recordOfKind(record, kind) {
3
+ return record.kind === kind ? record : undefined;
4
+ }
5
+ /** Select the newest generation by its Projection ledger sequence. */
6
+ export function selectCurrentGeneration(records) {
7
+ let launch;
8
+ for (const record of records)
9
+ launch = recordOfKind(record, "launch") ?? launch;
10
+ if (!launch)
11
+ return null;
12
+ let adoption;
13
+ let killIntent;
14
+ let interruptIntent;
15
+ let verdict;
16
+ for (const record of records) {
17
+ if (record.seq <= launch.seq || record.executionId !== launch.executionId)
18
+ continue;
19
+ adoption ??= recordOfKind(record, "adoption");
20
+ killIntent ??= recordOfKind(record, "kill-intent");
21
+ interruptIntent ??= recordOfKind(record, "interrupt-intent");
22
+ verdict ??= recordOfKind(record, "verdict");
23
+ }
24
+ return { launch, ...(adoption ? { adoption } : {}), ...(killIntent ? { killIntent } : {}), ...(interruptIntent ? { interruptIntent } : {}), ...(verdict ? { verdict } : {}) };
25
+ }
26
+ function verdictState(record) {
27
+ const state = record?.facts.state;
28
+ return typeof state === "string" && GENERATION_VERDICT_STATES.includes(state)
29
+ ? state
30
+ : undefined;
31
+ }
32
+ function phaseForVerdict(state) {
33
+ switch (state) {
34
+ case "completed": return "done";
35
+ case "dismissed": return "dismissed";
36
+ case "interrupted": return "done";
37
+ case "killed": return "killed";
38
+ case "lost-runner": return "lost";
39
+ case "failed":
40
+ case "launch-failed": return "failed";
41
+ }
42
+ }
43
+ export function deriveProjectionLife(input) {
44
+ const chain = selectCurrentGeneration(input.records);
45
+ if (!chain)
46
+ return { phase: "quiescent" };
47
+ const executionId = chain.launch.executionId;
48
+ if (chain.verdict) {
49
+ const state = verdictState(chain.verdict);
50
+ return state
51
+ ? { phase: phaseForVerdict(state), executionId, verdict: state }
52
+ : { phase: "unknown", executionId, diagnostic: "current generation has an invalid verdict" };
53
+ }
54
+ if (!chain.adoption) {
55
+ switch (input.runnerLease) {
56
+ case "held": return { phase: "minting", executionId };
57
+ case "released": return { phase: "lost", executionId, diagnostic: "startup owner died before adoption" };
58
+ case "unknown":
59
+ case undefined: return { phase: "unknown", executionId, diagnostic: "runner lease could not be determined" };
60
+ }
61
+ }
62
+ switch (input.runnerLease) {
63
+ case "held": return { phase: "active", executionId };
64
+ case "released": return { phase: "lost", executionId };
65
+ case "unknown":
66
+ case undefined: return { phase: "unknown", executionId, diagnostic: "runner lease could not be determined" };
67
+ }
68
+ }
@@ -1,4 +1,4 @@
1
- import { PROJECTION_GENERATION_SCHEMA_VERSION, PROJECTION_GENERATION_STORE_FILE, assertDatabaseOpen, beginGenerationWriteTransaction, closeDatabase, commitGenerationWriteTransaction, openMutableDatabase, openReadOnlyDatabase, readValidatedHistory, rollbackGenerationWriteTransaction, } from "./database.js";
1
+ import { PROJECTION_GENERATION_SCHEMA_VERSION, PROJECTION_GENERATION_STORE_FILE, assertDatabaseOpen, closeDatabase, connectionBoundGenerationStore, openMutableDatabase, openReadOnlyDatabase, readValidatedHistory, } from "./database.js";
2
2
  import { initializeIdentity as initializeProjectionIdentity, readIdentity as readProjectionIdentity, } from "./identity.js";
3
3
  import * as transitions from "./transitions.js";
4
4
  export { PROJECTION_GENERATION_SCHEMA_VERSION, PROJECTION_GENERATION_STORE_FILE, };
@@ -7,10 +7,12 @@ export class ProjectionGenerationStore {
7
7
  databasePath;
8
8
  readOnly;
9
9
  #database;
10
+ #generationStore;
10
11
  constructor(databasePath, database, readOnly) {
11
12
  this.databasePath = databasePath;
12
13
  this.#database = database;
13
14
  this.readOnly = readOnly;
15
+ this.#generationStore = connectionBoundGenerationStore(database, readOnly);
14
16
  }
15
17
  static open(projectionDirectory) {
16
18
  const { databasePath, database } = openMutableDatabase(projectionDirectory);
@@ -21,25 +23,33 @@ export class ProjectionGenerationStore {
21
23
  return new ProjectionGenerationStore(databasePath, database, true);
22
24
  }
23
25
  close() {
24
- rollbackGenerationWriteTransaction(this.#database);
26
+ assertDatabaseOpen(this.#database);
27
+ this.#generationStore.rollbackWriteTransaction();
25
28
  closeDatabase(this.#database);
26
29
  }
27
30
  beginWriteTransaction() {
28
- beginGenerationWriteTransaction(this.#database, this.readOnly);
31
+ assertDatabaseOpen(this.#database);
32
+ this.#generationStore.beginWriteTransaction();
29
33
  }
30
34
  commitWriteTransaction() {
31
- commitGenerationWriteTransaction(this.#database);
35
+ assertDatabaseOpen(this.#database);
36
+ this.#generationStore.commitWriteTransaction();
32
37
  }
33
38
  rollbackWriteTransaction() {
34
- rollbackGenerationWriteTransaction(this.#database);
39
+ assertDatabaseOpen(this.#database);
40
+ this.#generationStore.rollbackWriteTransaction();
41
+ }
42
+ connectionForRunnerClose() {
43
+ assertDatabaseOpen(this.#database);
44
+ return this.#database;
35
45
  }
36
46
  readRecords() {
37
47
  assertDatabaseOpen(this.#database);
38
- return readValidatedHistory(this.#database).records;
48
+ return readValidatedHistory(this.#database, this.readOnly).records;
39
49
  }
40
50
  readCurrentGeneration() {
41
51
  assertDatabaseOpen(this.#database);
42
- return readValidatedHistory(this.#database).current;
52
+ return readValidatedHistory(this.#database, this.readOnly).current;
43
53
  }
44
54
  initializeIdentity(identity) {
45
55
  initializeProjectionIdentity(this.#database, this.databasePath, this.readOnly, identity);