@astrosheep/keiyaku 2.9.12 → 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 (212) hide show
  1. package/README.md +22 -18
  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 +2 -2
  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/petition/handler.js +2 -1
  17. package/build/cli/commands/contract/petition/meta.js +2 -2
  18. package/build/cli/commands/contract/renew/handler.js +44 -3
  19. package/build/cli/commands/contract/renew/meta.js +5 -4
  20. package/build/cli/commands/metadata.js +12 -9
  21. package/build/cli/commands/projection/call/handler.js +1 -1
  22. package/build/cli/commands/projection/call/meta.js +1 -1
  23. package/build/cli/commands/projection/catalog.js +2 -2
  24. package/build/cli/commands/projection/{revive → fork}/handler.js +10 -5
  25. package/build/cli/commands/projection/fork/meta.js +12 -0
  26. package/build/cli/commands/projection/history/handler.js +46 -2
  27. package/build/cli/commands/projection/history/meta.js +3 -3
  28. package/build/cli/commands/projection/kill/meta.js +2 -2
  29. package/build/cli/commands/projection/status/handler.js +29 -13
  30. package/build/cli/commands/projection/status/meta.js +14 -3
  31. package/build/cli/commands/projection/tell/handler.js +8 -4
  32. package/build/cli/commands/projection/tell/meta.js +1 -1
  33. package/build/cli/commands/projection/wait/handler.js +45 -20
  34. package/build/cli/commands/projection/wait/meta.js +1 -1
  35. package/build/cli/commands/shared.js +1 -1
  36. package/build/cli/commands/system/catalog.js +2 -0
  37. package/build/cli/commands/task/add/handler.js +28 -12
  38. package/build/cli/commands/task/add/meta.js +18 -6
  39. package/build/cli/commands/task/catalog.js +2 -0
  40. package/build/cli/commands/task/compose/handler.js +2 -2
  41. package/build/cli/commands/task/compose/meta.js +2 -2
  42. package/build/cli/commands/task/doctor/handler.js +2 -3
  43. package/build/cli/commands/task/doctor/meta.js +2 -2
  44. package/build/cli/commands/task/done/meta.js +2 -2
  45. package/build/cli/commands/task/drop/handler.js +7 -2
  46. package/build/cli/commands/task/drop/meta.js +6 -3
  47. package/build/cli/commands/task/hold/meta.js +2 -2
  48. package/build/cli/commands/task/log/meta.js +2 -2
  49. package/build/cli/commands/task/ls/meta.js +2 -2
  50. package/build/cli/commands/task/note/handler.js +6 -0
  51. package/build/cli/commands/task/note/meta.js +11 -0
  52. package/build/cli/commands/task/resume/meta.js +2 -2
  53. package/build/cli/commands/task/shared.js +29 -15
  54. package/build/cli/commands/task/show/meta.js +2 -2
  55. package/build/cli/commands/task/start/meta.js +2 -2
  56. package/build/cli/commands/task/stop/meta.js +2 -2
  57. package/build/cli/commands/task/update/meta.js +3 -2
  58. package/build/cli/commands/verification/handler.js +43 -0
  59. package/build/cli/commands/verification/meta.js +13 -0
  60. package/build/cli/completion.js +16 -13
  61. package/build/cli/flags.js +43 -11
  62. package/build/cli/help.js +29 -30
  63. package/build/cli/index.js +6 -5
  64. package/build/cli/parse-flags.js +39 -0
  65. package/build/cli/parse-metadata.js +5 -2
  66. package/build/cli/parse-selectors.js +18 -0
  67. package/build/cli/parse.js +21 -8
  68. package/build/cli/render/arc.js +12 -3
  69. package/build/cli/render/audit.js +2 -2
  70. package/build/cli/render/call.js +4 -4
  71. package/build/cli/render/kanshi.js +122 -85
  72. package/build/cli/render/misc.js +2 -2
  73. package/build/cli/render/petition.js +16 -1
  74. package/build/cli/render/projection-history.js +17 -4
  75. package/build/cli/render/shared.js +5 -5
  76. package/build/cli/render/status-indicator.js +47 -0
  77. package/build/cli/render/status.js +106 -76
  78. package/build/cli/render/success-response.js +20 -5
  79. package/build/cli/render/task.js +6 -5
  80. package/build/cli/render/tell.js +10 -4
  81. package/build/cli/render/tool-command-normalization.js +137 -0
  82. package/build/cli/render/tool-presentation.js +2 -1
  83. package/build/cli/render/verification.js +23 -0
  84. package/build/cli/render/wait.js +101 -33
  85. package/build/cli/types.js +2 -1
  86. package/build/config/akuma-loader.js +21 -1
  87. package/build/config/settings/knobs.js +7 -1
  88. package/build/config/settings/schema.js +10 -5
  89. package/build/core/amend.js +102 -17
  90. package/build/core/arc.js +4 -4
  91. package/build/core/audit/candidate.js +10 -18
  92. package/build/core/audit/coordinates.js +12 -17
  93. package/build/core/audit/evidence.js +6 -4
  94. package/build/core/audit/facade.js +3 -3
  95. package/build/core/audit/report.js +1 -1
  96. package/build/core/bind-reconciliation.js +42 -23
  97. package/build/core/bind.js +182 -81
  98. package/build/core/call/call.js +6 -7
  99. package/build/core/call/context.js +43 -30
  100. package/build/core/call/execution.js +21 -6
  101. package/build/core/call/prompt.js +11 -42
  102. package/build/core/contract-carrier-runtime.js +209 -105
  103. package/build/core/contract-carrier.js +156 -73
  104. package/build/core/contract-view.js +42 -5
  105. package/build/core/contract.js +2 -1
  106. package/build/core/derived-replay.js +168 -35
  107. package/build/core/draft.js +9 -5
  108. package/build/core/forfeit.js +1 -1
  109. package/build/core/lifecycle-history.js +13 -0
  110. package/build/core/lifecycle-recovery.js +32 -27
  111. package/build/core/lifecycle-runner.js +14 -14
  112. package/build/core/log.js +1 -1
  113. package/build/core/outcome-base.js +2 -2
  114. package/build/core/projection/akuma-name.js +43 -0
  115. package/build/core/projection/generation/database.js +141 -79
  116. package/build/core/projection/generation/ledger.js +91 -0
  117. package/build/core/projection/generation/model.js +18 -13
  118. package/build/core/projection/generation/projection-generation-continuation.js +1 -1
  119. package/build/core/projection/generation/projection-generation-doorbell.js +88 -0
  120. package/build/core/projection/generation/projection-generation-execution.js +24 -0
  121. package/build/core/projection/generation/projection-generation-identity.js +4 -0
  122. package/build/core/projection/generation/projection-generation-launcher.js +72 -138
  123. package/build/core/projection/generation/projection-generation-process.js +16 -15
  124. package/build/core/projection/generation/projection-generation-runner.js +125 -79
  125. package/build/core/projection/generation/projection-generation-runtime.js +10 -61
  126. package/build/core/projection/generation/protocol.js +68 -0
  127. package/build/core/projection/generation/store.js +17 -7
  128. package/build/core/projection/generation/transitions.js +14 -14
  129. package/build/core/projection/index.js +5 -5
  130. package/build/core/projection/projection-core.js +6 -2
  131. package/build/core/projection/projection-execution-observer.js +1 -1
  132. package/build/core/projection/projection-history.js +7 -7
  133. package/build/core/projection/projection-kill.js +15 -6
  134. package/build/core/projection/projection-life-observer.js +8 -8
  135. package/build/core/projection/projection-mint.js +6 -8
  136. package/build/core/projection/projection-status-observation.js +77 -8
  137. package/build/core/projection/projection-status.js +151 -4
  138. package/build/core/projection/projection-wait.js +46 -27
  139. package/build/core/projection/projection-wake.js +27 -89
  140. package/build/core/projection/tell/launch-store.js +1 -1
  141. package/build/core/projection/tell/store.js +1 -1
  142. package/build/core/renew-build.js +204 -74
  143. package/build/core/renew-plan.js +34 -9
  144. package/build/core/renew-rewrite.js +56 -15
  145. package/build/core/renew.js +39 -35
  146. package/build/core/repository-ledger/accepted-fold-read.js +6 -3
  147. package/build/core/repository-ledger/codec.js +11 -5
  148. package/build/core/repository-ledger/current-state-store.js +13 -16
  149. package/build/core/repository-ledger/fold-repository.js +5 -6
  150. package/build/core/repository-ledger/identity.js +60 -0
  151. package/build/core/repository-ledger/inventory.js +3 -3
  152. package/build/core/repository-ledger/read-model.js +54 -6
  153. package/build/core/repository-ledger/write-transaction.js +9 -4
  154. package/build/core/run-control/connection-bound-close.js +41 -0
  155. package/build/core/run-control/detached-lease-bootstrap.js +208 -0
  156. package/build/core/run-control/detached-process.js +14 -0
  157. package/build/core/run-control/index.js +6 -0
  158. package/build/core/{process-group.js → run-control/process-tree.js} +1 -1
  159. package/build/core/run-control/runner-lease.js +172 -0
  160. package/build/core/{sqlite-process-lifetime-lock.js → run-control/sqlite-process-lifetime-lock.js} +4 -24
  161. package/build/core/seal.js +44 -32
  162. package/build/core/settlement/claim-delivery.js +22 -9
  163. package/build/core/settlement/claim.js +8 -6
  164. package/build/core/settlement/petition-claim-gates.js +9 -8
  165. package/build/core/settlement/petition-head-guard.js +5 -6
  166. package/build/core/settlement/petition-preview.js +21 -5
  167. package/build/core/settlement/petition.js +51 -17
  168. package/build/core/settlement/settlement.js +20 -15
  169. package/build/core/settlement/verdict.js +2 -2
  170. package/build/core/settlement/verification.js +492 -364
  171. package/build/core/status/board.js +184 -75
  172. package/build/core/status/drift.js +8 -10
  173. package/build/core/task/board.js +1 -0
  174. package/build/core/task/commands.js +25 -0
  175. package/build/core/task/compose.js +3 -2
  176. package/build/core/task/document.js +102 -39
  177. package/build/core/task/index.js +3 -3
  178. package/build/core/task/settlement-git.js +1 -4
  179. package/build/core/task/settlement-policy.js +27 -14
  180. package/build/core/task/source-board.js +1 -0
  181. package/build/core/task/task-bind-preparation.js +59 -9
  182. package/build/core/task/task-contract.js +2 -2
  183. package/build/core/task/task-store-repository.js +1 -0
  184. package/build/core/task/task.js +2 -2
  185. package/build/core/transcripts.js +4 -4
  186. package/build/core/verification-declaration.js +95 -0
  187. package/build/core/worktree-bootstrap.js +1 -1
  188. package/build/core/worktree-path.js +7 -11
  189. package/build/flow-error.js +9 -1
  190. package/build/generated/version.js +2 -2
  191. package/build/git/branches.js +41 -70
  192. package/build/git/commits.js +92 -21
  193. package/build/git/core.js +9 -161
  194. package/build/git/diff/preview.js +4 -4
  195. package/build/git/diff/read.js +4 -4
  196. package/build/git/diff/structured.js +5 -5
  197. package/build/git/process.js +229 -0
  198. package/build/git/refs.js +30 -5
  199. package/build/git/staging.js +39 -115
  200. package/build/git/streaming-batch.js +9 -16
  201. package/build/git/worktree.js +104 -91
  202. package/build/index.js +7 -0
  203. package/package.json +1 -1
  204. package/skills/keiyaku-akuma/SKILL.md +21 -20
  205. package/skills/keiyaku-task/SKILL.md +38 -4
  206. package/skills/keiyaku-workflow/SKILL.md +51 -11
  207. package/build/cli/commands/projection/revive/meta.js +0 -12
  208. package/build/core/projection/projection-life-protocol.js +0 -115
  209. package/build/core/projection/projection-runner-lock.js +0 -211
  210. package/build/core/projection/tell/database.js +0 -127
  211. package/build/core/settlement/verification-coordination.js +0 -305
  212. 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,10 +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({
181
+ const killed = commitTerminal(() => store.runnerKilledIfRequested({
116
182
  executionId: input.executionId,
117
183
  terminalAt: new Date(now()).toISOString(),
118
- });
184
+ }));
119
185
  if (killed.status !== "committed")
