@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
@@ -10,7 +10,7 @@ export function launchIfSettled(database, readOnly, input) {
10
10
  const executionId = assertExecutionId(input.executionId);
11
11
  const facts = validateLaunchFacts(input.facts, "launch facts");
12
12
  return writeTransition(database, readOnly, () => {
13
- const history = readValidatedHistory(database);
13
+ const history = readValidatedHistory(database, readOnly);
14
14
  if (history.records.some((record) => record.kind === "launch" && record.executionId === executionId)) {
15
15
  return rejected("execution-id-already-used", history.current);
16
16
  }
@@ -27,7 +27,7 @@ export function failInterruptWakeIfSettled(database, readOnly, input) {
27
27
  const successorFacts = validateLaunchFacts(input.successorFacts, "interrupt wake successor launch facts");
28
28
  const terminalAt = assertIsoTimestamp(input.terminalAt, "terminalAt");
29
29
  return writeTransition(database, readOnly, () => {
30
- const history = readValidatedHistory(database);
30
+ const history = readValidatedHistory(database, readOnly);
31
31
  const current = history.current;
32
32
  if (!current)
33
33
  return rejected("no-current-launch", null);
@@ -56,7 +56,7 @@ export function adoptionIfCurrent(database, readOnly, input) {
56
56
  const executionId = assertExecutionId(input.executionId);
57
57
  const facts = validateAdoptionFacts(input.facts, "adoption facts");
58
58
  return writeTransition(database, readOnly, () => {
59
- const history = readValidatedHistory(database);
59
+ const history = readValidatedHistory(database, readOnly);
60
60
  const current = history.current;
61
61
  if (!current)
62
62
  return rejected("no-current-launch", null);
@@ -73,7 +73,7 @@ export function requestKillIfOpen(database, readOnly, input) {
73
73
  const executionId = assertExecutionId(input.executionId);
74
74
  const killedAt = assertIsoTimestamp(input.killedAt, "killedAt");
75
75
  return writeTransition(database, readOnly, () => {
76
- const history = readValidatedHistory(database);
76
+ const history = readValidatedHistory(database, readOnly);
77
77
  const current = history.current;
78
78
  if (!current)
79
79
  return rejected("no-current-launch", null);
@@ -93,7 +93,7 @@ export function requestInterruptIfOpen(database, readOnly, input) {
93
93
  const executionId = assertExecutionId(input.executionId);
94
94
  const interruptedAt = assertIsoTimestamp(input.interruptedAt, "interruptedAt");
95
95
  return writeTransition(database, readOnly, () => {
96
- const history = readValidatedHistory(database);
96
+ const history = readValidatedHistory(database, readOnly);
97
97
  const current = history.current;
98
98
  if (!current)
99
99
  return rejected("no-current-launch", null);
@@ -111,6 +111,7 @@ export function requestInterruptIfOpen(database, readOnly, input) {
111
111
  }
112
112
  export function runnerKilledIfRequested(database, readOnly, input) {
113
113
  const executionId = assertExecutionId(input.executionId);
114
+ const terminalAt = assertIsoTimestamp(input.terminalAt, "terminalAt");
114
115
  const callerFacts = cloneFacts(input.facts ?? {}, "killed verdict facts");
115
116
  if ("state" in callerFacts) {
116
117
  throw new ProjectionGenerationStoreError("killed verdict facts.state is reserved");
@@ -121,8 +122,11 @@ export function runnerKilledIfRequested(database, readOnly, input) {
121
122
  if ("killedAt" in callerFacts) {
122
123
  throw new ProjectionGenerationStoreError("killed verdict facts.killedAt is reserved");
123
124
  }
125
+ if ("terminalAt" in callerFacts) {
126
+ throw new ProjectionGenerationStoreError("killed verdict facts.terminalAt is reserved");
127
+ }
124
128
  return writeTransition(database, readOnly, () => {
125
- const history = readValidatedHistory(database);
129
+ const history = readValidatedHistory(database, readOnly);
126
130
  const current = history.current;
127
131
  if (!current)
128
132
  return rejected("no-current-launch", null);
@@ -137,6 +141,7 @@ export function runnerKilledIfRequested(database, readOnly, input) {
137
141
  state: "killed",
138
142
  killIntentSeq: current.killIntent.seq,
139
143
  killedAt: current.killIntent.facts.killedAt,
144
+ terminalAt,
140
145
  }, "killed verdict facts");
141
146
  return committed([appendRecord(database, "verdict", executionId, facts, history.records.length + 1)]);
142
147
  });
@@ -153,7 +158,7 @@ export function runnerInterruptedIfRequested(database, readOnly, input) {
153
158
  ? undefined
154
159
  : assertIsoTimestamp(input.terminalAt, "terminalAt");
155
160
  return writeTransition(database, readOnly, () => {
156
- const history = readValidatedHistory(database);
161
+ const history = readValidatedHistory(database, readOnly);
157
162
  const current = history.current;
158
163
  if (!current)
159
164
  return rejected("no-current-launch", null);
@@ -184,7 +189,7 @@ export function runnerVerdictIfOpen(database, readOnly, input) {
184
189
  const callerFacts = cloneFacts(input.facts ?? {}, "runner verdict facts");
185
190
  const terminalAt = assertIsoTimestamp(input.terminalAt, "terminalAt");
186
191
  return writeTransition(database, readOnly, () => {
187
- const history = readValidatedHistory(database);
192
+ const history = readValidatedHistory(database, readOnly);
188
193
  const current = history.current;
189
194
  if (!current)
190
195
  return rejected("no-current-launch", null);
@@ -221,6 +226,7 @@ export function runnerVerdictIfOpen(database, readOnly, input) {
221
226
  }
222
227
  export function settleKillIfRunnerDead(database, readOnly, input) {
223
228
  const executionId = assertExecutionId(input.executionId);
229
+ const terminalAt = assertIsoTimestamp(input.terminalAt, "terminalAt");
224
230
  const releasedLockEvidence = cloneFacts(input.releasedLockEvidence, "released runner-lock evidence");
225
231
  if (Object.keys(releasedLockEvidence).length === 0) {
226
232
  throw new ProjectionGenerationStoreError("released runner-lock evidence must not be empty");
@@ -235,8 +241,11 @@ export function settleKillIfRunnerDead(database, readOnly, input) {
235
241
  if ("killedAt" in callerFacts) {
236
242
  throw new ProjectionGenerationStoreError("killed verdict facts.killedAt is reserved");
237
243
  }
244
+ if ("terminalAt" in callerFacts) {
245
+ throw new ProjectionGenerationStoreError("killed verdict facts.terminalAt is reserved");
246
+ }
238
247
  return writeTransition(database, readOnly, () => {
239
- const history = readValidatedHistory(database);
248
+ const history = readValidatedHistory(database, readOnly);
240
249
  const current = history.current;
241
250
  if (!current)
242
251
  return rejected("no-current-launch", null);
@@ -251,6 +260,7 @@ export function settleKillIfRunnerDead(database, readOnly, input) {
251
260
  state: "killed",
252
261
  killIntentSeq: current.killIntent.seq,
253
262
  killedAt: current.killIntent.facts.killedAt,
263
+ terminalAt,
254
264
  releasedLockEvidence,
255
265
  }, "killed verdict facts");
256
266
  return committed([appendRecord(database, "verdict", executionId, facts, history.records.length + 1)]);
@@ -264,7 +274,7 @@ export function verdictIfOpen(database, readOnly, input) {
264
274
  throw new ProjectionGenerationStoreError(`verdict '${input.verdict}' is reserved to its runner-owned transition`);
265
275
  }
266
276
  return writeTransition(database, readOnly, () => {
267
- const history = readValidatedHistory(database);
277
+ const history = readValidatedHistory(database, readOnly);
268
278
  const current = history.current;
269
279
  if (!current)
270
280
  return rejected("no-current-launch", null);
@@ -298,7 +308,7 @@ export function supersedeStartupTimeout(database, readOnly, input) {
298
308
  ? undefined
299
309
  : assertIsoTimestamp(input.terminalAt, "terminalAt");
300
310
  return writeTransition(database, readOnly, () => {
301
- const history = readValidatedHistory(database);
311
+ const history = readValidatedHistory(database, readOnly);
302
312
  const current = history.current;
303
313
  if (!current)
304
314
  return rejected("no-current-launch", null);
@@ -346,7 +356,7 @@ export function settleStartupFailure(database, readOnly, input) {
346
356
  ? undefined
347
357
  : assertIsoTimestamp(input.terminalAt, "terminalAt");
348
358
  return writeTransition(database, readOnly, () => {
349
- const history = readValidatedHistory(database);
359
+ const history = readValidatedHistory(database, readOnly);
350
360
  const current = history.current;
351
361
  if (!current)
352
362
  return rejected("no-current-launch", null);
@@ -385,7 +395,7 @@ export function replaceDeadRunner(database, readOnly, input) {
385
395
  ? undefined
386
396
  : assertIsoTimestamp(input.terminalAt, "terminalAt");
387
397
  return writeTransition(database, readOnly, () => {
388
- const history = readValidatedHistory(database);
398
+ const history = readValidatedHistory(database, readOnly);
389
399
  const current = history.current;
390
400
  if (!current)
391
401
  return rejected("no-current-launch", null);
@@ -426,7 +436,7 @@ export function handoff(database, readOnly, input) {
426
436
  const tellFence = validateTellFence([...input.tellFence], "handoff tellFence");
427
437
  const successorFacts = validateLaunchFacts({ ...successorFactsBase, tellFence }, "handoff successor launch facts");
428
438
  return writeTransition(database, readOnly, () => {
429
- const history = readValidatedHistory(database);
439
+ const history = readValidatedHistory(database, readOnly);
430
440
  const current = history.current;
431
441
  if (!current)
432
442
  return rejected("no-current-launch", null);
@@ -10,18 +10,18 @@ export { resolveProjectionCoordinate } from "./projection-coordinate.js";
10
10
  export { appendGenerationEvent, executionEventsPath, isProjectionId, isPublishedProjection, isoFromNowMs, projectionDir, projectionDirAtCoordinate, projectionRoot, readProjectionEvents, readGenerationEventsWithRetention, } from "./projection-core.js";
11
11
  export { readProjectionIdentity } from "./projection-identity.js";
12
12
  export { interruptProjectionGeneration, killProjectionGeneration } from "./projection-kill.js";
13
+ export { openProjectionGenerationDoorbell, projectionGenerationDoorbellAddress, ringProjectionGenerationDoorbell, } from "./generation/projection-generation-doorbell.js";
13
14
  export { observeProjectionLife, observeProjectionLifeSnapshot, } from "./projection-life-observer.js";
14
15
  export { mintProjection, mintRepositoryProjection, ProjectionMintCollisionError, } from "./projection-mint.js";
15
- export { acquireProjectionRunnerLock, acquireProjectionRunnerSettlementLock, acquireProjectionRunnerTransferTarget, initializeProjectionRunnerLockDatabase, isProjectionRunnerLockPlatformSupported, observeProjectionRunnerLock, projectionRunnerLockPath, } from "./projection-runner-lock.js";
16
- export { hasStrandableProjectionGeneration, prioritizeProjectionStatusRows, readAkumaNonterminalProjectionStatus, readExactProjectionStatus, readOperationalProjectionStatusBoard, } from "./projection-status.js";
16
+ export { hasStrandableProjectionGeneration, prioritizeProjectionStatusRows, paginateProjectionStatusRoster, triageProjectionStatusRows, readAkumaNonterminalProjectionStatus, readExactProjectionStatus, readOperationalProjectionStatusBoard, readProjectionRosterStatusBoard, selectProjectionStatus, } from "./projection-status.js";
17
17
  export { PROJECTION_HISTORY_FILTER_COMPLETION, PROJECTION_HISTORY_FILTER_FIELDS, PROJECTION_HISTORY_FILTER_GRAMMAR, parseProjectionHistoryFilter, parseProjectionHistorySort, readProjectionHistoryAddress, readExactProjectionHistory, readProjectionHistory, resolveProjectionHistoryAddress, selectProjectionHistory, } from "./projection-history.js";
18
18
  export { projectTerminalFailure, } from "./projection-terminal-failure.js";
19
19
  export { snapshotCommissionProjectionIds, snapshotCommissionProjectionIdsAtCoordinate, waitForProjection, waitForProjectionSet, } from "./projection-wait.js";
20
20
  export { assertTellEffortSupported, assertProjectionTellable, tellProjection, } from "./projection-wake.js";
21
21
  export { openProjectionGenerationStore, openProjectionGenerationStoreReadOnly, ProjectionGenerationStore, } from "./generation/store.js";
22
- export { createProjectionExecutionId } from "./generation/projection-generation-identity.js";
23
- export { PROJECTION_ADOPTION_TIMEOUT_MS, startProjectionGeneration, } from "./generation/projection-generation-launcher.js";
24
- export { projectionGenerationRunnerArgv, spawnProjectionGenerationRunner, } from "./generation/projection-generation-process.js";
22
+ export { createProjectionExecutionId, projectionRunnerLeasePath, } from "./generation/projection-generation-identity.js";
23
+ export { startProjectionGeneration } from "./generation/projection-generation-launcher.js";
24
+ export { projectionGenerationRunnerArgv, prepareProjectionGenerationRunner, } from "./generation/projection-generation-process.js";
25
25
  export { runProjectionGeneration, } from "./generation/projection-generation-runner.js";
26
26
  export { executeProjectionGeneration, prepareProjectionGenerationExecution, } from "./generation/projection-generation-execution.js";
27
27
  export { runProjectionGenerationRuntime } from "./generation/projection-generation-runtime.js";
@@ -3,14 +3,18 @@ import * as path from "node:path";
3
3
  import { isErrnoException } from "../../errno.js";
4
4
  import { resolveProjectionCoordinate } from "./projection-coordinate.js";
5
5
  import { ProjectionStateError, } from "../atomic-publish.js";
6
+ import { isAkumaName } from "./akuma-name.js";
6
7
  export { atomicPublishFile, ProjectionStateError, randomHex8 } from "../atomic-publish.js";
7
8
  export { isTellCausallyConsumed, } from "./tell/model.js";
8
9
  export { bindTellLaunchReservation, claimAllInbox, claimAndReserveFencedTells, claimFencedTells, listTellLaunchBatch, listTellLaunchAdmissions, isTellLaunchAdmissionMember, claimTellToInflight, listTellIds, markTellSubmitted, recordTellReceiptCapabilities, recordTellReceipt, readTellReceiptCapabilities, readTellOriginal, readTellSubmitted, snapshotPendingTells, writeInboxTell, } from "./tell/store.js";
9
10
  /** Relative segments for `.keiyaku/projection`. */
10
11
  export const PROJECTION_ROOT_SEGMENTS = [".keiyaku", "projection"];
11
- export const PROJECTION_ID_PATTERN = /^[a-zA-Z0-9]+(?:[.-][a-zA-Z0-9]+)*\/[0-9a-f]{8}$/;
12
+ const PROJECTION_HEX_PATTERN = /^[0-9a-f]{8}$/;
12
13
  export function isProjectionId(value) {
13
- return PROJECTION_ID_PATTERN.test(value);
14
+ const slash = value.indexOf("/");
15
+ if (slash <= 0 || slash !== value.lastIndexOf("/"))
16
+ return false;
17
+ return isAkumaName(value.slice(0, slash)) && PROJECTION_HEX_PATTERN.test(value.slice(slash + 1));
14
18
  }
15
19
  /** Derive one projection storage root from already-resolved repository authority. */
16
20
  export function projectionRootAtCoordinate(coordinate) {
@@ -1,5 +1,5 @@
1
1
  import { claimAllInbox, isTellLaunchAdmissionMember, listTellLaunchAdmissions, listTellIds, markTellSubmitted, bindTellLaunchReservation, recordTellReceiptCapabilities, recordTellReceipt, readTellSubmitted, readTellOriginal, isTellCausallyConsumed, appendGenerationEvent, } from "./projection-core.js";
2
- import { openProjectionTellObserver, } from "./tell/database.js";
2
+ import { openProjectionTellObserver } from "./generation/database.js";
3
3
  const LIVE_TELL_DELIVERY_THROTTLE_MS = 200;
4
4
  export function renderProjectionTellFrame(tellId, text) {
5
5
  void tellId;
@@ -8,18 +8,17 @@ import { parseStructuredQuery, STRUCTURED_QUERY_COMPLETION, STRUCTURED_QUERY_GRA
8
8
  import { selectValidStoredAgentEvents } from "../stored-agent-event.js";
9
9
  import { extractTerminalTimestamp } from "./generation/model.js";
10
10
  import { readProjectionAlias } from "./projection-alias.js";
11
- import { foldPublicProjectionActivity } from "./projection-activity.js";
11
+ import { foldPublicProjectionActivity, } from "./projection-activity.js";
12
12
  import { isProjectionId, projectionDir, projectionRoot, readGenerationEventsWithRetention, readProjectionEvents } from "./projection-core.js";
13
13
  import { projectionCoordinateRoot } from "./projection-coordinate.js";
14
14
  import { readProjectionIdentity } from "./projection-identity.js";
15
15
  import { observeProjectionLifeSnapshot } from "./projection-life-observer.js";
16
- import { selectCurrentGeneration } from "./projection-life-protocol.js";
17
- import { PROJECTION_ADOPTION_TIMEOUT_MS } from "./generation/projection-generation-launcher.js";
16
+ import { selectCurrentGeneration } from "./generation/protocol.js";
18
17
  export const PROJECTION_HISTORY_FILTER_FIELDS = ["contract", "akuma", "role", "outcome", "created", "updated"];
19
18
  export const PROJECTION_HISTORY_FILTER_GRAMMAR = STRUCTURED_QUERY_GRAMMAR;
20
19
  export const PROJECTION_HISTORY_FILTER_COMPLETION = STRUCTURED_QUERY_COMPLETION;
21
20
  const HISTORY_FIELDS = new Set(PROJECTION_HISTORY_FILTER_FIELDS);
22
- const HISTORY_ROLES = new Set(["call", "revive", "unknown"]);
21
+ const HISTORY_ROLES = new Set(["call", "fork", "unknown"]);
23
22
  const HISTORY_OUTCOMES = new Set(["minting", "startup-timeout", "unknown", "out", "lost", "done", "killed", "dismissed", "failed", "dead", "incompatible"]);
24
23
  const RELATIVE_TIME = /^now-([1-9][0-9]*)([smhdw])$/;
25
24
  const UNIT_MS = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 };
@@ -119,7 +118,7 @@ function outcomeFromLife(phase) {
119
118
  function roleFromRecords(records) {
120
119
  const inputs = records?.find((record) => record.kind === "launch")?.facts.inputs;
121
120
  return inputs && typeof inputs === "object" && !Array.isArray(inputs) && typeof inputs.sourceArtifactId === "string"
122
- ? "revive"
121
+ ? "fork"
123
122
  : records?.some((record) => record.kind === "launch") ? "call" : "unknown";
124
123
  }
125
124
  async function artifactFromVerdict(cwd, artifactId, executionId, index) {
@@ -235,10 +234,9 @@ async function readRun(artifactRoot, directory, id, alias, artifactIndex, addres
235
234
  const diagnostics = [];
236
235
  const snapshot = observeProjectionLifeSnapshot(directory, {
237
236
  nowMs: Date.now(),
238
- startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
239
237
  });
240
238
  if (snapshot.compatibility === "incompatible") {
241
- return { id, ...(alias ? { alias } : {}), akuma: null, provider: null, pid: null, role: "unknown", contractId: null, outcome: "incompatible", mintedAt: null, terminalAt: null, generation: null, activityAt: null, artifact: null, artifacts: [], activityRetention: "incompatible", activityCount: 0, activityRange: null, telemetry: null, consequences: null, diagnostics: [snapshot.diagnostic], activity: null };
239
+ return { id, ...(alias ? { alias } : {}), akuma: null, provider: null, pid: null, role: "unknown", contractId: null, outcome: "incompatible", mintedAt: null, terminalAt: null, generation: null, activityAt: null, artifact: null, artifacts: [], activityRetention: "incompatible", activityCount: 0, activityRange: null, telemetry: null, consequences: null, diagnostics: [snapshot.diagnostic], terminalAnswer: null, activity: null };
242
240
  }
243
241
  let identity;
244
242
  try {
@@ -296,6 +294,9 @@ async function readRun(artifactRoot, directory, id, alias, artifactIndex, addres
296
294
  telemetry: telemetryFromActivity(activity),
297
295
  consequences: null,
298
296
  diagnostics,
297
+ terminalAnswer: typeof current?.verdict?.facts.finalMessage === "string"
298
+ ? { kind: "text", text: current.verdict.facts.finalMessage }
299
+ : null,
299
300
  activity,
300
301
  };
301
302
  }
@@ -384,7 +385,6 @@ async function retainedArtifactRelationships(root, artifactId, preferredAkuma) {
384
385
  return [];
385
386
  const snapshot = observeProjectionLifeSnapshot(directory, {
386
387
  nowMs: Date.now(),
387
- startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
388
388
  });
389
389
  if (snapshot.compatibility === "incompatible" || !snapshot.records)
390
390
  return [];
@@ -1,5 +1,7 @@
1
1
  import { openProjectionGenerationStore } from "./generation/store.js";
2
- import { acquireProjectionRunnerSettlementLock, observeProjectionRunnerLock, ProjectionRunnerLockUnavailableError, } from "./projection-runner-lock.js";
2
+ import { acquireRunnerLostProof, observeRunnerLease, RunnerLeaseUnavailableError, } from "../run-control/runner-lease.js";
3
+ import { projectionRunnerLeasePath } from "./generation/projection-generation-identity.js";
4
+ import { ringProjectionGenerationDoorbell } from "./generation/projection-generation-doorbell.js";
3
5
  /** The runner observes this durable intent and owns native termination. */
4
6
  export function interruptProjectionGeneration(projectionDirectory, nowMs = Date.now()) {
5
7
  const store = openProjectionGenerationStore(projectionDirectory);
@@ -11,19 +13,23 @@ export function interruptProjectionGeneration(projectionDirectory, nowMs = Date.
11
13
  if (current.verdict)
12
14
  return { status: "already-closed", executionId: current.launch.executionId };
13
15
  const executionId = current.launch.executionId;
14
- if (!current.adoption || observeProjectionRunnerLock(projectionDirectory).state !== "held") {
16
+ if (!current.adoption || observeRunnerLease(projectionRunnerLeasePath(projectionDirectory)).state !== "held") {
15
17
  return { status: "already-closed", executionId };
16
18
  }
17
19
  const request = store.requestInterruptIfOpen({
18
20
  executionId,
19
21
  interruptedAt: new Date(nowMs).toISOString(),
20
22
  });
21
- if (request.status === "committed")
23
+ if (request.status === "committed") {
24
+ ringProjectionGenerationDoorbell(store.readIdentity(), executionId);
22
25
  return { status: "accepted", executionId };
26
+ }
23
27
  if (request.reason === "not-current")
24
28
  continue;
25
- if (request.reason === "interrupt-requested")
29
+ if (request.reason === "interrupt-requested") {
30
+ ringProjectionGenerationDoorbell(store.readIdentity(), executionId);
26
31
  return { status: "accepted", executionId };
32
+ }
27
33
  return { status: "already-closed", executionId };
28
34
  }
29
35
  }
@@ -31,7 +37,7 @@ export function interruptProjectionGeneration(projectionDirectory, nowMs = Date.
31
37
  store.close();
32
38
  }
33
39
  }
34
- function settleReleasedLeash(store, projectionDirectory, executionId) {
40
+ function settleReleasedLeash(store, projectionDirectory, executionId, terminalAt) {
35
41
  const current = store.readCurrentGeneration();
36
42
  if (current?.launch.executionId !== executionId
37
43
  || current.verdict) {
@@ -39,10 +45,10 @@ function settleReleasedLeash(store, projectionDirectory, executionId) {
39
45
  }
40
46
  let deathEvidence;
41
47
  try {
42
- deathEvidence = acquireProjectionRunnerSettlementLock(projectionDirectory);
48
+ deathEvidence = acquireRunnerLostProof(projectionRunnerLeasePath(projectionDirectory));
43
49
  }
44
50
  catch (error) {
45
- if (error instanceof ProjectionRunnerLockUnavailableError) {
51
+ if (error instanceof RunnerLeaseUnavailableError) {
46
52
  return { status: "accepted", executionId };
47
53
  }
48
54
  return { status: "accepted", executionId };
@@ -50,7 +56,8 @@ function settleReleasedLeash(store, projectionDirectory, executionId) {
50
56
  try {
51
57
  store.settleKillIfRunnerDead({
52
58
  executionId,
53
- releasedLockEvidence: { state: "acquired", executionId },
59
+ releasedLockEvidence: { state: "acquired", executionId, observedAt: terminalAt },
60
+ terminalAt,
54
61
  });
55
62
  }
56
63
  finally {
@@ -78,11 +85,14 @@ export function killProjectionGeneration(projectionDirectory, nowMs = Date.now()
78
85
  const executionId = current.launch.executionId;
79
86
  const request = store.requestKillIfOpen({ executionId, killedAt });
80
87
  if (request.status === "committed") {
81
- return settleReleasedLeash(store, projectionDirectory, executionId);
88
+ // A missing, stale, or rejected endpoint has no lifecycle meaning.
89
+ ringProjectionGenerationDoorbell(store.readIdentity(), executionId);
90
+ return settleReleasedLeash(store, projectionDirectory, executionId, new Date(nowMs).toISOString());
82
91
  }
83
92
  if (request.reason === "kill-requested") {
84
93
  // Idempotent first intent: still accepted, then one leash observation.
85
- return settleReleasedLeash(store, projectionDirectory, executionId);
94
+ ringProjectionGenerationDoorbell(store.readIdentity(), executionId);
95
+ return settleReleasedLeash(store, projectionDirectory, executionId, new Date(nowMs).toISOString());
86
96
  }
87
97
  if (request.reason === "already-terminal") {
88
98
  return { status: "already-closed", executionId };
@@ -1,13 +1,15 @@
1
1
  import { isIncompatibleProjectionGenerationStoreError, openProjectionGenerationStoreReadOnly, } from "./generation/store.js";
2
- import { deriveProjectionLife, selectCurrentGeneration, } from "./projection-life-protocol.js";
3
- import { observeProjectionRunnerLock } from "./projection-runner-lock.js";
2
+ import { deriveProjectionLife, selectCurrentGeneration, } from "./generation/protocol.js";
3
+ import { observeRunnerLease } from "../run-control/runner-lease.js";
4
+ import { projectionRunnerLeasePath } from "./generation/projection-generation-identity.js";
4
5
  function errorDetail(error) {
5
6
  return error instanceof Error ? error.message : String(error);
6
7
  }
7
8
  /**
8
9
  * Observe the current generation without creating, repairing, or mutating
9
10
  * projection storage. Generation history is the ordering authority; the
10
- * runner lock is consulted only for an adopted, non-terminal generation.
11
+ * runner lock is consulted for every non-terminal generation. Launch age is
12
+ * not lifecycle authority.
11
13
  */
12
14
  export function observeProjectionLifeSnapshot(projectionDirectory, options) {
13
15
  const readRecords = () => {
@@ -37,18 +39,17 @@ export function observeProjectionLifeSnapshot(projectionDirectory, options) {
37
39
  };
38
40
  }
39
41
  const current = selectCurrentGeneration(records);
40
- if (!current || current.verdict || !current.adoption) {
42
+ if (!current || current.verdict) {
41
43
  return {
42
44
  compatibility: "current",
43
45
  life: deriveProjectionLife({
44
46
  records,
45
47
  nowMs: options.nowMs,
46
- startupTimeoutMs: options.startupTimeoutMs,
47
48
  }),
48
49
  records,
49
50
  };
50
51
  }
51
- const lock = observeProjectionRunnerLock(projectionDirectory, options.platform);
52
+ const lock = observeRunnerLease(projectionRunnerLeasePath(projectionDirectory));
52
53
  let after;
53
54
  try {
54
55
  after = readRecords();
@@ -74,13 +75,12 @@ export function observeProjectionLifeSnapshot(projectionDirectory, options) {
74
75
  compatibility: "current",
75
76
  life: deriveProjectionLife({
76
77
  records,
77
- runnerLock: lock.state === "held"
78
+ runnerLease: lock.state === "held"
78
79
  ? "held"
79
80
  : lock.state === "released"
80
81
  ? "released"
81
82
  : "unknown",
82
83
  nowMs: options.nowMs,
83
- startupTimeoutMs: options.startupTimeoutMs,
84
84
  }),
85
85
  records,
86
86
  };
@@ -5,7 +5,9 @@ import { ProjectionStateError, randomHex8 } from "../atomic-publish.js";
5
5
  import { isoFromNowMs, PROJECTION_ROOT_SEGMENTS, projectionDir, projectionRoot, } from "./projection-core.js";
6
6
  import { parseExecutionAuthoritySelection, parseExecutionPact, parseExecutionPactFields, } from "../execution-pact.js";
7
7
  import { openProjectionGenerationStore } from "./generation/store.js";
8
- import { initializeProjectionRunnerLockDatabase } from "./projection-runner-lock.js";
8
+ import { initializeRunnerLeaseDatabase } from "../run-control/runner-lease.js";
9
+ import { projectionRunnerLeasePath } from "./generation/projection-generation-identity.js";
10
+ import { isAkumaName } from "./akuma-name.js";
9
11
  export class ProjectionMintCollisionError extends Error {
10
12
  code = "PROJECTION_MINT_COLLISION";
11
13
  constructor(message, options) {
@@ -15,14 +17,10 @@ export class ProjectionMintCollisionError extends Error {
15
17
  }
16
18
  const DEFAULT_MINT_ATTEMPTS = 16;
17
19
  function assertMintSlug(slug) {
18
- const normalized = slug.trim();
19
- if (!normalized) {
20
- throw new ProjectionStateError("projection slug cannot be empty");
21
- }
22
- if (!/^[a-zA-Z0-9]+(?:[.-][a-zA-Z0-9]+)*$/.test(normalized)) {
20
+ if (!isAkumaName(slug)) {
23
21
  throw new ProjectionStateError(`invalid projection slug: ${slug}`);
24
22
  }
25
- return normalized;
23
+ return slug;
26
24
  }
27
25
  function removeEmptyAkumaRoot(dir) {
28
26
  try {
@@ -115,7 +113,7 @@ function publishReservedProjection(pact, reserved) {
115
113
  finally {
116
114
  heart.close();
117
115
  }
118
- initializeProjectionRunnerLockDatabase(reserved.stagingDir);
116
+ initializeRunnerLeaseDatabase(projectionRunnerLeasePath(reserved.stagingDir));
119
117
  fs.renameSync(reserved.stagingDir, reserved.dir);
120
118
  published = true;
121
119
  return { id: reserved.id, dir: reserved.dir, pact };
@@ -7,12 +7,51 @@ import { foldPublicProjectionActivity } from "./projection-activity.js";
7
7
  import { readProjectionIdentity } from "./projection-identity.js";
8
8
  import { readPendingTellWindow, readProjectionTellCounts } from "./tell/store.js";
9
9
  import { ProjectionGenerationStoreError } from "./generation/store.js";
10
- import { PROJECTION_ADOPTION_TIMEOUT_MS } from "./generation/projection-generation-launcher.js";
11
10
  import { observeProjectionLifeSnapshot } from "./projection-life-observer.js";
12
- import { selectCurrentGeneration } from "./projection-life-protocol.js";
11
+ import { selectCurrentGeneration } from "./generation/protocol.js";
13
12
  import { invalidActivityEventDiagnostic, selectValidStoredAgentEvents } from "../stored-agent-event.js";
14
13
  import { projectTerminalFailure } from "./projection-terminal-failure.js";
15
14
  import { extractTerminalTimestamp } from "./generation/model.js";
15
+ function latestActivityRow(activity, kind, predicate) {
16
+ const rows = activity?.retainedTimeline.filter((row) => row.kind === kind) ?? [];
17
+ return [...rows].reverse().find((row) => predicate?.(row) ?? true);
18
+ }
19
+ export function deriveProjectionStatusReadFacts(input) {
20
+ const warning = latestActivityRow(input.activity, "warning");
21
+ const runningTool = latestActivityRow(input.activity, "ran", (row) => row.state === "running");
22
+ const said = latestActivityRow(input.activity, "said");
23
+ const answer = typeof input.verdictFacts?.finalMessage === "string"
24
+ ? { kind: "text", text: input.verdictFacts.finalMessage }
25
+ : undefined;
26
+ const responsePath = typeof input.verdictFacts?.responsePath === "string" ? input.verdictFacts.responsePath : undefined;
27
+ const artifactId = typeof input.verdictFacts?.artifactId === "string" ? input.verdictFacts.artifactId : undefined;
28
+ const launchInputs = input.launchFacts?.inputs;
29
+ const incognito = launchInputs !== null && typeof launchInputs === "object" && !Array.isArray(launchInputs)
30
+ && launchInputs.persistProjectionResponse === false;
31
+ const persistenceFailed = input.verdictFacts?.responsePersistError !== undefined;
32
+ const snapshot = warning
33
+ ? { kind: "warning", row: warning }
34
+ : runningTool
35
+ ? { kind: "running-tool", row: runningTool }
36
+ : said
37
+ ? { kind: "said", row: said }
38
+ : input.activity?.plan
39
+ ? { kind: "plan", plan: input.activity.plan }
40
+ : undefined;
41
+ const responseArtifact = input.verdictFacts === undefined
42
+ ? undefined
43
+ : responsePath
44
+ ? { state: "retained", path: responsePath, ...(artifactId ? { artifactId } : {}) }
45
+ : incognito
46
+ ? { state: "none", reason: "incognito" }
47
+ : { state: "unavailable", reason: persistenceFailed ? "persistence-failed" : "not-observed" };
48
+ return {
49
+ ...(answer ? { terminalAnswer: answer } : {}),
50
+ ...(responseArtifact ? { responseArtifact } : {}),
51
+ attention: { ...(warning ? { latestWarning: warning } : {}) },
52
+ ...(snapshot ? { snapshot } : {}),
53
+ };
54
+ }
16
55
  const MAX_STATUS_FILE_BYTES = 256 * 1024;
17
56
  class ProjectionStatusFileError extends Error {
18
57
  diagnostic;
@@ -233,11 +272,19 @@ function readActivityFacts(projectionDir, executionId, nowMs, diagnostics, fallb
233
272
  function terminalTimestamp(current) {
234
273
  return extractTerminalTimestamp(current?.verdict?.facts);
235
274
  }
275
+ function terminalAgeAt(terminalAt, observedAtMs) {
276
+ const terminalAtMs = Date.parse(terminalAt ?? "");
277
+ return Number.isFinite(terminalAtMs) ? Math.max(0, observedAtMs - terminalAtMs) : undefined;
278
+ }
279
+ function projectionStatusState(life) {
280
+ if (life.verdict === "lost-runner")
281
+ return "dead";
282
+ return life.phase === "active" || life.phase === "quiescent" ? "out" : life.phase;
283
+ }
236
284
  export function readProjectionSummary(projectionDir, id, alias, nowMs, options) {
237
285
  const diagnostics = [];
238
286
  const snapshot = observeProjectionLifeSnapshot(projectionDir, {
239
287
  nowMs,
240
- startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
241
288
  });
242
289
  if (snapshot.compatibility === "incompatible") {
243
290
  return {
@@ -251,8 +298,7 @@ export function readProjectionSummary(projectionDir, id, alias, nowMs, options)
251
298
  };
252
299
  }
253
300
  const life = snapshot.life;
254
- const phase = life.phase;
255
- const state = phase === "active" || phase === "quiescent" ? "out" : phase;
301
+ const state = projectionStatusState(life);
256
302
  const identityFacts = options?.requiresIdentity?.(state) === false
257
303
  ? null
258
304
  : readIdentityFacts(projectionDir, diagnostics);
@@ -268,27 +314,38 @@ export function readProjectionSummary(projectionDir, id, alias, nowMs, options)
268
314
  }
269
315
  export function readProjectionRow(summary, nowMs, readDetails) {
270
316
  const { id, alias, projectionDir, snapshot } = summary;
317
+ const identity = { canonicalId: id };
271
318
  const diagnostics = [...summary.diagnostics];
272
319
  if (snapshot.compatibility === "incompatible") {
273
320
  return {
274
321
  id,
322
+ identity,
275
323
  observedAtMs: nowMs,
276
324
  ...(alias ? { alias } : {}),
277
325
  state: "incompatible",
278
326
  observation: snapshot,
279
327
  tellCounts: emptyTellCounts(),
328
+ attention: {},
280
329
  diagnostics,
281
330
  };
282
331
  }
283
332
  const identityFacts = summary.identityFacts;
284
333
  if (identityFacts === null && !readDetails) {
334
+ const current = snapshot.records ? selectCurrentGeneration(snapshot.records) : null;
335
+ const terminalAt = terminalTimestamp(current);
336
+ const terminalAgeMs = current?.verdict ? terminalAgeAt(terminalAt, nowMs) : undefined;
337
+ const readFacts = deriveProjectionStatusReadFacts({ state: summary.state });
285
338
  return {
286
339
  id,
340
+ identity,
287
341
  observedAtMs: nowMs,
288
342
  ...(alias ? { alias } : {}),
289
343
  state: summary.state,
290
344
  observation: { compatibility: "current", lifecycle: snapshot.life },
345
+ ...(terminalAt ? { terminalAt } : {}),
346
+ ...(terminalAgeMs !== undefined ? { terminalAgeMs } : {}),
291
347
  tellCounts: emptyTellCounts(),
348
+ ...readFacts,
292
349
  diagnostics,
293
350
  };
294
351
  }
@@ -297,6 +354,7 @@ export function readProjectionRow(summary, nowMs, readDetails) {
297
354
  ?? "projection identity could not be observed";
298
355
  return {
299
356
  id,
357
+ identity,
300
358
  observedAtMs: nowMs,
301
359
  ...(alias ? { alias } : {}),
302
360
  state: "unknown",
@@ -305,6 +363,7 @@ export function readProjectionRow(summary, nowMs, readDetails) {
305
363
  lifecycle: { phase: "unknown", diagnostic },
306
364
  },
307
365
  tellCounts: emptyTellCounts(),
366
+ attention: {},
308
367
  diagnostics,
309
368
  };
310
369
  }
@@ -325,8 +384,10 @@ export function readProjectionRow(summary, nowMs, readDetails) {
325
384
  : {};
326
385
  const mintedAtMs = Date.parse(identityFacts.mintedAt ?? "");
327
386
  const terminalAt = terminalTimestamp(current);
387
+ const terminalAtMs = Date.parse(terminalAt ?? "");
388
+ const terminalAgeMs = current?.verdict ? terminalAgeAt(terminalAt, nowMs) : undefined;
328
389
  const durationAnchorMs = current?.verdict
329
- ? (terminalAt === undefined ? Number.NaN : Date.parse(terminalAt))
390
+ ? terminalAtMs
330
391
  : nowMs;
331
392
  const durationMs = Number.isFinite(mintedAtMs) && Number.isFinite(durationAnchorMs)
332
393
  ? Math.max(0, durationAnchorMs - mintedAtMs)
@@ -336,13 +397,19 @@ export function readProjectionRow(summary, nowMs, readDetails) {
336
397
  if (life.phase === "unknown" && life.diagnostic) {
337
398
  diagnostics.push({ kind: "unknown-lifecycle", detail: life.diagnostic });
338
399
  }
339
- const phase = life.phase;
340
- const state = phase === "active" || phase === "quiescent" ? "out" : phase;
400
+ const state = projectionStatusState(life);
341
401
  const terminalFailure = current?.verdict
342
402
  ? projectTerminalFailure(current.verdict.facts.state, current.verdict.facts)
343
403
  : undefined;
404
+ const readFacts = deriveProjectionStatusReadFacts({
405
+ state,
406
+ activity: activityFacts.activity,
407
+ verdictFacts: current?.verdict?.facts,
408
+ launchFacts: current?.launch.facts,
409
+ });
344
410
  return {
345
411
  id,
412
+ identity,
346
413
  observedAtMs: nowMs,
347
414
  ...(alias ? { alias } : {}),
348
415
  state,
@@ -353,11 +420,13 @@ export function readProjectionRow(summary, nowMs, readDetails) {
353
420
  pid: identityFacts.pid,
354
421
  mintedAt: identityFacts.mintedAt,
355
422
  ...(terminalAt ? { terminalAt } : {}),
423
+ ...(terminalAgeMs !== undefined ? { terminalAgeMs } : {}),
356
424
  ...(durationMs !== undefined ? { durationMs } : {}),
357
425
  ...(identityFacts.workspaceRoot ? { workspaceRoot: identityFacts.workspaceRoot } : {}),
358
426
  ...activityFacts,
359
427
  ...(displayAgeMs !== undefined ? { activityAgeMs: displayAgeMs } : {}),
360
428
  ...(terminalFailure ? { terminalFailure } : {}),
429
+ ...readFacts,
361
430
  tellCounts,
362
431
  ...(pendingTells.length > 0 ? { pendingTells } : {}),
363
432
  diagnostics,