@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
@@ -5,7 +5,6 @@ import { isProjectionId, projectionRoot, projectionRootAtCoordinate } from "./pr
5
5
  import { projectionAliasRootAtCoordinate, readProjectionAliasAtRoot, } from "./projection-alias.js";
6
6
  import { resolveProjectionCoordinate } from "./projection-coordinate.js";
7
7
  import { readProjectionIdentity } from "./projection-identity.js";
8
- import { PROJECTION_ADOPTION_TIMEOUT_MS } from "./generation/projection-generation-launcher.js";
9
8
  import { observeProjectionLifeSnapshot, } from "./projection-life-observer.js";
10
9
  import { worktreeContainsCoordinate } from "../worktree-path.js";
11
10
  import { readProjectionRow, readProjectionSummary } from "./projection-status-observation.js";
@@ -75,6 +74,104 @@ export function prioritizeProjectionStatusRows(rows) {
75
74
  const total = Object.values(byState).reduce((sum, count) => sum + count, 0);
76
75
  return total > 0 ? { rows: visible, omitted: { total, byState } } : { rows: visible };
77
76
  }
77
+ const TRIAGE_UNKNOWN_LIMIT = 8;
78
+ const TRIAGE_ACTIVE_LIMIT = 8;
79
+ const TRIAGE_RECENT_TERMINAL_LIMIT = 3;
80
+ function isTerminalVerdictState(state) {
81
+ return state === "failed"
82
+ || state === "dead"
83
+ || state === "done"
84
+ || state === "killed"
85
+ || state === "dismissed"
86
+ || state === "incompatible";
87
+ }
88
+ function isUnknownRow(row) {
89
+ return row.state === "lost" || row.state === "unknown";
90
+ }
91
+ function isStartupFailedRow(row) {
92
+ return row.state === "startup-timeout";
93
+ }
94
+ function isActiveRow(row) {
95
+ return row.state === "minting" || row.state === "out";
96
+ }
97
+ /**
98
+ * Main status is a bounded lifecycle summary, not a second roster. Keep
99
+ * category limits here so JSON and the human renderer receive the same rows.
100
+ */
101
+ export function triageProjectionStatusRows(rows) {
102
+ if (rows.length === 0)
103
+ return { rows: [] };
104
+ const ordered = [...rows].sort(compareProjectionStatusRows);
105
+ const startupFailedRows = ordered.filter(isStartupFailedRow);
106
+ const unknownRows = ordered.filter(isUnknownRow);
107
+ const activeRows = ordered.filter(isActiveRow);
108
+ const terminalRows = ordered.filter((row) => isTerminalVerdictState(row.state));
109
+ const boundedObservationFailures = ordered
110
+ .filter((row) => isStartupFailedRow(row) || isUnknownRow(row))
111
+ .slice(0, TRIAGE_UNKNOWN_LIMIT);
112
+ const startupFailed = boundedObservationFailures.filter(isStartupFailedRow);
113
+ const unknown = boundedObservationFailures.filter(isUnknownRow);
114
+ const selected = new Set(boundedObservationFailures.map((row) => row.id));
115
+ const active = ordered.filter((row) => !selected.has(row.id) && isActiveRow(row)).slice(0, TRIAGE_ACTIVE_LIMIT);
116
+ active.forEach((row) => selected.add(row.id));
117
+ const recentTerminal = ordered
118
+ .filter((row) => !selected.has(row.id) && isTerminalVerdictState(row.state))
119
+ .slice(0, TRIAGE_RECENT_TERMINAL_LIMIT);
120
+ recentTerminal.forEach((row) => selected.add(row.id));
121
+ const omittedRows = ordered.filter((row) => !selected.has(row.id));
122
+ const byState = {
123
+ failed: 0, dead: 0, done: 0, killed: 0, dismissed: 0, incompatible: 0,
124
+ };
125
+ for (const row of omittedRows) {
126
+ if (isOmissibleProjectionStatusState(row.state))
127
+ byState[row.state] += 1;
128
+ }
129
+ const omittedTerminalTotal = Object.values(byState).reduce((sum, count) => sum + count, 0);
130
+ return {
131
+ rows: [...startupFailed, ...unknown, ...active, ...recentTerminal],
132
+ triage: {
133
+ startupFailed,
134
+ unknown,
135
+ active,
136
+ recentTerminal,
137
+ totals: {
138
+ startupFailed: startupFailedRows.length,
139
+ unknown: unknownRows.length,
140
+ active: activeRows.length,
141
+ terminal: terminalRows.length,
142
+ },
143
+ overflow: omittedRows.length,
144
+ },
145
+ ...(omittedTerminalTotal > 0 ? { omitted: { total: omittedTerminalTotal, byState } } : {}),
146
+ };
147
+ }
148
+ /** Terminal history is pageable; active observations stay complete. */
149
+ export function paginateProjectionStatusRoster(board, input) {
150
+ const limit = input.limit ?? 20;
151
+ const offset = input.offset ?? 0;
152
+ const nonterminal = board.rows.filter((row) => !isTerminalVerdictState(row.state));
153
+ const terminal = board.rows.filter((row) => isTerminalVerdictState(row.state));
154
+ const selectedTerminal = terminal.slice(offset, offset + limit);
155
+ const omittedRows = [...terminal.slice(0, offset), ...terminal.slice(offset + limit)];
156
+ const byState = {
157
+ failed: 0, dead: 0, done: 0, killed: 0, dismissed: 0, incompatible: 0,
158
+ };
159
+ for (const row of omittedRows) {
160
+ if (isOmissibleProjectionStatusState(row.state))
161
+ byState[row.state] += 1;
162
+ }
163
+ return {
164
+ rows: [...nonterminal, ...selectedTerminal],
165
+ ...(omittedRows.length > 0 ? { omitted: { total: omittedRows.length, byState } } : {}),
166
+ pagination: {
167
+ limit,
168
+ offset,
169
+ returned: selectedTerminal.length,
170
+ total: terminal.length,
171
+ hasMore: offset + selectedTerminal.length < terminal.length,
172
+ },
173
+ };
174
+ }
78
175
  export function requiresProjectionStatusDetails(state) {
79
176
  return !isOmissibleProjectionStatusState(state);
80
177
  }
