@astrosheep/keiyaku 1.0.0 → 1.0.2

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 (123) hide show
  1. package/README.md +73 -56
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/call-terms_v2.js +103 -0
  4. package/build/agents/harness/activity-values.js +54 -0
  5. package/build/agents/harness/event-channel.js +55 -20
  6. package/build/agents/harness/events.js +47 -1
  7. package/build/agents/harness/execution-handle.js +292 -61
  8. package/build/agents/harness/index.js +103 -121
  9. package/build/agents/harness/pump.js +36 -47
  10. package/build/agents/harness/runtime.js +3 -0
  11. package/build/agents/launch-snapshot_v2.js +348 -0
  12. package/build/agents/opencode-sdk.js +42 -169
  13. package/build/agents/providers/claude-agent-sdk.js +240 -79
  14. package/build/agents/providers/codex-app-server.js +280 -72
  15. package/build/agents/providers/codex-sdk.js +159 -41
  16. package/build/agents/providers/event-type-tail.js +16 -0
  17. package/build/agents/providers/opencode-sdk.js +165 -59
  18. package/build/agents/providers/pi.js +254 -42
  19. package/build/agents/selector_v2.js +122 -0
  20. package/build/cli/commands/akuma.js +120 -42
  21. package/build/cli/completion.js +6 -7
  22. package/build/cli/flags.js +36 -10
  23. package/build/cli/help.js +42 -26
  24. package/build/cli/index.js +225 -93
  25. package/build/cli/parse.js +194 -75
  26. package/build/cli/projection-address.js +11 -0
  27. package/build/cli/render/address.js +88 -0
  28. package/build/cli/render/arc.js +10 -10
  29. package/build/cli/render/call.js +59 -0
  30. package/build/cli/render/compact-text.js +3 -0
  31. package/build/cli/render/errors.js +4 -2
  32. package/build/cli/render/format.js +6 -0
  33. package/build/cli/render/line-width.js +70 -0
  34. package/build/cli/render/misc.js +7 -4
  35. package/build/cli/render/petition.js +4 -3
  36. package/build/cli/render/projection-activity.js +201 -0
  37. package/build/cli/render/shared.js +46 -12
  38. package/build/cli/render/status.js +59 -10
  39. package/build/cli/render/tool-ledger-rollup.js +74 -0
  40. package/build/cli/render/tool-presentation.js +97 -0
  41. package/build/cli/render/wait.js +229 -0
  42. package/build/cli/skills-install.js +42 -24
  43. package/build/cli/subagent-guard.js +1 -1
  44. package/build/config/akuma-loader_v2.js +222 -0
  45. package/build/config/env-keys.js +0 -4
  46. package/build/config/env.js +0 -6
  47. package/build/config/settings.js +325 -12
  48. package/build/core/addressing.js +224 -0
  49. package/build/core/amend.js +30 -47
  50. package/build/core/arc.js +68 -60
  51. package/build/core/bind.js +105 -25
  52. package/build/core/call-persist.js +69 -0
  53. package/build/core/call.js +658 -0
  54. package/build/core/claim.js +72 -44
  55. package/build/core/command-io.js +19 -19
  56. package/build/core/context.js +11 -36
  57. package/build/core/draft.js +2 -3
  58. package/build/core/entry.js +101 -6
  59. package/build/core/execution-coordinate.js +62 -0
  60. package/build/core/execution-pact.js +141 -0
  61. package/build/core/forfeit.js +2 -3
  62. package/build/core/hints.js +10 -10
  63. package/build/core/ids.js +6 -0
  64. package/build/core/ledger.js +16 -1
  65. package/build/core/petition-claim-gates.js +14 -12
  66. package/build/core/petition-claim.js +1 -0
  67. package/build/core/petition-run.js +6 -2
  68. package/build/core/petition.js +63 -21
  69. package/build/core/places.js +106 -13
  70. package/build/core/projection/generation.js +412 -0
  71. package/build/core/projection/heart.js +316 -0
  72. package/build/core/projection/identity.js +81 -0
  73. package/build/core/projection/leash.js +87 -0
  74. package/build/core/projection/mint.js +154 -0
  75. package/build/core/projection-activity.js +219 -0
  76. package/build/core/projection-coordinate.js +35 -0
  77. package/build/core/projection-core.js +69 -463
  78. package/build/core/projection-generation-continuation.js +31 -0
  79. package/build/core/projection-generation-execution.js +148 -0
  80. package/build/core/projection-generation-identity.js +17 -0
  81. package/build/core/projection-generation-launcher.js +90 -0
  82. package/build/core/projection-generation-process.js +43 -0
  83. package/build/core/projection-generation-runner.js +144 -0
  84. package/build/core/projection-generation-runtime.js +15 -0
  85. package/build/core/projection-generation-store.js +707 -0
  86. package/build/core/projection-identity.js +11 -0
  87. package/build/core/projection-life-observer.js +87 -0
  88. package/build/core/projection-life-protocol.js +95 -0
  89. package/build/core/projection-mint.js +96 -24
  90. package/build/core/projection-runner-lock.js +230 -0
  91. package/build/core/projection-status.js +207 -107
  92. package/build/core/projection-tells.js +401 -0
  93. package/build/core/projection-wait.js +155 -0
  94. package/build/core/projection-wake.js +109 -231
  95. package/build/core/queue_v2.js +342 -0
  96. package/build/core/registry.js +6 -3
  97. package/build/core/render.js +63 -1
  98. package/build/core/renew.js +80 -53
  99. package/build/core/scope.js +186 -137
  100. package/build/core/seal.js +16 -19
  101. package/build/core/status.js +88 -25
  102. package/build/core/stored-agent-event.js +101 -0
  103. package/build/core/transcripts.js +214 -228
  104. package/build/core/verdict.js +27 -15
  105. package/build/core/worktree-bootstrap.js +187 -0
  106. package/build/core/worktree-path.js +72 -18
  107. package/build/flow-error.js +4 -6
  108. package/build/generated/version.js +1 -1
  109. package/build/git/branches.js +6 -1
  110. package/build/index.js +8 -5
  111. package/build/keiyaku.js +1 -3
  112. package/package.json +6 -5
  113. package/skills/keiyaku/SKILL.md +6 -47
  114. package/skills/keiyaku-akuma/SKILL.md +75 -0
  115. package/build/agents/effective-policy.js +0 -32
  116. package/build/agents/index.js +0 -114
  117. package/build/agents/selector.js +0 -28
  118. package/build/cli/render/summon.js +0 -17
  119. package/build/cli/render/tell.js +0 -11
  120. package/build/config/akuma-loader.js +0 -268
  121. package/build/core/queue.js +0 -73
  122. package/build/core/summon-persist.js +0 -73
  123. package/build/core/summon.js +0 -377
