@astrosheep/keiyaku 2.9.4 → 2.9.6

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 (143) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/call-terms.js +1 -0
  3. package/build/agents/harness/execution-handle.js +1 -0
  4. package/build/agents/harness/outcome.js +31 -5
  5. package/build/agents/harness/projection.js +151 -78
  6. package/build/agents/harness/router.js +2 -1
  7. package/build/agents/providers/claude-agent-sdk/adapter.js +13 -1
  8. package/build/agents/providers/claude-agent-sdk/registration.js +2 -1
  9. package/build/agents/providers/codex-app-server/adapter.js +43 -10
  10. package/build/agents/providers/codex-app-server/events.js +23 -0
  11. package/build/agents/providers/codex-app-server/registration.js +2 -1
  12. package/build/agents/providers/opencode-sdk/adapter.js +7 -0
  13. package/build/agents/providers/opencode-sdk/registration.js +2 -1
  14. package/build/agents/providers/pi/adapter.js +10 -0
  15. package/build/agents/providers/pi/events.js +4 -3
  16. package/build/agents/providers/pi/registration.js +3 -1
  17. package/build/agents/types.js +2 -0
  18. package/build/cli/commands/akuma/akuma/meta.js +1 -1
  19. package/build/cli/commands/akuma/list/meta-ls.js +1 -1
  20. package/build/cli/commands/akuma/view/meta.js +1 -1
  21. package/build/cli/commands/contract/amend/meta.js +4 -4
  22. package/build/cli/commands/contract/arc/handler.js +1 -1
  23. package/build/cli/commands/contract/arc/meta.js +4 -4
  24. package/build/cli/commands/contract/audit/handler.js +16 -0
  25. package/build/cli/commands/contract/audit/meta.js +16 -0
  26. package/build/cli/commands/contract/bind/meta.js +20 -11
  27. package/build/cli/commands/contract/catalog.js +2 -0
  28. package/build/cli/commands/contract/forfeit/meta.js +1 -1
  29. package/build/cli/commands/contract/log/meta.js +1 -1
  30. package/build/cli/commands/contract/petition/handler.js +1 -2
  31. package/build/cli/commands/contract/petition/meta.js +6 -6
  32. package/build/cli/commands/contract/renew/handler.js +2 -2
  33. package/build/cli/commands/contract/renew/meta.js +4 -4
  34. package/build/cli/commands/metadata.js +3 -2
  35. package/build/cli/commands/projection/call/meta.js +4 -1
  36. package/build/cli/commands/projection/kill/meta.js +1 -1
  37. package/build/cli/commands/projection/revive/meta.js +4 -1
  38. package/build/cli/commands/projection/status/meta.js +1 -1
  39. package/build/cli/commands/projection/tell/handler.js +1 -1
  40. package/build/cli/commands/projection/tell/meta.js +4 -1
  41. package/build/cli/commands/projection/wait/meta.js +1 -1
  42. package/build/cli/commands/skills/install/meta.js +1 -1
  43. package/build/cli/commands/skills/skills/meta.js +1 -1
  44. package/build/cli/commands/system/completion/meta.js +1 -1
  45. package/build/cli/commands/system/dump-env/meta.js +1 -1
  46. package/build/cli/commands/system/guide/meta.js +1 -1
  47. package/build/cli/commands/task/add/handler.js +2 -2
  48. package/build/cli/commands/task/add/meta.js +4 -1
  49. package/build/cli/commands/task/doctor/meta.js +1 -1
  50. package/build/cli/commands/task/done/meta.js +1 -1
  51. package/build/cli/commands/task/drop/meta.js +1 -1
  52. package/build/cli/commands/task/ls/handler.js +10 -4
  53. package/build/cli/commands/task/ls/meta.js +3 -3
  54. package/build/cli/commands/task/show/meta.js +1 -1
  55. package/build/cli/commands/task/start/meta.js +1 -1
  56. package/build/cli/commands/task/stop/meta.js +1 -1
  57. package/build/cli/commands/task/task/meta.js +1 -1
  58. package/build/cli/commands/task/update/handler.js +2 -3
  59. package/build/cli/commands/task/update/meta.js +4 -1
  60. package/build/cli/completion.js +9 -2
  61. package/build/cli/flags.js +19 -3
  62. package/build/cli/help.js +3 -3
  63. package/build/cli/index.js +59 -75
  64. package/build/cli/output-stream-error-policy.js +19 -5
  65. package/build/cli/parse-flags.js +19 -2
  66. package/build/cli/parse-metadata.js +1 -7
  67. package/build/cli/parse-selectors.js +1 -0
  68. package/build/cli/parse.js +16 -7
  69. package/build/cli/projection-address.js +6 -1
  70. package/build/cli/render/audit.js +121 -0
  71. package/build/cli/render/errors.js +18 -1
  72. package/build/cli/render/format.js +5 -2
  73. package/build/cli/render/misc.js +3 -1
  74. package/build/cli/render/projection-activity.js +12 -6
  75. package/build/cli/render/status.js +28 -18
  76. package/build/cli/render/success-response.js +6 -5
  77. package/build/cli/render/task.js +12 -43
  78. package/build/cli/render/terminal-failure.js +7 -2
  79. package/build/cli/render/wait.js +36 -8
  80. package/build/config/env-keys.js +2 -0
  81. package/build/config/env.js +3 -0
  82. package/build/core/amend.js +20 -94
  83. package/build/core/arc.js +14 -6
  84. package/build/core/audit/candidate.js +48 -0
  85. package/build/core/audit/coordinates.js +180 -0
  86. package/build/core/audit/evidence.js +158 -0
  87. package/build/core/audit/facade.js +42 -0
  88. package/build/core/audit/readiness.js +4 -0
  89. package/build/core/audit/report.js +38 -0
  90. package/build/core/audit.js +1 -0
  91. package/build/core/bind.js +1 -1
  92. package/build/core/call/execution.js +1 -0
  93. package/build/core/contract-view.js +2 -3
  94. package/build/core/draft.js +33 -20
  95. package/build/core/entry.js +3 -4
  96. package/build/core/markdown/lex.js +2 -1
  97. package/build/core/projection/generation/database.js +41 -16
  98. package/build/core/projection/generation/model.js +22 -2
  99. package/build/core/projection/generation/projection-generation-execution.js +20 -7
  100. package/build/core/projection/generation/projection-generation-launcher.js +4 -4
  101. package/build/core/projection/generation/projection-generation-runner.js +1 -0
  102. package/build/core/projection/generation/store.js +1 -1
  103. package/build/core/projection/generation/transitions.js +19 -3
  104. package/build/core/projection/index.js +2 -2
  105. package/build/core/projection/projection-execution-observer.js +35 -21
  106. package/build/core/projection/projection-life-observer.js +16 -2
  107. package/build/core/projection/projection-status.js +67 -22
  108. package/build/core/projection/projection-terminal-failure.js +2 -0
  109. package/build/core/projection/projection-wait.js +6 -9
  110. package/build/core/projection/projection-wake.js +30 -6
  111. package/build/core/renew.js +41 -22
  112. package/build/core/scope.js +129 -2
  113. package/build/core/seal.js +36 -15
  114. package/build/core/settlement/petition-claim-gates.js +4 -33
  115. package/build/core/settlement/petition-preview.js +1 -1
  116. package/build/core/settlement/petition.js +10 -3
  117. package/build/core/settlement/settlement.js +3 -10
  118. package/build/core/settlement/verification.js +59 -0
  119. package/build/core/status/board.js +48 -28
  120. package/build/core/stored-agent-event.js +3 -2
  121. package/build/core/task/board.js +1 -0
  122. package/build/core/task/commands.js +20 -12
  123. package/build/core/task/document.js +5 -0
  124. package/build/core/task/index.js +1 -1
  125. package/build/core/task/model.js +8 -1
  126. package/build/core/task/query.js +214 -0
  127. package/build/core/task/settlement-git.js +115 -16
  128. package/build/core/task/settlement-policy.js +17 -9
  129. package/build/core/task/source-board.js +1 -0
  130. package/build/core/task/task-contract.js +125 -51
  131. package/build/core/task/task-store-repository.js +1 -0
  132. package/build/core/task/task.js +2 -0
  133. package/build/flow-error.js +2 -4
  134. package/build/generated/version.js +2 -2
  135. package/build/git/diff/parsers.js +23 -13
  136. package/build/git/diff/structured.js +127 -0
  137. package/build/index.js +7 -7
  138. package/build/telemetry/logger.js +4 -2
  139. package/package.json +1 -1
  140. package/skills/keiyaku/SKILL.md +2 -2
  141. package/skills/keiyaku-akuma/SKILL.md +17 -6
  142. package/skills/keiyaku-task/SKILL.md +16 -7
  143. package/skills/keiyaku-workflow/SKILL.md +48 -13