@@ -161,7 +258,29 @@ export async function readOperationalProjectionStatusBoard(cwd, nowMs) {
161
258
  const rows = summaries.map((summary) => readProjectionRow(summary, nowMs, requiresProjectionStatusDetails(summary.state)));
162
259
  // A published root is an observable surface even when it currently has no
163
260
  // rows. The caller must distinguish that fact from an unavailable root.
164
- return prioritizeProjectionStatusRows(rows);
261
+ return triageProjectionStatusRows(rows);
262
+ }
263
+ /** Full roster read. The main board applies triage separately; this surface never caps stored projections. */
264
+ export async function readProjectionRosterStatusBoard(cwd, nowMs) {
265
+ const coordinate = await resolveProjectionCoordinate(cwd);
266
+ const topology = readProjectionRootTopology(coordinate);
267
+ if (!topology)
268
+ return null;
269
+ const aliases = await readProjectionStatusAliases(projectionAliasRootAtCoordinate(coordinate));
270
+ const summaries = [];
271
+ for (const akuma of topology.akumas) {
272
+ if (akuma.startsWith("."))
273
+ continue;
274
+ const scoped = readAkumaProjectionStatusSummaries(topology.root, akuma, aliases, nowMs);
275
+ if (scoped === null)
276
+ return null;
277
+ summaries.push(...scoped);
278
+ }
279
+ return {
280
+ rows: summaries
281
+ .map((summary) => readProjectionRow(summary, nowMs, requiresProjectionStatusDetails(summary.state)))
282
+ .sort(compareProjectionStatusRows),
283
+ };
165
284
  }
