@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
@@ -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"
@@ -225,7 +213,10 @@ export async function waitForProjection(input) {
225
213
  const terminalAtMs = verdict === undefined ? undefined : Date.parse(verdict.at);
226
214
  const terminalWithinDeadline = terminal && (deadline === undefined
227
215
  || (typeof terminalAtMs === "number" && Number.isFinite(terminalAtMs) && terminalAtMs < deadline));
228
- if (terminalWithinDeadline || (deadline !== undefined && observedAtMs >= 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,80 +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
- terminalAt,
238
- });
239
- if (settled.status === "committed")
240
- store.commitWriteTransaction();
241
- else
242
- store.rollbackWriteTransaction();
243
- return "pending";
244
- }
245
- deathEvidence.close();
246
- let launchOwnership;
247
- try {
248
- launchOwnership = acquireProjectionRunnerLock(projectionDirectory);
249
- }
250
- catch (error) {
251
- store.rollbackWriteTransaction();
252
- if (error instanceof ProjectionRunnerLockUnavailableError)
253
- return "pending";
254
- return "pending";
255
- }
256
- return await startCommittedSuccessor(projectionDirectory, projectionId, successorExecutionId, startGeneration, {
257
- commitLaunch: () => {
258
- const transition = store.supersedeStartupTimeout({
259
- executionId: current.launch.executionId,
260
- releasedLockEvidence: { observedAt: terminalAt, state: "acquired" },
261
- diagnostic: { detail: "runner did not adopt before startup timeout" },
262
- successorExecutionId,
263
- successorFacts: facts,
264
- terminalAt,
265
- });
266
- if (transition.status === "committed")
267
- store.commitWriteTransaction();
268
- else
269
- store.rollbackWriteTransaction();
270
- return transition.status === "committed";
271
- },
272
- launchOwnership,
273
- });
274
- }
275
- finally {
276
- store.rollbackWriteTransaction();
277
- deathEvidence.close();
278
- }
279
- }
280
209
  if (life.phase === "lost" && !current.verdict) {
281
210
  let deathEvidence;
282
211
  try {
283
- deathEvidence = acquireProjectionRunnerSettlementLock(projectionDirectory);
212
+ deathEvidence = acquireRunnerLostProof(projectionRunnerLeasePath(projectionDirectory));
284
213
  }
285
214
  catch (error) {
286
- if (error instanceof ProjectionRunnerLockUnavailableError)
215
+ if (error instanceof RunnerLeaseUnavailableError)
287
216
  return "active";
288
217
  return "pending";
289
218
  }
@@ -291,7 +220,6 @@ export async function tellProjection(projectionDirectory, projectionId, effort,
291
220
  store.beginWriteTransaction();
292
221
  const protectedCurrent = store.readCurrentGeneration();
293
222
  if (protectedCurrent?.launch.executionId !== current.launch.executionId
294
- || !protectedCurrent.adoption
295
223
  || protectedCurrent.verdict) {
296
224
  store.rollbackWriteTransaction();
297
225
  return "pending";
@@ -309,26 +237,36 @@ export async function tellProjection(projectionDirectory, projectionId, effort,
309
237
  store.rollbackWriteTransaction();
310
238
  return "pending";
311
239
  }
240
+ const currentWasAdopted = protectedCurrent.adoption !== undefined;
312
241
  deathEvidence.close();
313
242
  let launchOwnership;
314
243
  try {
315
- launchOwnership = acquireProjectionRunnerLock(projectionDirectory);
244
+ launchOwnership = acquireRunnerLease(projectionRunnerLeasePath(projectionDirectory));
316
245
  }
317
246
  catch (error) {
318
247
  store.rollbackWriteTransaction();
319
- if (error instanceof ProjectionRunnerLockUnavailableError)
248
+ if (error instanceof RunnerLeaseUnavailableError)
320
249
  return "active";
321
250
  return "pending";
322
251
  }
323
252
  return await startCommittedSuccessor(projectionDirectory, projectionId, successorExecutionId, startGeneration, {
324
253
  commitLaunch: () => {
325
- const transition = store.replaceDeadRunner({
326
- executionId: current.launch.executionId,
327
- releasedLockEvidence: { observedAt: terminalAt, state: "acquired" },
328
- successorExecutionId,
329
- successorFacts: facts,
330
- terminalAt,
331
- });
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
+ });
332
270
  if (transition.status === "committed")
333
271
  store.commitWriteTransaction();
334
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) {