@@ -1,22 +1,27 @@
1
1
  import { claimAllInbox, listTellIds, markTellSubmitted, markTellConsumed, readTellSubmitted, readTellOriginal, isTellCausallyConsumed, appendGenerationEvent, } from "./projection-core.js";
2
2
  import { openProjectionTellObserver, } from "./tell/database.js";
3
3
  const LIVE_TELL_DELIVERY_THROTTLE_MS = 200;
4
- function submitEligible(projectionDirectory, executionId, control, requireInbox, submitting, observer, pendingCompletionEvidence) {
4
+ export function renderProjectionTellFrame(tellId, text) {
5
+ return `--- tell ${tellId} ---\n${text}`;
6
+ }
7
+ function submitEligible(projectionDirectory, executionId, control, requireInbox, submitting, submissions, excludedTellIds, observer, pendingCompletionEvidence) {
5
8
  if (requireInbox && !observer?.hasInbox())
6
9
  return;
7
10
  claimAllInbox(projectionDirectory);
8
11
  for (const tellId of listTellIds(projectionDirectory, "inflight")) {
12
+ if (excludedTellIds.has(tellId))
13
+ continue;
9
14
  if (submitting.has(tellId))
10
15
  continue;
11
16
  // Construct before pin: a sync read/build failure must not leave submitting stuck.
12
17
  const original = readTellOriginal(projectionDirectory, "inflight", tellId);
13
18
  const tell = {
14
19
  id: tellId,
15
- text: `--- tell ${tellId} ---\n${original.text}`,
20
+ text: renderProjectionTellFrame(tellId, original.text),
16
21
  ...(original.effort ? { effort: original.effort } : {}),
17
22
  };
18
23
  submitting.add(tellId);
19
- void Promise.resolve()
24
+ const submission = Promise.resolve()
20
25
  .then(() => control.enqueue(tell))
21
26
  .then((ack) => {
22
27
  if (!ack)
@@ -39,6 +44,8 @@ function submitEligible(projectionDirectory, executionId, control, requireInbox,
39
44
  pendingCompletionEvidence.delete(tellId);
40
45
  })
41
46
  .finally(() => submitting.delete(tellId));
47
+ submissions.add(submission);
48
+ void submission.finally(() => submissions.delete(submission));
42
49
  }
43
50
  }
44
51
  export function createLiveTellDeliveryController(projectionDirectory, executionId, dependencies = {}) {
@@ -47,8 +54,10 @@ export function createLiveTellDeliveryController(projectionDirectory, executionI
47
54
  const clearTimer = dependencies.clearTimer ?? ((timer) => clearTimeout(timer));
48
55
  const openObserver = dependencies.openObserver ?? openProjectionTellObserver;
49
56
  const submitting = new Set();
57
+ const submissions = new Set();
50
58
  // In-memory only: bridges completion-before-ack when a tell is still submitting.
51
59
  const pendingCompletionEvidence = new Map();
60
+ const initial = new Set(dependencies.initialTellIds);
52
61
  let observer;
53
62
  let timer;
54
63
  let nextOpportunityAt = 0;
@@ -93,7 +102,7 @@ export function createLiveTellDeliveryController(projectionDirectory, executionI
93
102
  try {
94
103
  observer ??= openObserver(projectionDirectory);
95
104
  if (observer.hasDataVersionChanged()) {
96
- submitEligible(projectionDirectory, executionId, control, true, submitting, observer, pendingCompletionEvidence);
105
+ submitEligible(projectionDirectory, executionId, control, true, submitting, submissions, initial, observer, pendingCompletionEvidence);
97
106
  }
98
107
  }
99
108
  catch {
@@ -131,7 +140,14 @@ export function createLiveTellDeliveryController(projectionDirectory, executionI
131
140
  return;
132
141
  latestControl = control;
133
142
  cancelOpportunity();
134
- submitEligible(projectionDirectory, executionId, control, false, submitting, observer, pendingCompletionEvidence);
143
+ for (const tellId of initial) {
144
+ markTellSubmitted(projectionDirectory, tellId, {
145
+ executionId,
146
+ fence: checkpoint.fence,
147
+ });
148
+ initial.delete(tellId);
149
+ }
150
+ submitEligible(projectionDirectory, executionId, control, false, submitting, submissions, initial, observer, pendingCompletionEvidence);
135
151
  if (!checkpoint.consumptionEvidence)
136
152
  return;
137
153
  for (const tellId of listTellIds(projectionDirectory, "submitted")) {
@@ -149,13 +165,14 @@ export function createLiveTellDeliveryController(projectionDirectory, executionI
149
165
  pendingCompletionEvidence.set(tellId, [checkpoint.fence]);
150
166
  }
151
167
  },
152
- close() {
153
- if (closed)
154
- return;
155
- closed = true;
156
- cancelOpportunity();
157
- discardObserver();
168
+ async close() {
169
+ if (!closed) {
170
+ closed = true;
171
+ cancelOpportunity();
172
+ discardObserver();
173
+ }
158
174
  // Do not drop pendingCompletionEvidence: in-flight acks may still submit/consume.
175
+ await Promise.all([...submissions]);
159
176
  },
160
177
  };
161
178
  }
@@ -174,8 +191,8 @@ export function buildSyncCheckpointCallback(projectionDirectory, executionId) {
174
191
  */
175
192
  export function createProjectionExecutionObserver(input, dependencies = {}) {
176
193
  const appendEvent = dependencies.appendGenerationEvent ?? appendGenerationEvent;
177
- const tellController = createLiveTellDeliveryController(input.projectionDirectory, input.executionId);
178
- let closed = false;
194
+ const tellController = createLiveTellDeliveryController(input.projectionDirectory, input.executionId, { initialTellIds: input.initialTellIds });
195
+ let closePromise;
179
196
  const observer = {
180
197
  onEvent(event) {
181
198
  appendEvent(input.projectionDirectory, input.executionId, event);
@@ -190,15 +207,12 @@ export function createProjectionExecutionObserver(input, dependencies = {}) {
190
207
  return {
191
208
  observer,
192
209
  close() {
193
- if (closed)
194
- return;
195
- closed = true;
196
- try {
197
- tellController.close();
198
- }
199
- catch {
210
+ if (closePromise)
211
+ return closePromise;
212
+ closePromise = tellController.close().catch(() => {
200
213
  // Controller cleanup cannot change provider completion.
201
- }
214
+ });
215
+ return closePromise;
202
216
  },
203
217
  };
204
218
  }
@@ -1,4 +1,4 @@
1
- import { openProjectionGenerationStoreReadOnly, } from "./generation/store.js";
1
+ import { isIncompatibleProjectionGenerationStoreError, openProjectionGenerationStoreReadOnly, } from "./generation/store.js";
2
2
  import { deriveProjectionLife, selectCurrentGeneration, } from "./projection-life-protocol.js";
3
3
  import { observeProjectionRunnerLock } from "./projection-runner-lock.js";
4
4
  function errorDetail(error) {
@@ -25,7 +25,11 @@ export function observeProjectionLifeSnapshot(projectionDirectory, options) {
25
25
  records = readRecords();
26
26
  }
27
27
  catch (error) {
28
+ if (isIncompatibleProjectionGenerationStoreError(error)) {
29
+ return { compatibility: "incompatible", diagnostic: errorDetail(error) };
30
+ }
28
31
  return {
32
+ compatibility: "current",
29
33
  life: {
30
34
  phase: "unknown",
31
35
  diagnostic: `generation history could not be observed: ${errorDetail(error)}`,
@@ -35,6 +39,7 @@ export function observeProjectionLifeSnapshot(projectionDirectory, options) {
35
39
  const current = selectCurrentGeneration(records);
36
40
  if (!current || current.verdict || !current.adoption) {
37
41
  return {
42
+ compatibility: "current",
38
43
  life: deriveProjectionLife({
39
44
  records,
40
45
  nowMs: options.nowMs,
@@ -49,7 +54,11 @@ export function observeProjectionLifeSnapshot(projectionDirectory, options) {
49
54
  after = readRecords();
50
55
  }
51
56
  catch (error) {
57
+ if (isIncompatibleProjectionGenerationStoreError(error)) {
58
+ return { compatibility: "incompatible", diagnostic: errorDetail(error) };
59
+ }
52
60
  return {
61
+ compatibility: "current",
53
62
  life: {
54
63
  phase: "unknown",
55
64
  executionId: current.launch.executionId,
@@ -62,6 +71,7 @@ export function observeProjectionLifeSnapshot(projectionDirectory, options) {
62
71
  if (records.length !== after.length || beforeTail !== afterTail)
63
72
  continue;
64
73
  return {
74
+ compatibility: "current",
65
75
  life: deriveProjectionLife({
66
76
  records,
67
77
  runnerLock: lock.state === "held"
@@ -76,6 +86,7 @@ export function observeProjectionLifeSnapshot(projectionDirectory, options) {
76
86
  };
77
87
  }
78
88
  return {
89
+ compatibility: "current",
79
90
  life: {
80
91
  phase: "unknown",
81
92
  diagnostic: "generation history changed during runner-lock observation",
@@ -83,5 +94,8 @@ export function observeProjectionLifeSnapshot(projectionDirectory, options) {
83
94
  };
84
95
  }
85
96
  export function observeProjectionLife(projectionDirectory, options) {
86
- return observeProjectionLifeSnapshot(projectionDirectory, options).life;
97
+ const snapshot = observeProjectionLifeSnapshot(projectionDirectory, options);
98
+ return snapshot.compatibility === "current"
99
+ ? snapshot.life
100
+ : { phase: "unknown", diagnostic: snapshot.diagnostic };
87
101
  }
@@ -10,10 +10,11 @@ import { readProjectionTellCounts } from "./tell/store.js";
10
10
  import { ProjectionGenerationStoreError, } from "./generation/store.js";
11
11
  import { PROJECTION_ADOPTION_TIMEOUT_MS } from "./generation/projection-generation-launcher.js";
12
12
  import { observeProjectionLifeSnapshot } from "./projection-life-observer.js";
13
- import { selectCurrentGeneration } from "./projection-life-protocol.js";
13
+ import { selectCurrentGeneration, } from "./projection-life-protocol.js";
14
14
  import { invalidActivityEventDiagnostic, selectValidStoredAgentEvents } from "../stored-agent-event.js";
15
15
  import { worktreeContainsCoordinate } from "../worktree-path.js";
16
16
  import { projectTerminalFailure, } from "./projection-terminal-failure.js";
17
+ import { extractTerminalTimestamp } from "./generation/model.js";
17
18
  const MAX_STATUS_FILE_BYTES = 256 * 1024;
18
19
  const STATUS_GROUP = {
19
20
  "startup-timeout": 0,
@@ -26,13 +27,15 @@ const STATUS_GROUP = {
26
27
  done: 3,
27
28
  killed: 4,
28
29
  dismissed: 4,
30
+ incompatible: 5,
29
31
  };
30
32
  function isOmissibleProjectionStatusState(state) {
31
33
  return state === "failed"
32
34
  || state === "dead"
33
35
  || state === "done"
34
36
  || state === "killed"
35
- || state === "dismissed";
37
+ || state === "dismissed"
38
+ || state === "incompatible";
36
39
  }
37
40
  function rowSortTime(row) {
38
41
  if (row.activityAtMs !== undefined && Number.isFinite(row.activityAtMs)) {
@@ -71,6 +74,7 @@ export function prioritizeProjectionStatusRows(rows, showAll) {
71
74
  done: 0,
72
75
  killed: 0,
73
76
  dismissed: 0,
77
+ incompatible: 0,
74
78
  };
75
79
  for (const row of ordered) {
76
80
  if (isOmissibleProjectionStatusState(row.state))
@@ -239,64 +243,99 @@ function readStoredEvents(projectionDir, executionId, diagnostics) {
239
243
  }
240
244
  return values;
241
245
  }
242
- function latestValidEventAtMs(events) {
246
+ function latestValidEvent(events) {
243
247
  let latest;
244
248
  for (const event of events) {
245
249
  const atMs = Date.parse(event.at);
246
250
  if (!Number.isFinite(atMs))
247
251
  continue;
248
- if (latest === undefined || atMs > latest)
249
- latest = atMs;
252
+ if (latest === undefined || atMs > latest.atMs)
253
+ latest = { at: event.at, atMs };
250
254
  }
251
255
  return latest;
252
256
  }
253
- function readActivityFacts(projectionDir, executionId, nowMs, diagnostics, fallbackAtMs) {
254
- if (!executionId)
255
- return {};
257
+ function readActivityFacts(projectionDir, executionId, nowMs, diagnostics, fallbackAt) {
258
+ const fallbackAtMs = fallbackAt === undefined ? undefined : Date.parse(fallbackAt);
259
+ const validFallback = fallbackAt !== undefined && Number.isFinite(fallbackAtMs)
260
+ ? { at: fallbackAt, atMs: fallbackAtMs }
261
+ : undefined;
262
+ if (!executionId) {
263
+ return validFallback
264
+ ? { activityAt: validFallback.at, activityAtMs: validFallback.atMs, activityAgeMs: Math.max(0, nowMs - validFallback.atMs) }
265
+ : {};
266
+ }
256
267
  const stored = readStoredEvents(projectionDir, executionId, diagnostics);
257
268
  const { events, malformed } = selectValidStoredAgentEvents(stored);
258
269
  const invalidActivity = invalidActivityEventDiagnostic(malformed);
259
270
  if (invalidActivity)
260
271
  diagnostics.push(invalidActivity);
261
- const activityAtMs = latestValidEventAtMs(events) ?? fallbackAtMs;
272
+ const latest = latestValidEvent(events) ?? validFallback;
262
273
  return {
263
274
  ...(events.length > 0 ? { activity: foldPublicProjectionActivity(events) } : {}),
264
- ...(activityAtMs !== undefined
275
+ ...(latest !== undefined
265
276
  ? {
266
- activityAtMs,
267
- activityAgeMs: Math.max(0, nowMs - activityAtMs),
277
+ activityAt: latest.at,
278
+ activityAtMs: latest.atMs,
279
+ activityAgeMs: Math.max(0, nowMs - latest.atMs),
268
280
  }
269
281
  : {}),
270
282
  };
271
283
  }
284
+ function terminalTimestamp(current) {
285
+ return extractTerminalTimestamp(current?.verdict?.facts);
286
+ }
272
287
  function readProjectionRow(projectionDir, id, alias, nowMs) {
273
288
  const diagnostics = [];
289
+ const snapshot = observeProjectionLifeSnapshot(projectionDir, {
290
+ nowMs,
291
+ startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
292
+ });
293
+ if (snapshot.compatibility === "incompatible") {
294
+ return {
295
+ id,
296
+ ...(alias ? { alias } : {}),
297
+ state: "incompatible",
298
+ observation: snapshot,
299
+ tellCounts: emptyTellCounts(),
300
+ diagnostics,
301
+ };
302
+ }
274
303
  const identityFacts = readIdentityFacts(projectionDir, diagnostics);
275
304
  const tellCounts = identityFacts.identity
276
305
  ? readHeartTellCounts(projectionDir, diagnostics)
277
306
  : emptyTellCounts();
278
307
  if (!identityFacts.identity) {
308
+ const diagnostic = diagnostics.map((entry) => entry.detail).find((detail) => detail !== undefined)
309
+ ?? "projection identity could not be observed";
279
310
  return {
280
311
  id,
312
+ ...(alias ? { alias } : {}),
281
313
  state: "unknown",
314
+ observation: {
315
+ compatibility: "current",
316
+ lifecycle: { phase: "unknown", diagnostic },
317
+ },
282
318
  tellCounts,
283
319
  diagnostics,
284
320
  };
285
321
  }
286
- const observation = observeProjectionLifeSnapshot(projectionDir, {
287
- nowMs,
288
- startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
289
- });
290
- const life = observation.life;
291
- const current = observation.records
292
- ? selectCurrentGeneration(observation.records)
322
+ const life = snapshot.life;
323
+ const current = snapshot.records
324
+ ? selectCurrentGeneration(snapshot.records)
293
325
  : null;
294
326
  const adoptedAt = current?.adoption?.facts.adoptedAt;
295
327
  const adoptedAtMs = typeof adoptedAt === "string"
296
328
  ? Date.parse(adoptedAt)
297
329
  : Number.NaN;
298
- const activityFacts = readActivityFacts(projectionDir, life.executionId, nowMs, diagnostics, Number.isFinite(adoptedAtMs) ? adoptedAtMs : undefined);
330
+ const activityFacts = readActivityFacts(projectionDir, life.executionId, nowMs, diagnostics, typeof adoptedAt === "string" && Number.isFinite(adoptedAtMs) ? adoptedAt : identityFacts.mintedAt);
299
331
  const mintedAtMs = Date.parse(identityFacts.mintedAt ?? "");
332
+ const terminalAt = terminalTimestamp(current);
333
+ const durationAnchorMs = current?.verdict
334
+ ? (terminalAt === undefined ? Number.NaN : Date.parse(terminalAt))
335
+ : nowMs;
336
+ const durationMs = Number.isFinite(mintedAtMs) && Number.isFinite(durationAnchorMs)
337
+ ? Math.max(0, durationAnchorMs - mintedAtMs)
338
+ : undefined;
300
339
  const displayAgeMs = activityFacts.activityAgeMs
301
340
  ?? (Number.isFinite(mintedAtMs) ? Math.max(0, nowMs - mintedAtMs) : undefined);
302
341
  if (life.phase === "unknown" && life.diagnostic) {
@@ -311,10 +350,13 @@ function readProjectionRow(projectionDir, id, alias, nowMs) {
311
350
  id,
312
351
  ...(alias ? { alias } : {}),
313
352
  state,
353
+ observation: { compatibility: "current", lifecycle: life },
314
354
  akuma: identityFacts.akuma,
315
355
  provider: identityFacts.provider,
316
356
  pid: identityFacts.pid,
317
357
  mintedAt: identityFacts.mintedAt,
358
+ ...(terminalAt ? { terminalAt } : {}),
359
+ ...(durationMs !== undefined ? { durationMs } : {}),
318
360
  ...(identityFacts.workspaceRoot ? { workspaceRoot: identityFacts.workspaceRoot } : {}),
319
361
  ...activityFacts,
320
362
  ...(displayAgeMs !== undefined ? { activityAgeMs: displayAgeMs } : {}),
@@ -448,10 +490,13 @@ export async function hasStrandableProjectionGeneration(input) {
448
490
  || !worktreeContainsCoordinate(input.deliveryWorktree, identity.workspaceRoot)) {
449
491
  continue;
450
492
  }
451
- const phase = observeProjectionLifeSnapshot(projectionDir, {
493
+ const observation = observeProjectionLifeSnapshot(projectionDir, {
452
494
  nowMs: input.nowMs,
453
495
  startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
454
- }).life.phase;
496
+ });
497
+ if (observation.compatibility !== "current")
498
+ continue;
499
+ const phase = observation.life.phase;
455
500
  if (phase === "active"
456
501
  || phase === "minting"
457
502
  || phase === "startup-timeout"
@@ -3,6 +3,8 @@ import { RESPONSE_ARTIFACT_ID_RE } from "../response-artifact-id.js";
3
3
  import { OUTCOME_ERROR_VERSION } from "../../agents/types.js";
4
4
  const HARNESS_FAILURE_CODES = new Set([
5
5
  "SUBAGENT_TIMEOUT",
6
+ "SUBAGENT_TRANSPORT_ERROR",
7
+ "SUBAGENT_POLICY_BUDGET",
6
8
  "SUBAGENT_NON_ZERO_EXIT",
7
9
  "SUBAGENT_SPAWN_ERROR",
8
10
  "SUBAGENT_EXEC_ERROR",
@@ -4,6 +4,7 @@ import { foldPublicProjectionActivity } from "./projection-activity.js";
4
4
  import { readGenerationEvents, isProjectionId, projectionRoot, } from "./projection-core.js";
5
5
  import { readProjectionIdentity } from "./projection-identity.js";
6
6
  import { RESPONSE_ARTIFACT_ID_RE } from "../response-artifact-id.js";
7
+ import { extractTerminalTimestamp } from "./generation/model.js";
7
8
  import { PROJECTION_ADOPTION_TIMEOUT_MS } from "./generation/projection-generation-launcher.js";
8
9
  import { observeProjectionLifeSnapshot } from "./projection-life-observer.js";
9
10
  import { selectCurrentGeneration } from "./projection-life-protocol.js";
@@ -100,13 +101,7 @@ function relativeEventLedgerFile(executionId) {
100
101
  return `${executionId}.events.jsonl`;
101
102
  }
102
103
  function timestampFact(current, nowMs) {
103
- const facts = current.verdict?.facts;
104
- for (const key of ["completedAt", "failedAt", "dismissedAt", "killedAt"]) {
105
- const value = facts?.[key];
106
- if (typeof value === "string" && Number.isFinite(Date.parse(value)))
107
- return value;
108
- }
109
- return new Date(nowMs).toISOString();
104
+ return extractTerminalTimestamp(current.verdict?.facts) ?? new Date(nowMs).toISOString();
110
105
  }
111
106
  function verdictFromCurrent(current, nowMs) {
112
107
  if (!current.verdict)
@@ -180,10 +175,12 @@ export async function waitForProjection(input) {
180
175
  nowMs: observedAtMs,
181
176
  startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
182
177
  });
183
- const current = observation.records
178
+ const current = observation.compatibility === "current" && observation.records
184
179
  ? selectCurrentGeneration(observation.records)
185
180
  : null;
186
- const life = observation.life;
181
+ const life = observation.compatibility === "current"
182
+ ? observation.life
183
+ : { phase: "unknown", diagnostic: observation.diagnostic };
187
184
  const verdict = current ? verdictFromCurrent(current, observedAtMs) : undefined;
188
185
  const terminal = verdict !== undefined;
189
186
  const terminalAtMs = verdict === undefined ? undefined : Date.parse(verdict.at);
@@ -6,9 +6,25 @@ import { applyContinuationEffort, snapshotPendingTells, } from "./generation/pro
6
6
  import { createProjectionExecutionId } from "./generation/projection-generation-identity.js";
7
7
  import { PROJECTION_ADOPTION_POLL_MS, PROJECTION_ADOPTION_TIMEOUT_MS, startProjectionGeneration, } from "./generation/projection-generation-launcher.js";
8
8
  import { openProjectionGenerationStore, openProjectionGenerationStoreReadOnly, } from "./generation/store.js";
9
- import { observeProjectionLife } from "./projection-life-observer.js";
9
+ import { observeProjectionLifeSnapshot } from "./projection-life-observer.js";
10
10
  import { observeProjectionRunnerLock } from "./projection-runner-lock.js";
11
11
  import { setTimeout as delay } from "node:timers/promises";
12
+ function incompatibleProjectionError(projectionId) {
13
+ const subject = projectionId ? `Projection '${projectionId}'` : "Projection";
14
+ return new FlowError("PROJECTION_INCOMPATIBLE", `${subject} has incompatible historical storage; keiyaku status --all can inspect it, but tell cannot change it.`);
15
+ }
16
+ /**
17
+ * Tell is a current-schema mutation only. This gate runs before the handler
18
+ * writes an inbox row, so historical storage never acquires pending mail.
19
+ */
20
+ export function assertProjectionTellable(projectionDirectory) {
21
+ const observation = observeProjectionLifeSnapshot(projectionDirectory, {
22
+ nowMs: Date.now(),
23
+ startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
24
+ });
25
+ if (observation.compatibility === "incompatible")
26
+ throw incompatibleProjectionError();
27
+ }
12
28
  function readCurrentGeneration(projectionDirectory) {
13
29
  const store = openProjectionGenerationStoreReadOnly(projectionDirectory);
14
30
  try {
@@ -28,7 +44,7 @@ function successorFacts(current, snapshot, nowMs, requireContinuation) {
28
44
  : {};
29
45
  const session = verdictFacts?.session ?? inputs.session;
30
46
  const launchSnapshot = parseResolvedAkumaLaunchSnapshot(inputs.launchSnapshot);
31
- const recoverableSession = isRecoverableSessionEnvelope(launchSnapshot.provider.name, session);
47
+ const recoverableSession = isRecoverableSessionEnvelope(launchSnapshot.provider.kind, session);
32
48
  if (requireContinuation && !recoverableSession) {
33
49
  throw new FlowError("PROJECTION_WAKE_FAILED", "The tell was saved, but the successor could not be started because its provider session is not recoverable.");
34
50
  }
@@ -86,7 +102,7 @@ async function startCommittedSuccessor(projectionDirectory, projectionId, execut
86
102
  const started = await startGeneration(projectionDirectory, executionId);
87
103
  if (started.status === "adopted")
88
104
  return "woken";
89
- throw new FlowError("PROJECTION_WAKE_FAILED", `The tell was saved for '${projectionId}', but its successor could not be started.`, {
105
+ throw new FlowError("PROJECTION_WAKE_FAILED", `The tell was saved for '${projectionId}', but its successor could not be started (cause: ${started.cause}).`, {
90
106
  facts: {
91
107
  kind: "projection_wake_failure",
92
108
  projectionId,
@@ -96,7 +112,7 @@ async function startCommittedSuccessor(projectionDirectory, projectionId, execut
96
112
  });
97
113
  }
98
114
  function sessionWakeFailure(projectionId, cause) {
99
- return new FlowError("PROJECTION_WAKE_FAILED", `The tell was saved for '${projectionId}', but its successor could not be started.`, {
115
+ return new FlowError("PROJECTION_WAKE_FAILED", `The tell was saved for '${projectionId}', but its successor could not be started (cause: session_unrecoverable).`, {
100
116
  cause,
101
117
  facts: {
102
118
  kind: "projection_wake_failure",
@@ -112,10 +128,14 @@ export async function tellProjection(projectionDirectory, projectionId, effort,
112
128
  const observationStartedAt = Date.now();
113
129
  for (;;) {
114
130
  const nowMs = Date.now();
115
- const life = observeProjectionLife(projectionDirectory, {
131
+ const observation = observeProjectionLifeSnapshot(projectionDirectory, {
116
132
  nowMs,
117
133
  startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
118
134
  });
135
+ if (observation.compatibility === "incompatible") {
136
+ throw incompatibleProjectionError(projectionId);
137
+ }
138
+ const life = observation.life;
119
139
  if (life.phase === "active")
120
140
  return "active";
121
141
  if (life.phase === "minting" || life.phase === "quiescent") {
@@ -148,6 +168,7 @@ export async function tellProjection(projectionDirectory, projectionId, effort,
148
168
  const store = openProjectionGenerationStore(projectionDirectory);
149
169
  try {
150
170
  if (life.phase === "startup-timeout") {
171
+ const terminalAt = new Date(nowMs).toISOString();
151
172
  const transition = store.supersedeStartupTimeout({
152
173
  executionId: current.launch.executionId,
153
174
  observedAtMs: nowMs,
@@ -155,6 +176,7 @@ export async function tellProjection(projectionDirectory, projectionId, effort,
155
176
  diagnostic: { detail: "runner did not adopt before startup timeout" },
156
177
  successorExecutionId,
157
178
  successorFacts: facts,
179
+ terminalAt,
158
180
  });
159
181
  if (transition.status === "committed") {
160
182
  return await startCommittedSuccessor(projectionDirectory, projectionId, successorExecutionId, startGeneration);
@@ -168,11 +190,13 @@ export async function tellProjection(projectionDirectory, projectionId, effort,
168
190
  const lock = observeProjectionRunnerLock(projectionDirectory);
169
191
  if (lock.state !== "released")
170
192
  return lock.state === "held" ? "active" : "pending";
193
+ const terminalAt = new Date(nowMs).toISOString();
171
194
  const transition = store.replaceDeadRunner({
172
195
  executionId: current.launch.executionId,
173
- releasedLockEvidence: { observedAt: new Date(nowMs).toISOString() },
196
+ releasedLockEvidence: { observedAt: terminalAt },
174
197
  successorExecutionId,
175
198
  successorFacts: facts,
199
+ terminalAt,
176
200
  });
177
201
  if (transition.status === "committed") {
178
202
  return await startCommittedSuccessor(projectionDirectory, projectionId, successorExecutionId, startGeneration);