166
285
  export async function readExactProjectionStatus(cwd, nowMs, projectionId) {
167
286
  if (!isProjectionId(projectionId))
@@ -191,6 +310,36 @@ export async function readExactProjectionStatus(cwd, nowMs, projectionId) {
191
310
  const aliases = await readProjectionStatusAliases(projectionAliasRootAtCoordinate(coordinate));
192
311
  return readProjectionRow(readProjectionSummary(dir, projectionId, aliases.get(projectionId), nowMs), nowMs, true);
193
312
  }
313
+ /** Shared bounded selector for human and structured status consumers. */
314
+ export async function selectProjectionStatus(cwd, nowMs, input) {
315
+ if (input.kind === "operational")
316
+ return { input, board: await readOperationalProjectionStatusBoard(cwd, nowMs) };
317
+ const coordinate = await resolveProjectionCoordinate(cwd);
318
+ const canonicalId = isProjectionId(input.token)
319
+ ? input.token
320
+ : await readProjectionAliasAtRoot(projectionAliasRootAtCoordinate(coordinate), input.token);
321
+ if (!canonicalId)
322
+ return { input, board: null };
323
+ const row = await readExactProjectionStatus(cwd, nowMs, canonicalId);
324
+ if (!row)
325
+ return { input, board: null };
326
+ const aliasMovedTo = input.token === canonicalId
327
+ ? undefined
328
+ : await readProjectionAliasAtRoot(projectionAliasRootAtCoordinate(coordinate), input.token);
329
+ return {
330
+ input,
331
+ board: {
332
+ rows: [{
333
+ ...row,
334
+ addressedBy: {
335
+ token: input.token,
336
+ kind: input.token === canonicalId ? "canonical" : "alias",
337
+ ...(aliasMovedTo && aliasMovedTo !== canonicalId ? { aliasMovedTo } : {}),
338
+ },
339
+ }],
340
+ },
341
+ };
342
+ }
194
343
  export async function readAkumaNonterminalProjectionStatus(cwd, nowMs, akuma) {
195
344
  if (!isProjectionId(`${akuma}/00000000`))
196
345
  return null;
@@ -278,14 +427,12 @@ export async function hasStrandableProjectionGeneration(input) {
278
427
  }
279
428
  const observation = observeProjectionLifeSnapshot(projectionDir, {
280
429
  nowMs: input.nowMs,
281
- startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
282
430
  });
283
431
  if (observation.compatibility !== "current")
284
432
  continue;
285
433
  const phase = observation.life.phase;
286
434
  if (phase === "active"
287
435
  || phase === "minting"
288
- || phase === "startup-timeout"
289
436
  || phase === "unknown") {
290
437
  return true;
291
438
  }
@@ -6,9 +6,7 @@ import { resolveProjectionCoordinate } from "./projection-coordinate.js";
6
6
  import { readProjectionIdentity } from "./projection-identity.js";
7
7
  import { RESPONSE_ARTIFACT_ID_RE } from "../response-artifact-id.js";
8
8
  import { extractTerminalTimestamp } from "./generation/model.js";
9
- import { PROJECTION_ADOPTION_TIMEOUT_MS } from "./generation/projection-generation-launcher.js";
10
9
  import { observeProjectionLifeSnapshot } from "./projection-life-observer.js";
11
- import { deriveProjectionLife } from "./projection-life-protocol.js";
12
10
  import { readProjectionGenerationContinuationSnapshot, } from "./generation/projection-generation-continuation.js";
13
11
  import { projectTerminalFailure, } from "./projection-terminal-failure.js";
14
12
  import { invalidActivityEventDiagnostic, selectValidStoredAgentEvents, } from "../stored-agent-event.js";
@@ -179,26 +177,16 @@ export async function waitForProjection(input) {
179
177
  const observedAtMs = now();
180
178
  const continuation = readContinuationSnapshot(input.projectionDirectory, 10);
181
179
  const current = continuation.current;
182
- let life = deriveProjectionLife({
183
- records: continuation.records,
180
+ const observation = observeProjectionLifeSnapshot(input.projectionDirectory, {
184
181
  nowMs: observedAtMs,
185
- startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
186
182
  });
187
- if (current && !current.verdict && current.adoption) {
188
- const observation = observeProjectionLifeSnapshot(input.projectionDirectory, {
189
- nowMs: observedAtMs,
190
- startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
191
- });
192
- if (observation.compatibility === "incompatible") {
193
- life = { phase: "unknown", diagnostic: observation.diagnostic };
194
- }
195
- else if ((observation.records?.at(-1)?.seq ?? 0) !== continuation.generationSequence) {
196
- continue;
197
- }
198
- else {
199
- life = observation.life;
200
- }
183
+ if (observation.compatibility === "incompatible") {
184
+ return { state: "unknown", diagnostic: observation.diagnostic };
201
185
  }
186
+ if ((observation.records?.at(-1)?.seq ?? 0) !== continuation.generationSequence) {
187
+ continue;
188
+ }
189
+ const life = observation.life;
202
190
  if (!wakeAttempted
203
191
  && current
204
192
  && life.phase !== "active"
@@ -223,9 +211,12 @@ export async function waitForProjection(input) {
223
211
  const verdict = current ? verdictFromCurrent(current, observedAtMs) : undefined;
224
212
  const terminal = verdict !== undefined;
225
213
  const terminalAtMs = verdict === undefined ? undefined : Date.parse(verdict.at);
226
- const terminalWithinDeadline = terminal && (input.deadlineMs === undefined
227
- || (typeof terminalAtMs === "number" && Number.isFinite(terminalAtMs) && terminalAtMs < input.deadlineMs));
228
- if (terminalWithinDeadline || (deadline !== undefined && observedAtMs >= deadline)) {
214
+ const terminalWithinDeadline = terminal && (deadline === undefined
215
+ || (typeof terminalAtMs === "number" && Number.isFinite(terminalAtMs) && terminalAtMs < deadline));
216
+ const runnerDiedWithoutVerdict = verdict === undefined && life.phase === "lost";
217
+ if (terminalWithinDeadline
218
+ || runnerDiedWithoutVerdict
219
+ || (deadline !== undefined && observedAtMs >= deadline)) {
229
220
  const revalidated = readContinuationSnapshot(input.projectionDirectory, 10);
230
221
  if (revalidated.generationSequence !== continuation.generationSequence)
231
222
  continue;
@@ -253,6 +244,9 @@ export async function waitForProjection(input) {
253
244
  ...(text ? { outcomeText: text } : {}),
254
245
  };
255
246
  }
247
+ if (runnerDiedWithoutVerdict) {
248
+ return { state: "lost", pact, snapshot };
249
+ }
256
250
  return {
257
251
  state: "timeout",
258
252
  pact,
@@ -276,7 +270,9 @@ export async function waitForProjectionSet(input) {
276
270
  if (input.signal?.aborted)
277
271
  abort();
278
272
  input.signal?.addEventListener("abort", abort, { once: true });
279
- const members = [...input.projections].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
273
+ // Admission order is provenance. Presentation applies its own operational
274
+ // triage ordering after this fixed set has been observed.
275
+ const members = [...input.projections];
280
276
  const now = input.now ?? Date.now;
281
277
  const deadlineMs = input.timeoutMs === undefined ? undefined : now() + input.timeoutMs;
282
278
  try {
@@ -285,7 +281,13 @@ export async function waitForProjectionSet(input) {
285
281
  ...(deadlineMs === undefined ? {} : { deadlineMs }),
286
282
  now,
287
283
  signal: controller.signal,
288
- }).then((result) => ({ index, id: member.id, result }), (error) => ({ index, id: member.id, error })));
284
+ }).then((result) => ({ index, member, result }), (error) => ({ index, member, error })));
285
+ const memberResult = (member, result) => ({
286
+ id: member.id,
287
+ ...(member.inputToken ? { inputToken: member.inputToken } : {}),
288
+ ...(member.alias ? { alias: member.alias } : {}),
289
+ result,
290
+ });
289
291
  if (input.selection === "all") {
290
292
  const settled = await Promise.all(waits);
291
293
  for (const item of settled) {
@@ -294,23 +296,40 @@ export async function waitForProjectionSet(input) {
294
296
  }
295
297
  return {
296
298
  selection: "all",
297
- results: settled.map((item) => ({ id: item.id, result: item.result })),
299
+ results: settled.map((item) => {
300
+ const value = item;
301
+ return memberResult(value.member, value.result);
302
+ }),
298
303
  };
299
304
  }
300
305
  const pending = new Set(waits.keys());
301
- const observed = [];
306
+ const observed = new Map();
302
307
  while (pending.size > 0) {
303
308
  const next = await Promise.race([...pending].map((index) => waits[index]));
304
309
  pending.delete(next.index);
305
310
  if ("error" in next)
306
311
  throw next.error;
307
312
  if (next.result.state === "terminal") {
313
+ observed.set(next.index, next.result);
308
314
  controller.abort();
309
- return { selection: "any", selectedId: next.id, results: [{ id: next.id, result: next.result }] };
315
+ // A winner ends the waiting condition, not the admitted read set. Let
316
+ // in-flight readers unwind, then take one immediate read-only snapshot
317
+ // of every member under a shared cutoff.
318
+ await Promise.all(waits);
319
+ const snapshotDeadlineMs = now();
320
+ const snapshots = await Promise.all(members.map(async (member, index) => memberResult(member, observed.get(index) ?? await waitForProjection({
321
+ projectionDirectory: member.projectionDirectory,
322
+ deadlineMs: snapshotDeadlineMs,
323
+ now,
324
+ }))));
325
+ return { selection: "any", selectedId: next.member.id, results: snapshots };
310
326
  }
311
- observed.push({ id: next.id, result: next.result });
327
+ observed.set(next.index, next.result);
312
328
  }
313
- return { selection: "any", results: observed.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0) };
329
+ return {
330
+ selection: "any",
331
+ results: members.map((member, index) => memberResult(member, observed.get(index))),
332
+ };
314
333
  }
315
334
  finally {
316
335
  input.signal?.removeEventListener("abort", abort);
@@ -4,11 +4,13 @@ import { isRecoverableSessionEnvelope } from "../../agents/harness/index.js";
4
4
  import { ProjectionStateError } from "../atomic-publish.js";
5
5
  import { applyContinuationEffort, readProjectionGenerationContinuationSnapshot, } from "./generation/projection-generation-continuation.js";
6
6
  import { createProjectionExecutionId } from "./generation/projection-generation-identity.js";
7
- import { PROJECTION_ADOPTION_POLL_MS, PROJECTION_ADOPTION_TIMEOUT_MS, startProjectionGeneration, } from "./generation/projection-generation-launcher.js";
7
+ import { startProjectionGeneration } from "./generation/projection-generation-launcher.js";
8
8
  import { openProjectionGenerationStore, openProjectionGenerationStoreReadOnly, } from "./generation/store.js";
9
9
  import { observeProjectionLifeSnapshot } from "./projection-life-observer.js";
10
- import { acquireProjectionRunnerLock, acquireProjectionRunnerSettlementLock, ProjectionRunnerLockUnavailableError, } from "./projection-runner-lock.js";
10
+ import { acquireRunnerLease, acquireRunnerLostProof, RunnerLeaseUnavailableError, } from "../run-control/runner-lease.js";
11
+ import { projectionRunnerLeasePath } from "./generation/projection-generation-identity.js";
11
12
  import { setTimeout as delay } from "node:timers/promises";
13
+ const PROJECTION_WAKE_POLL_MS = 20;
12
14
  function incompatibleProjectionError(projectionId) {
13
15
  const subject = projectionId ? `Projection '${projectionId}'` : "Projection";
14
16
  const statusCoordinate = projectionId ?? "<projection-id>";
@@ -21,7 +23,6 @@ function incompatibleProjectionError(projectionId) {
21
23
  export function assertProjectionTellable(projectionDirectory) {
22
24
  const observation = observeProjectionLifeSnapshot(projectionDirectory, {
23
25
  nowMs: Date.now(),
24
- startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
25
26
  });
26
27
  if (observation.compatibility === "incompatible")
27
28
  throw incompatibleProjectionError();
@@ -165,12 +166,10 @@ export function persistInterruptWakeFailure(projectionDirectory, error, nowMs =
165
166
  export async function tellProjection(projectionDirectory, projectionId, effort, dependencies = {}) {
166
167
  const startGeneration = dependencies.startGeneration ?? startProjectionGeneration;
167
168
  const sleep = dependencies.sleep ?? ((milliseconds) => delay(milliseconds));
168
- const observationStartedAt = Date.now();
169
169
  for (;;) {
170
170
  const nowMs = Date.now();
171
171
  const observation = observeProjectionLifeSnapshot(projectionDirectory, {
172
172
  nowMs,
173
- startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
174
173
  });
175
174
  if (observation.compatibility === "incompatible") {
176
175
  throw incompatibleProjectionError(projectionId);
@@ -179,10 +178,7 @@ export async function tellProjection(projectionDirectory, projectionId, effort,
179
178
  if (life.phase === "active")
180
179
  return "active";
181
180
  if (life.phase === "minting" || life.phase === "quiescent") {
182
- if (life.phase === "quiescent" && nowMs - observationStartedAt >= PROJECTION_ADOPTION_TIMEOUT_MS) {
183
- throw sessionWakeFailure(projectionId, new Error("projection has no generation continuation"));
184
- }
185
- await sleep(PROJECTION_ADOPTION_POLL_MS);
181
+ await sleep(PROJECTION_WAKE_POLL_MS);
186
182
  continue;
187
183
  }
188
184
  if (life.phase === "unknown") {
@@ -200,7 +196,7 @@ export async function tellProjection(projectionDirectory, projectionId, effort,
200
196
  const successorExecutionId = createProjectionExecutionId(nowMs);
201
197
  let facts;
202
198
  try {
203
- facts = successorFacts(current, { ...snapshot, ...(effort ? { effort } : {}) }, nowMs, life.phase !== "startup-timeout");
199
+ facts = successorFacts(current, { ...snapshot, ...(effort ? { effort } : {}) }, nowMs, current.adoption !== undefined);
204
200
  }
205
201
  catch (error) {
206
202
  if (error instanceof FlowError && error.code === "PROJECTION_WAKE_FAILED") {
@@ -210,79 +206,13 @@ export async function tellProjection(projectionDirectory, projectionId, effort,
210
206
  }
211
207
  const store = openProjectionGenerationStore(projectionDirectory);
212
208
  try {
213
- if (life.phase === "startup-timeout") {
214
- const terminalAt = new Date(nowMs).toISOString();
215
- let deathEvidence;
216
- try {
217
- deathEvidence = acquireProjectionRunnerSettlementLock(projectionDirectory);
218
- }
219
- catch (error) {
220
- if (error instanceof ProjectionRunnerLockUnavailableError)
221
- return "pending";
222
- return "pending";
223
- }
224
- try {
225
- store.beginWriteTransaction();
226
- const protectedCurrent = store.readCurrentGeneration();
227
- if (protectedCurrent?.launch.executionId !== current.launch.executionId
228
- || protectedCurrent.adoption
229
- || protectedCurrent.verdict) {
230
- store.rollbackWriteTransaction();
231
- return "pending";
232
- }
233
- if (protectedCurrent.killIntent) {
234
- const settled = store.settleKillIfRunnerDead({
235
- executionId: current.launch.executionId,
236
- releasedLockEvidence: { observedAt: terminalAt, state: "acquired" },
237
- });
238
- if (settled.status === "committed")
239
- store.commitWriteTransaction();
240
- else
241
- store.rollbackWriteTransaction();
242
- return "pending";
243
- }
244
- deathEvidence.close();
245
- let launchOwnership;
246
- try {
247
- launchOwnership = acquireProjectionRunnerLock(projectionDirectory);
248
- }
249
- catch (error) {
250
- store.rollbackWriteTransaction();
251
- if (error instanceof ProjectionRunnerLockUnavailableError)
252
- return "pending";
253
- return "pending";
254
- }
255
- return await startCommittedSuccessor(projectionDirectory, projectionId, successorExecutionId, startGeneration, {
256
- commitLaunch: () => {
257
- const transition = store.supersedeStartupTimeout({
258
- executionId: current.launch.executionId,
259
- releasedLockEvidence: { observedAt: terminalAt, state: "acquired" },
260
- diagnostic: { detail: "runner did not adopt before startup timeout" },
261
- successorExecutionId,
262
- successorFacts: facts,
263
- terminalAt,
264
- });
265
- if (transition.status === "committed")
266
- store.commitWriteTransaction();
267
- else
268
- store.rollbackWriteTransaction();
269
- return transition.status === "committed";
270
- },
271
- launchOwnership,
272
- });
273
- }
274
- finally {
275
- store.rollbackWriteTransaction();
276
- deathEvidence.close();
277
- }
278
- }
279
209
  if (life.phase === "lost" && !current.verdict) {
280
210
  let deathEvidence;
281
211
  try {
282
- deathEvidence = acquireProjectionRunnerSettlementLock(projectionDirectory);
212
+ deathEvidence = acquireRunnerLostProof(projectionRunnerLeasePath(projectionDirectory));
283
213
  }
284
214
  catch (error) {
285
- if (error instanceof ProjectionRunnerLockUnavailableError)
215
+ if (error instanceof RunnerLeaseUnavailableError)
286
216
  return "active";
287
217
  return "pending";
288
218
  }
@@ -290,7 +220,6 @@ export async function tellProjection(projectionDirectory, projectionId, effort,
290
220
  store.beginWriteTransaction();
291
221
  const protectedCurrent = store.readCurrentGeneration();
292
222
  if (protectedCurrent?.launch.executionId !== current.launch.executionId
293
- || !protectedCurrent.adoption
294
223
  || protectedCurrent.verdict) {
295
224
  store.rollbackWriteTransaction();
296
225
  return "pending";
@@ -300,6 +229,7 @@ export async function tellProjection(projectionDirectory, projectionId, effort,
300
229
  const settled = store.settleKillIfRunnerDead({
301
230
  executionId: current.launch.executionId,
302
231
  releasedLockEvidence: { observedAt: terminalAt, state: "acquired" },
232
+ terminalAt,
303
233
  });
304
234
  if (settled.status === "committed")
305
235
  store.commitWriteTransaction();
@@ -307,26 +237,36 @@ export async function tellProjection(projectionDirectory, projectionId, effort,
307
237
  store.rollbackWriteTransaction();
308
238
  return "pending";
309
239
  }
240
+ const currentWasAdopted = protectedCurrent.adoption !== undefined;
310
241
  deathEvidence.close();
311
242
  let launchOwnership;
312
243
  try {
313
- launchOwnership = acquireProjectionRunnerLock(projectionDirectory);
244
+ launchOwnership = acquireRunnerLease(projectionRunnerLeasePath(projectionDirectory));
314
245
  }
315
246
  catch (error) {
316
247
  store.rollbackWriteTransaction();
317
- if (error instanceof ProjectionRunnerLockUnavailableError)
248
+ if (error instanceof RunnerLeaseUnavailableError)
318
249
  return "active";
319
250
  return "pending";
320
251
  }
321
252
  return await startCommittedSuccessor(projectionDirectory, projectionId, successorExecutionId, startGeneration, {
322
253
  commitLaunch: () => {
323
- const transition = store.replaceDeadRunner({
324
- executionId: current.launch.executionId,
325
- releasedLockEvidence: { observedAt: terminalAt, state: "acquired" },
326
- successorExecutionId,
327
- successorFacts: facts,
328
- terminalAt,
329
- });
254
+ const transition = currentWasAdopted
255
+ ? store.replaceDeadRunner({
256
+ executionId: current.launch.executionId,
257
+ releasedLockEvidence: { observedAt: terminalAt, state: "acquired" },
258
+ successorExecutionId,
259
+ successorFacts: facts,
260
+ terminalAt,
261
+ })
262
+ : store.supersedeStartupTimeout({
263
+ executionId: current.launch.executionId,
264
+ releasedLockEvidence: { observedAt: terminalAt, state: "acquired" },
265
+ diagnostic: { detail: "startup owner died before adoption" },
266
+ successorExecutionId,
267
+ successorFacts: facts,
268
+ terminalAt,
269
+ });
330
270
  if (transition.status === "committed")
331
271
  store.commitWriteTransaction();
332
272
  else
@@ -1,5 +1,5 @@
1
1
  import { ProjectionStateError } from "../../atomic-publish.js";
2
- import { withRead, withWrite } from "./database.js";
2
+ import { withProjectionHeartRead as withRead, withProjectionHeartWrite as withWrite, } from "../generation/database.js";
3
3
  import { parseFence, requireExecutionId, submittedFromRow } from "./model.js";
4
4
  function selectTell(database, tellId) {
5
5
  return database.prepare(`
@@ -1,5 +1,5 @@
1
1
  import { ProjectionStateError, randomHex8 } from "../../atomic-publish.js";
2
- import { withRead, withReadSnapshot, withWrite } from "./database.js";
2
+ import { withProjectionHeartRead as withRead, withProjectionHeartReadSnapshot as withReadSnapshot, withProjectionHeartWrite as withWrite, } from "../generation/database.js";
3
3
  import { assertTellIdRandomHex, originalFromRow, PENDING_PROJECTION_TELL_STATES, parseFence, requireExecutionId, submittedFromRow, } from "./model.js";
4
4
  export { bindTellLaunchReservation, claimAndReserveFencedTells, claimFencedTells, isTellLaunchAdmissionMember, listTellLaunchAdmissions, listTellLaunchBatch, markTellSubmitted, } from "./launch-store.js";
5
5
  function selectTell(database, tellId) {