120
186
  return { status: "rejected" };
121
187
  settled = true;
@@ -129,10 +195,10 @@ export async function runProjectionGeneration(input) {
129
195
  }
130
196
  if (current.killIntent) {
131
197
  await prepared.dispose();
132
- const killed = store.runnerKilledIfRequested({
198
+ const killed = commitTerminal(() => store.runnerKilledIfRequested({
133
199
  executionId: input.executionId,
134
200
  terminalAt: new Date(now()).toISOString(),
135
- });
201
+ }));
136
202
  if (killed.status !== "committed")
137
203
  return { status: "rejected" };
138
204
  settled = true;
@@ -140,68 +206,35 @@ export async function runProjectionGeneration(input) {
140
206
  }
141
207
  handle = prepared.start();
142
208
  const settleInterrupted = (facts = {}) => {
143
- const transition = store.runnerInterruptedIfRequested({
209
+ pullStopIntent?.(true);
210
+ const transition = commitTerminal(() => store.runnerInterruptedIfRequested({
144
211
  executionId: input.executionId,
145
212
  facts,
146
213
  terminalAt: new Date(now()).toISOString(),
147
- });
214
+ }));
148
215
  if (transition.status !== "committed")
149
216
  return { status: "rejected" };
150
217
  settled = true;
151
- runnerLock.close();
152
- runnerLock = undefined;
153
- void input.wakeAfterInterrupt?.().catch(() => {
154
- // The ordinary wake path persists its typed failure. A later tell can retry.
155
- });
218
+ wakeAfterLockRelease = true;
156
219
  return { status: "completed" };
157
220
  };
158
221
  const settleRunnerOutcome = (execution) => {
159
- const transition = store.runnerVerdictIfOpen({
222
+ pullStopIntent?.(true);
223
+ const transition = commitTerminal(() => store.runnerVerdictIfOpen({
160
224
  executionId: input.executionId,
161
225
  verdict: execution.verdict,
162
226
  facts: execution.facts,
163
227
  terminalAt: new Date(now()).toISOString(),
164
- });
228
+ }));
165
229
  if (transition.status !== "committed")
166
230
  return { status: "rejected" };
167
231
  settled = true;
168
232
  if (transition.records[0]?.facts.state === "interrupted") {
169
- runnerLock.close();
170
- runnerLock = undefined;
171
- void input.wakeAfterInterrupt?.().catch(() => {
172
- // The ordinary wake path persists its typed failure. A later observer retries.
173
- });
233
+ wakeAfterLockRelease = true;
174
234
  }
175
235
  return { status: "completed" };
176
236
  };
177
- let gracefulAbortInvoked = false;
178
- let forceAbortInvoked = false;
179
- let sawKillIntent = false;
180
- let sawInterruptIntent = false;
181
- const observeStopBoundary = () => {
182
- try {
183
- if (!tellObserver.hasDataVersionChanged())
184
- return;
185
- const live = store.readCurrentGeneration();
186
- if (live?.launch.executionId !== input.executionId)
187
- return;
188
- if (live.verdict)
189
- return;
190
- sawKillIntent = live.killIntent !== undefined;
191
- sawInterruptIntent = live.interruptIntent !== undefined;
192
- if (!sawKillIntent && !sawInterruptIntent)
193
- return;
194
- if (gracefulAbortInvoked)
195
- return;
196
- gracefulAbortInvoked = true;
197
- void handle.abort("graceful").catch(() => {
198
- // Abort observation failures fail closed for this boundary.
199
- });
200
- }
201
- catch {
202
- // Observation failures fail closed and must not change provider outcome.
203
- }
204
- };
237
+ const observeStopBoundary = () => { pullStopIntent?.(); };
205
238
  // Mutable box so async settlement is readable without control-flow narrowing traps.
206
239
  const outcomeBox = { status: "pending" };
207
240
  let resolveActivity;
@@ -213,6 +246,7 @@ export async function runProjectionGeneration(input) {
213
246
  resolveActivity = undefined;
214
247
  resolve?.();
215
248
  };
249
+ notifyMainWait = bumpActivity;
216
250
  const outcomeWatch = handle.outcome.then((execution) => {
217
251
  outcomeBox.status = "fulfilled";
218
252
  outcomeBox.execution = execution;
@@ -226,6 +260,7 @@ export async function runProjectionGeneration(input) {
226
260
  });
227
261
  // Always rejection-observe so abandon paths never surface unhandled rejections.
228
262
  observeAbandoned(outcomeWatch);
263
+ const eventDrainState = { closed: false };
229
264
  const eventDrain = (async () => {
230
265
  try {
231
266
  for await (const _event of handle.events) {
@@ -236,20 +271,20 @@ export async function runProjectionGeneration(input) {
236
271
  }
237
272
  }
238
273
  catch {
239
- // 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();
240
279
  }
241
- bumpActivity();
242
280
  })();
243
281
  observeAbandoned(eventDrain);
244
- // A provider may be silently executing when an operator interrupts it.
245
- // The durable observer is the sole stop-intent authority in that interval.
246
- intentTimer = setInterval(() => {
247
- observeStopBoundary();
248
- bumpActivity();
249
- }, 100);
250
- intentTimer.unref?.();
251
282
  while (outcomeBox.status === "pending" && !sawKillIntent && !sawInterruptIntent) {
252
- await activityChanged();
283
+ const wakeSources = [outcomeWatch, activityChanged()];
284
+ if (!eventDrainState.closed)
285
+ wakeSources.push(eventDrain);
286
+ await Promise.race(wakeSources);
287
+ observeStopBoundary();
253
288
  }
254
289
  if (sawKillIntent || sawInterruptIntent) {
255
290
  // Grace race: outcome settlement inside the window is stop proof.
@@ -281,11 +316,11 @@ export async function runProjectionGeneration(input) {
281
316
  throw killOutcome.error;
282
317
  throw new Error("interrupt outcome did not establish provider stop");
283
318
  }
284
- const killed = store.runnerKilledIfRequested({
319
+ const killed = commitTerminal(() => store.runnerKilledIfRequested({
285
320
  executionId: input.executionId,
286
321
  ...(facts && Object.keys(facts).length > 0 ? { facts } : {}),
287
322
  terminalAt: new Date(now()).toISOString(),
288
- });
323
+ }));
289
324
  if (killed.status !== "committed")
290
325
  return { status: "rejected" };
291
326
  settled = true;
@@ -297,17 +332,17 @@ export async function runProjectionGeneration(input) {
297
332
  await handle.abort("force");
298
333
  if (sawInterruptIntent)
299
334
  return settleInterrupted();
300
- const killed = store.runnerKilledIfRequested({
335
+ const killed = commitTerminal(() => store.runnerKilledIfRequested({
301
336
  executionId: input.executionId,
302
337
  terminalAt: new Date(now()).toISOString(),
303
- });
338
+ }));
304
339
  if (killed.status !== "committed")
305
340
  return { status: "rejected" };
306
341
  settled = true;
307
342
  return { status: "completed" };
308
343
  }
309
344
  catch (error) {
310
- const failed = store.verdictIfOpen({
345
+ const failed = commitTerminal(() => store.verdictIfOpen({
311
346
  executionId: input.executionId,
312
347
  verdict: "failed",
313
348
  facts: {
@@ -323,7 +358,7 @@ export async function runProjectionGeneration(input) {
323
358
  exitCode: null,
324
359
  },
325
360
  },
326
- });
361
+ }));
327
362
  // Durably completed after the failed verdict; outer catch must not overwrite.
328
363
  settled = true;
329
364
  if (failed.status !== "committed")
@@ -354,6 +389,7 @@ export async function runProjectionGeneration(input) {
354
389
  try {
355
390
  const successorOwnership = runnerLock;
356
391
  await input.startSuccessor(successorExecutionId, () => {
392
+ pullStopIntent?.(true);
357
393
  const transition = store.handoff({
358
394
  executionId: input.executionId,
359
395
  completionFacts: execution.facts,
@@ -387,7 +423,7 @@ export async function runProjectionGeneration(input) {
387
423
  return settleRunnerOutcome(execution);
388
424
  }
389
425
  catch (error) {
390
- if (adopted && !settled) {
426
+ if (adopted && !settled && runnerLock) {
391
427
  let cleanupError;
392
428
  if (handle) {
393
429
  try {
@@ -397,9 +433,10 @@ export async function runProjectionGeneration(input) {
397
433
  cleanupError = failure;
398
434
  }
399
435
  }
436
+ pullStopIntent?.(true);
400
437
  const live = store.readCurrentGeneration();
401
438
  const detail = error instanceof Error ? error.message : String(error);
402
- store.runnerVerdictIfOpen({
439
+ const failed = commitTerminal(() => store.runnerVerdictIfOpen({
403
440
  executionId: input.executionId,
404
441
  verdict: "failed",
405
442
  facts: {
@@ -419,14 +456,13 @@ export async function runProjectionGeneration(input) {
419
456
  },
420
457
  },
421
458
  terminalAt: new Date(now()).toISOString(),
422
- });
459
+ }));
460
+ settled = failed.status === "committed";
423
461
  }
424
462
  throw error;
425
463
  }
426
464
  finally {
427
465
  clearGraceTimer();
428
- if (intentTimer !== undefined)
429
- clearInterval(intentTimer);
430
466
  try {
431
467
  tellObserver?.close();
432
468
  }
@@ -434,10 +470,20 @@ export async function runProjectionGeneration(input) {
434
470
  // Observer cleanup cannot reopen life or product authority.
435
471
  }
436
472
  try {
437
- runnerLock?.close();
473
+ await doorbell?.close();
438
474
  }
439
475
  finally {
440
- 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
+ });
441
487
  }
442
488
  }
443
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);