@@ -0,0 +1,401 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import { ProjectionStateError, randomHex8 } from "./atomic-publish.js";
5
+ import { PROJECTION_GENERATION_STORE_FILE, } from "./projection-generation-store.js";
6
+ import { configureHeartBusyTimeout } from "./projection/heart.js";
7
+ export const PROJECTION_TELL_STATES = [
8
+ "inbox",
9
+ "inflight",
10
+ "submitted",
11
+ "consumed",
12
+ "delivered",
13
+ ];
14
+ const PENDING_TELL_LIMIT = 10_000;
15
+ function databasePath(projectionDirectory) {
16
+ return path.join(projectionDirectory, PROJECTION_GENERATION_STORE_FILE);
17
+ }
18
+ function openDatabase(projectionDirectory, readOnly) {
19
+ const target = databasePath(projectionDirectory);
20
+ if (!fs.lstatSync(target).isFile()) {
21
+ throw new ProjectionStateError("projection heart must be a regular file");
22
+ }
23
+ const database = new DatabaseSync(target, {
24
+ readOnly,
25
+ enableForeignKeyConstraints: true,
26
+ enableDoubleQuotedStringLiterals: false,
27
+ allowExtension: false,
28
+ });
29
+ configureHeartBusyTimeout(database);
30
+ if (!readOnly) {
31
+ database.exec("PRAGMA foreign_keys = ON");
32
+ database.exec("PRAGMA trusted_schema = OFF");
33
+ }
34
+ return database;
35
+ }
36
+ function rollbackQuietly(database) {
37
+ try {
38
+ database.exec("ROLLBACK");
39
+ }
40
+ catch {
41
+ // Preserve the transition failure.
42
+ }
43
+ }
44
+ function withRead(projectionDirectory, read) {
45
+ const database = openDatabase(projectionDirectory, true);
46
+ try {
47
+ return read(database);
48
+ }
49
+ finally {
50
+ database.close();
51
+ }
52
+ }
53
+ function withWrite(projectionDirectory, write) {
54
+ const database = openDatabase(projectionDirectory, false);
55
+ try {
56
+ database.exec("BEGIN IMMEDIATE");
57
+ try {
58
+ const result = write(database);
59
+ database.exec("COMMIT");
60
+ return result;
61
+ }
62
+ catch (error) {
63
+ rollbackQuietly(database);
64
+ throw error;
65
+ }
66
+ }
67
+ finally {
68
+ database.close();
69
+ }
70
+ }
71
+ function requireExecutionId(value) {
72
+ if (typeof value !== "string" || value.trim() === "") {
73
+ throw new ProjectionStateError("submitted tell executionId must be a non-empty string");
74
+ }
75
+ return value;
76
+ }
77
+ function parseFence(value) {
78
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
79
+ throw new ProjectionStateError("tell fence is required");
80
+ }
81
+ const record = value;
82
+ if (record.kind === "turn" && typeof record.turnId === "string" && record.turnId.length > 0) {
83
+ return { kind: "turn", turnId: record.turnId };
84
+ }
85
+ if (record.kind === "ordinal"
86
+ && typeof record.ordinal === "number"
87
+ && Number.isSafeInteger(record.ordinal)
88
+ && record.ordinal >= 0) {
89
+ return { kind: "ordinal", ordinal: record.ordinal };
90
+ }
91
+ throw new ProjectionStateError("tell fence must be turn(string) or ordinal(non-negative integer)");
92
+ }
93
+ function decodeJson(value, location) {
94
+ try {
95
+ return JSON.parse(value);
96
+ }
97
+ catch (error) {
98
+ throw new ProjectionStateError(`${location} contains malformed JSON`, { cause: error });
99
+ }
100
+ }
101
+ function parseReplay(value) {
102
+ if (value === null)
103
+ return [];
104
+ const decoded = decodeJson(value, "tell replay history");
105
+ if (!Array.isArray(decoded))
106
+ throw new ProjectionStateError("tell replay history must be an array");
107
+ return decoded.map((item) => {
108
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
109
+ throw new ProjectionStateError("tell replay history entry must be an object");
110
+ }
111
+ const entry = item;
112
+ return {
113
+ executionId: requireExecutionId(entry.executionId),
114
+ fence: parseFence(entry.fence),
115
+ };
116
+ });
117
+ }
118
+ function originalFromRow(row) {
119
+ return {
120
+ version: 1,
121
+ text: row.text,
122
+ createdAt: row.created_at,
123
+ ...(row.effort ? { effort: row.effort } : {}),
124
+ };
125
+ }
126
+ function submittedFromRow(row) {
127
+ if (row.state !== "submitted" && row.state !== "consumed" && row.state !== "delivered") {
128
+ throw new ProjectionStateError(`tell '${row.tell_id}' is not submitted`);
129
+ }
130
+ if (row.execution_id === null || row.fence_json === null) {
131
+ throw new ProjectionStateError(`tell '${row.tell_id}' is missing submission facts`);
132
+ }
133
+ const replayedFrom = parseReplay(row.replayed_from_json);
134
+ return {
135
+ ...originalFromRow(row),
136
+ executionId: requireExecutionId(row.execution_id),
137
+ fence: parseFence(decodeJson(row.fence_json, "tell fence")),
138
+ ...(replayedFrom.length > 0 ? { replayedFrom } : {}),
139
+ };
140
+ }
141
+ function selectTell(database, tellId) {
142
+ return database.prepare(`
143
+ SELECT
144
+ tell_id,
145
+ text,
146
+ created_at,
147
+ effort,
148
+ state,
149
+ execution_id,
150
+ fence_json,
151
+ replayed_from_json,
152
+ delivered_at
153
+ FROM projection_tells
154
+ WHERE tell_id = ?
155
+ `).get(tellId);
156
+ }
157
+ function assertTellIdRandomHex(randomHex) {
158
+ if (!/^[0-9a-f]{8}$/.test(randomHex)) {
159
+ throw new ProjectionStateError(`tell id random segment must be 8 lowercase hex chars, got ${randomHex}`);
160
+ }
161
+ return randomHex;
162
+ }
163
+ export function listTellIds(projectionDirectory, state) {
164
+ return withRead(projectionDirectory, (database) => database.prepare(`
165
+ SELECT tell_id
166
+ FROM projection_tells
167
+ WHERE state = ?
168
+ ORDER BY tell_id
169
+ `).all(state).map((row) => row.tell_id));
170
+ }
171
+ export function readTellOriginal(projectionDirectory, state, tellId) {
172
+ return withRead(projectionDirectory, (database) => {
173
+ const row = selectTell(database, tellId);
174
+ if (!row || row.state !== state) {
175
+ throw new ProjectionStateError(`tell '${tellId}' is not in ${state}`);
176
+ }
177
+ return originalFromRow(row);
178
+ });
179
+ }
180
+ export function readTellSubmitted(projectionDirectory, tellId) {
181
+ return withRead(projectionDirectory, (database) => {
182
+ const row = selectTell(database, tellId);
183
+ if (!row)
184
+ throw new ProjectionStateError(`tell '${tellId}' does not exist`);
185
+ return submittedFromRow(row);
186
+ });
187
+ }
188
+ export function writeInboxTell(projectionDirectory, text, nowMs, options) {
189
+ if (!Number.isSafeInteger(nowMs) || nowMs < 0) {
190
+ throw new ProjectionStateError("nowMs must be a safe non-negative integer");
191
+ }
192
+ const tellId = `${nowMs}-${assertTellIdRandomHex((options?.randomHex ?? randomHex8)())}`;
193
+ return withWrite(projectionDirectory, (database) => {
194
+ const pending = database.prepare(`
195
+ SELECT COUNT(*) AS count
196
+ FROM projection_tells
197
+ WHERE state IN ('inbox', 'inflight', 'submitted', 'delivered')
198
+ `).get();
199
+ if (Number(pending.count) >= PENDING_TELL_LIMIT) {
200
+ throw new ProjectionStateError(`projection has reached the ${PENDING_TELL_LIMIT} pending tell limit`);
201
+ }
202
+ try {
203
+ database.prepare(`
204
+ INSERT INTO projection_tells (
205
+ tell_id, text, created_at, effort, state
206
+ ) VALUES (?, ?, ?, ?, 'inbox')
207
+ `).run(tellId, text, new Date(nowMs).toISOString(), options?.effort ?? null);
208
+ }
209
+ catch (error) {
210
+ throw new ProjectionStateError(`tell id already exists: ${tellId}`, { cause: error });
211
+ }
212
+ return tellId;
213
+ });
214
+ }
215
+ export function claimTellToInflight(projectionDirectory, tellId) {
216
+ return withWrite(projectionDirectory, (database) => {
217
+ const result = database.prepare(`
218
+ UPDATE projection_tells
219
+ SET state = 'inflight'
220
+ WHERE tell_id = ? AND state = 'inbox'
221
+ `).run(tellId);
222
+ return Number(result.changes) === 1;
223
+ });
224
+ }
225
+ export function claimAllInbox(projectionDirectory) {
226
+ return withWrite(projectionDirectory, (database) => {
227
+ const rows = database.prepare(`
228
+ SELECT tell_id
229
+ FROM projection_tells
230
+ WHERE state = 'inbox'
231
+ ORDER BY tell_id
232
+ `).all();
233
+ for (const row of rows) {
234
+ database.prepare(`
235
+ UPDATE projection_tells
236
+ SET state = 'inflight'
237
+ WHERE tell_id = ? AND state = 'inbox'
238
+ `).run(row.tell_id);
239
+ }
240
+ return rows.map((row) => row.tell_id);
241
+ });
242
+ }
243
+ export function claimFencedTells(projectionDirectory, fence, executionId) {
244
+ const owner = requireExecutionId(executionId);
245
+ return withWrite(projectionDirectory, (database) => {
246
+ const claimed = [];
247
+ const seen = new Set();
248
+ for (const tellId of fence) {
249
+ if (seen.has(tellId))
250
+ throw new ProjectionStateError(`duplicate tell in generation fence: ${tellId}`);
251
+ seen.add(tellId);
252
+ const row = selectTell(database, tellId);
253
+ if (!row)
254
+ throw new ProjectionStateError(`generation tell fence member is missing: ${tellId}`);
255
+ if (row.state === "inbox") {
256
+ database.prepare(`
257
+ UPDATE projection_tells
258
+ SET state = 'inflight'
259
+ WHERE tell_id = ? AND state = 'inbox'
260
+ `).run(tellId);
261
+ claimed.push(tellId);
262
+ continue;
263
+ }
264
+ if (row.state === "inflight") {
265
+ claimed.push(tellId);
266
+ continue;
267
+ }
268
+ if (row.state === "consumed" && row.execution_id === owner)
269
+ continue;
270
+ throw new ProjectionStateError(`generation tell fence member is not claimable: ${tellId}`);
271
+ }
272
+ return claimed;
273
+ });
274
+ }
275
+ export function markTellSubmitted(projectionDirectory, tellId, fields) {
276
+ const executionId = requireExecutionId(fields.executionId);
277
+ const fence = parseFence(fields.fence);
278
+ const replayedFrom = (fields.replayedFrom ?? []).map((entry) => ({
279
+ executionId: requireExecutionId(entry.executionId),
280
+ fence: parseFence(entry.fence),
281
+ }));
282
+ withWrite(projectionDirectory, (database) => {
283
+ const row = selectTell(database, tellId);
284
+ if (!row)
285
+ throw new ProjectionStateError(`tell '${tellId}' does not exist`);
286
+ if (row.state === "submitted")
287
+ return;
288
+ if (row.state !== "inflight") {
289
+ throw new ProjectionStateError(`tell '${tellId}' is not inflight`);
290
+ }
291
+ database.prepare(`
292
+ UPDATE projection_tells
293
+ SET
294
+ state = 'submitted',
295
+ execution_id = ?,
296
+ fence_json = ?,
297
+ replayed_from_json = ?
298
+ WHERE tell_id = ? AND state = 'inflight'
299
+ `).run(executionId, JSON.stringify(fence), replayedFrom.length > 0 ? JSON.stringify(replayedFrom) : null, tellId);
300
+ });
301
+ }
302
+ export function markTellConsumed(projectionDirectory, tellId) {
303
+ withWrite(projectionDirectory, (database) => {
304
+ const result = database.prepare(`
305
+ UPDATE projection_tells
306
+ SET state = 'consumed', delivered_at = NULL
307
+ WHERE tell_id = ? AND state IN ('submitted', 'delivered')
308
+ `).run(tellId);
309
+ if (Number(result.changes) !== 1) {
310
+ throw new ProjectionStateError(`tell '${tellId}' is not submitted or delivered`);
311
+ }
312
+ });
313
+ }
314
+ export function readProjectionTellCounts(projectionDirectory) {
315
+ return withRead(projectionDirectory, (database) => {
316
+ const counts = {
317
+ inbox: 0,
318
+ inflight: 0,
319
+ submitted: 0,
320
+ consumed: 0,
321
+ delivered: 0,
322
+ };
323
+ const rows = database.prepare(`
324
+ SELECT state, COUNT(*) AS count
325
+ FROM projection_tells
326
+ GROUP BY state
327
+ `).all();
328
+ for (const row of rows)
329
+ counts[row.state] = Number(row.count);
330
+ return counts;
331
+ });
332
+ }
333
+ export function snapshotPendingTells(projectionDirectory) {
334
+ return withRead(projectionDirectory, (database) => {
335
+ const rows = database.prepare(`
336
+ SELECT tell_id, effort
337
+ FROM projection_tells
338
+ WHERE state IN ('inbox', 'inflight', 'submitted', 'delivered')
339
+ ORDER BY tell_id
340
+ `).all();
341
+ const effort = rows.reduce((selected, row) => row.effort ?? selected, undefined);
342
+ return {
343
+ tellIds: rows.map((row) => row.tell_id),
344
+ ...(effort ? { effort } : {}),
345
+ };
346
+ });
347
+ }
348
+ export function demoteSubmittedTellsForReplay(projectionDirectory, executionId) {
349
+ const currentExecutionId = requireExecutionId(executionId);
350
+ return withWrite(projectionDirectory, (database) => {
351
+ const rows = database.prepare(`
352
+ SELECT
353
+ tell_id,
354
+ text,
355
+ created_at,
356
+ effort,
357
+ state,
358
+ execution_id,
359
+ fence_json,
360
+ replayed_from_json,
361
+ delivered_at
362
+ FROM projection_tells
363
+ WHERE state IN ('submitted', 'delivered')
364
+ ORDER BY tell_id
365
+ `).all();
366
+ for (const row of rows) {
367
+ const submitted = submittedFromRow(row);
368
+ if (submitted.executionId === currentExecutionId) {
369
+ throw new ProjectionStateError(`submitted tell belongs to current execution: ${row.tell_id}`);
370
+ }
371
+ const replayedFrom = [
372
+ ...(submitted.replayedFrom ?? []),
373
+ { executionId: submitted.executionId, fence: submitted.fence },
374
+ ];
375
+ database.prepare(`
376
+ UPDATE projection_tells
377
+ SET
378
+ state = 'inflight',
379
+ execution_id = NULL,
380
+ fence_json = NULL,
381
+ replayed_from_json = ?,
382
+ delivered_at = NULL
383
+ WHERE tell_id = ? AND state IN ('submitted', 'delivered')
384
+ `).run(JSON.stringify(replayedFrom), row.tell_id);
385
+ }
386
+ return rows.map((row) => row.tell_id);
387
+ });
388
+ }
389
+ /** The only tell-consumption predicate. */
390
+ export function isTellCausallyConsumed(submitted, checkpoint) {
391
+ if (submitted.kind !== checkpoint.kind) {
392
+ throw new ProjectionStateError(`tell fence kind mismatch: ${submitted.kind} vs ${checkpoint.kind}`);
393
+ }
394
+ if (submitted.kind === "ordinal" && checkpoint.kind === "ordinal") {
395
+ return checkpoint.ordinal > submitted.ordinal;
396
+ }
397
+ if (submitted.kind === "turn" && checkpoint.kind === "turn") {
398
+ return checkpoint.turnId === submitted.turnId;
399
+ }
400
+ throw new ProjectionStateError("unreachable tell fence comparison");
401
+ }
@@ -0,0 +1,155 @@
1
+ import { foldAgentActivity } from "./projection-activity.js";
2
+ import { readGenerationEvents, } from "./projection-core.js";
3
+ import { readProjectionIdentity } from "./projection-identity.js";
4
+ import { PROJECTION_ADOPTION_TIMEOUT_MS } from "./projection-generation-launcher.js";
5
+ import { observeProjectionLifeSnapshot } from "./projection-life-observer.js";
6
+ import { selectCurrentGeneration } from "./projection-life-protocol.js";
7
+ import { readProjectionTellCounts } from "./projection-tells.js";
8
+ import { invalidActivityEventDiagnostic, selectValidStoredAgentEvents, } from "./stored-agent-event.js";
9
+ const WAIT_POLL_MS = 100;
10
+ function sleep(ms) {
11
+ return new Promise((resolve) => setTimeout(resolve, ms));
12
+ }
13
+ function waitingTellCount(projectionDirectory) {
14
+ const counts = readProjectionTellCounts(projectionDirectory);
15
+ return counts.inbox + counts.inflight + counts.submitted + counts.delivered;
16
+ }
17
+ function latestValidEventAgeMs(events, nowMs) {
18
+ let latest;
19
+ for (const event of events) {
20
+ const atMs = Date.parse(event.at);
21
+ if (!Number.isFinite(atMs))
22
+ continue;
23
+ if (latest === undefined || atMs > latest)
24
+ latest = atMs;
25
+ }
26
+ return latest === undefined ? undefined : Math.max(0, nowMs - latest);
27
+ }
28
+ function adoptionAgeMs(current, nowMs) {
29
+ const adoptedAt = current?.adoption?.facts.adoptedAt;
30
+ if (typeof adoptedAt !== "string")
31
+ return undefined;
32
+ const adoptedAtMs = Date.parse(adoptedAt);
33
+ return Number.isFinite(adoptedAtMs)
34
+ ? Math.max(0, nowMs - adoptedAtMs)
35
+ : undefined;
36
+ }
37
+ function relativeEventLedgerFile(executionId) {
38
+ return `${executionId}.events.jsonl`;
39
+ }
40
+ function timestampFact(current, nowMs) {
41
+ const facts = current.verdict?.facts;
42
+ for (const key of ["completedAt", "failedAt", "dismissedAt"]) {
43
+ const value = facts?.[key];
44
+ if (typeof value === "string" && Number.isFinite(Date.parse(value)))
45
+ return value;
46
+ }
47
+ return new Date(nowMs).toISOString();
48
+ }
49
+ function verdictFromCurrent(current, nowMs) {
50
+ if (!current.verdict)
51
+ return undefined;
52
+ const state = current.verdict.facts.state;
53
+ if (typeof state !== "string")
54
+ return undefined;
55
+ const detail = current.verdict.facts.detail;
56
+ return {
57
+ state: state,
58
+ at: timestampFact(current, nowMs),
59
+ ...(typeof detail === "string" && detail.trim() ? { detail } : {}),
60
+ };
61
+ }
62
+ function outcomeText(current) {
63
+ const finalMessage = current.verdict?.facts.finalMessage;
64
+ if (typeof finalMessage === "string" && finalMessage.trim())
65
+ return finalMessage;
66
+ const detail = current.verdict?.facts.detail;
67
+ return typeof detail === "string" && detail.trim() ? detail : undefined;
68
+ }
69
+ function buildSnapshot(input) {
70
+ const { projectionDirectory, pact, phase, current, events, malformedEvents, nowMs } = input;
71
+ const anchorAt = current?.verdict ? timestampFact(current, nowMs) : new Date(nowMs).toISOString();
72
+ const anchorMs = Date.parse(anchorAt);
73
+ const mintedAtMs = Date.parse(pact.mintedAt);
74
+ const activityAgeMs = latestValidEventAgeMs(events, nowMs)
75
+ ?? adoptionAgeMs(current, nowMs);
76
+ const invalidActivity = invalidActivityEventDiagnostic(malformedEvents);
77
+ const responsePath = current?.verdict?.facts.responsePath;
78
+ const artifactId = current?.verdict?.facts.artifactId;
79
+ return {
80
+ phase,
81
+ observedAtMs: Number.isFinite(anchorMs) ? anchorMs : nowMs,
82
+ durationMs: Number.isFinite(mintedAtMs) && Number.isFinite(anchorMs)
83
+ ? Math.max(0, anchorMs - mintedAtMs)
84
+ : 0,
85
+ ...(activityAgeMs !== undefined ? { activityAgeMs } : {}),
86
+ tellsWaiting: waitingTellCount(projectionDirectory),
87
+ activity: foldAgentActivity(events),
88
+ ...(invalidActivity ? { eventDiagnostics: [invalidActivity] } : {}),
89
+ workspaceRoot: pact.workspaceRoot,
90
+ ...(current ? {
91
+ executionId: current.launch.executionId,
92
+ eventLedgerFile: relativeEventLedgerFile(current.launch.executionId),
93
+ } : {}),
94
+ ...(typeof responsePath === "string" && responsePath.trim() ? { responsePath } : {}),
95
+ ...(typeof artifactId === "string" && artifactId.trim() ? { artifactId } : {}),
96
+ };
97
+ }
98
+ export async function waitForProjection(input) {
99
+ const now = input.now ?? Date.now;
100
+ let pact;
101
+ try {
102
+ pact = readProjectionIdentity(input.projectionDirectory);
103
+ }
104
+ catch (error) {
105
+ return {
106
+ state: "unknown",
107
+ diagnostic: `projection identity could not be observed: ${error instanceof Error ? error.message : String(error)}`,
108
+ };
109
+ }
110
+ const deadline = input.timeoutMs === undefined ? undefined : now() + input.timeoutMs;
111
+ for (;;) {
112
+ input.signal?.throwIfAborted();
113
+ const observedAtMs = now();
114
+ const observation = observeProjectionLifeSnapshot(input.projectionDirectory, {
115
+ nowMs: observedAtMs,
116
+ startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
117
+ });
118
+ const current = observation.records
119
+ ? selectCurrentGeneration(observation.records)
120
+ : null;
121
+ const life = observation.life;
122
+ const verdict = current ? verdictFromCurrent(current, observedAtMs) : undefined;
123
+ const terminal = verdict !== undefined;
124
+ if (terminal || (deadline !== undefined && observedAtMs >= deadline)) {
125
+ const stored = current
126
+ ? readGenerationEvents(input.projectionDirectory, current.launch.executionId)
127
+ : [];
128
+ const { events, malformed } = selectValidStoredAgentEvents(stored);
129
+ const snapshot = buildSnapshot({
130
+ projectionDirectory: input.projectionDirectory,
131
+ pact,
132
+ phase: life.phase,
133
+ current,
134
+ events,
135
+ malformedEvents: malformed,
136
+ nowMs: observedAtMs,
137
+ });
138
+ if (terminal && current) {
139
+ const text = outcomeText(current);
140
+ return {
141
+ state: "terminal",
142
+ pact,
143
+ verdict,
144
+ snapshot,
145
+ ...(text ? { outcomeText: text } : {}),
146
+ };
147
+ }
148
+ return { state: "timeout", pact, snapshot };
149
+ }
150
+ const waitMs = deadline === undefined
151
+ ? WAIT_POLL_MS
152
+ : Math.max(0, Math.min(WAIT_POLL_MS, deadline - observedAtMs));
153
+ await sleep(waitMs);
154
+ }
155
+ }