@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
@@ -1,16 +1,15 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { randomBytes } from "node:crypto";
4
3
  import { isErrnoException } from "../errno.js";
5
- import { createUlid } from "./ids.js";
6
- import { atomicPublishFile, ProjectionStateError, randomHex8, } from "./atomic-publish.js";
4
+ import { projectionCoordinateRoot } from "./projection-coordinate.js";
5
+ import { ProjectionStateError, } from "./atomic-publish.js";
6
+ import { readProjectionIdentity } from "./projection-identity.js";
7
7
  export { atomicPublishFile, ProjectionStateError, randomHex8 } from "./atomic-publish.js";
8
+ export { claimAllInbox, claimFencedTells, claimTellToInflight, demoteSubmittedTellsForReplay, isTellCausallyConsumed, listTellIds, markTellConsumed, markTellSubmitted, readTellOriginal, readTellSubmitted, snapshotPendingTells, writeInboxTell, } from "./projection-tells.js";
8
9
  /** Relative segments for `.keiyaku/projection`. */
9
10
  export const PROJECTION_ROOT_SEGMENTS = [".keiyaku", "projection"];
10
- /** Tell layer directories created at mint (and only these five). */
11
- export const PROJECTION_LAYER_DIRS = ["inbox", "inflight", "submitted", "consumed", "delivered"];
12
11
  export function projectionRoot(cwd) {
13
- return path.join(cwd, ...PROJECTION_ROOT_SEGMENTS);
12
+ return path.join(projectionCoordinateRoot(cwd), ...PROJECTION_ROOT_SEGMENTS);
14
13
  }
15
14
  export function projectionDir(cwd, id) {
16
15
  return path.join(projectionRoot(cwd), id);
@@ -24,21 +23,17 @@ export function isoFromNowMs(nowMs) {
24
23
  }
25
24
  return new Date(nowMs).toISOString();
26
25
  }
27
- /** Projection execution IDs share the repository's ULID family. */
28
- export function createProjectionExecutionId(nowMs = Date.now()) {
29
- return createUlid(nowMs, [...randomBytes(10)]);
30
- }
31
- export function isReadyProjection(dir) {
26
+ /** A published projection is any visible non-dot real directory under the projection root. */
27
+ export function isPublishedProjection(dir) {
32
28
  try {
33
- fs.accessSync(path.join(dir, "ready"));
34
- return true;
29
+ return !path.basename(dir).startsWith(".") && fs.lstatSync(dir).isDirectory();
35
30
  }
36
31
  catch {
37
32
  return false;
38
33
  }
39
34
  }
40
- /** readdir-based prefix resolution. Only ready projections are candidates. Ambiguity lists candidates; never guesses. */
41
- export function resolveProjectionPrefix(cwd, prefix) {
35
+ /** readdir-based address resolution. Atomic mint rename makes every visible non-dot directory published. */
36
+ export function resolveProjectionPrefix(cwd, prefix, commissionId) {
42
37
  const needle = prefix.trim();
43
38
  if (!needle) {
44
39
  return { status: "none" };
@@ -54,19 +49,26 @@ export function resolveProjectionPrefix(cwd, prefix) {
54
49
  }
55
50
  throw error;
56
51
  }
57
- const candidates = entries
52
+ const publishedIds = entries
58
53
  .filter((id) => {
59
54
  if (id.startsWith("."))
60
55
  return false;
61
- try {
62
- return fs.statSync(path.join(root, id)).isDirectory() && isReadyProjection(path.join(root, id));
63
- }
64
- catch {
65
- return false;
66
- }
56
+ const directory = path.join(root, id);
57
+ return isPublishedProjection(directory);
67
58
  })
68
- .filter((id) => id === needle || id.startsWith(needle))
69
59
  .sort();
60
+ const addressed = publishedIds.filter((id) => {
61
+ if (id === needle || id.startsWith(needle))
62
+ return true;
63
+ const suffix = /-([0-9a-f]{8})$/.exec(id)?.[1];
64
+ return suffix === needle;
65
+ });
66
+ const candidates = commissionId === undefined
67
+ ? addressed
68
+ : addressed.filter((id) => {
69
+ const identity = readProjectionIdentity(path.join(root, id));
70
+ return identity.binding.kind === "commission" && identity.binding.commissionId === commissionId;
71
+ });
70
72
  if (candidates.length === 1) {
71
73
  return { status: "resolved", id: candidates[0] };
72
74
  }
@@ -75,467 +77,71 @@ export function resolveProjectionPrefix(cwd, prefix) {
75
77
  }
76
78
  return { status: "ambiguous", candidates };
77
79
  }
78
- export function listTellIds(projectionDirectory, layer) {
79
- const dir = path.join(projectionDirectory, layer);
80
- try {
81
- return fs
82
- .readdirSync(dir)
83
- .filter((name) => !name.startsWith("."))
84
- .sort();
85
- }
86
- catch (error) {
87
- if (isErrnoException(error) && error.code === "ENOENT") {
88
- return [];
89
- }
90
- throw error;
91
- }
92
- }
93
- export function readTellOriginal(projectionDirectory, layer, tellId) {
94
- const raw = fs.readFileSync(path.join(projectionDirectory, layer, tellId), "utf8");
95
- return JSON.parse(raw);
96
- }
97
- export function readTellSubmitted(projectionDirectory, tellId) {
98
- const raw = fs.readFileSync(path.join(projectionDirectory, "submitted", tellId), "utf8");
99
- return JSON.parse(raw);
100
- }
101
- function assertTellIdRandomHex(randomHex) {
102
- if (!/^[0-9a-f]{8}$/.test(randomHex)) {
103
- throw new ProjectionStateError(`tell id random segment must be 8 lowercase hex chars, got ${randomHex}`);
104
- }
105
- return randomHex;
106
- }
107
- function assertTellExecutionId(executionId) {
108
- if (typeof executionId !== "string" || executionId.length === 0) {
109
- throw new ProjectionStateError("submitted tell executionId must be a non-empty string");
110
- }
111
- return executionId;
112
- }
113
- function assertTellFence(fence) {
114
- if (!fence || typeof fence !== "object")
115
- throw new ProjectionStateError("tell fence is required");
116
- const value = fence;
117
- if (value.kind === "turn" && typeof value.turnId === "string" && value.turnId.length > 0) {
118
- return { kind: "turn", turnId: value.turnId };
119
- }
120
- if (value.kind === "ordinal" && typeof value.ordinal === "number" && Number.isSafeInteger(value.ordinal) && value.ordinal >= 0) {
121
- return { kind: "ordinal", ordinal: value.ordinal };
122
- }
123
- throw new ProjectionStateError("tell fence must be turn(string) or ordinal(non-negative integer)");
124
- }
125
- /** The only tell-consumption predicate. */
126
- export function isTellCausallyConsumed(submitted, checkpoint) {
127
- if (submitted.kind !== checkpoint.kind) {
128
- throw new ProjectionStateError(`tell fence kind mismatch: ${submitted.kind} vs ${checkpoint.kind}`);
129
- }
130
- if (submitted.kind === "ordinal" && checkpoint.kind === "ordinal")
131
- return checkpoint.ordinal > submitted.ordinal;
132
- if (submitted.kind === "turn" && checkpoint.kind === "turn")
133
- return checkpoint.turnId === submitted.turnId;
134
- throw new ProjectionStateError("unreachable tell fence comparison");
135
- }
136
- /** Write immutable tell original into inbox. Filename `<timestamp-ms>-<8hex>`. */
137
- export function writeInboxTell(projectionDirectory, text, nowMs, options) {
138
- const tellId = `${nowMs}-${assertTellIdRandomHex((options?.randomHex ?? randomHex8)())}`;
139
- const document = {
140
- version: 1,
141
- text,
142
- createdAt: isoFromNowMs(nowMs),
143
- ...(options?.effort ? { effort: options.effort } : {}),
144
- };
145
- const target = path.join(projectionDirectory, "inbox", tellId);
146
- try {
147
- fs.writeFileSync(target, encodeProjectionJson(document), { flag: "wx" });
148
- }
149
- catch (error) {
150
- if (isErrnoException(error) && error.code === "EEXIST") {
151
- throw new ProjectionStateError(`tell id already exists in inbox: ${tellId}`);
152
- }
153
- throw error;
154
- }
155
- return tellId;
156
- }
157
- /** inbox → inflight pure rename (claim). Admission-lock serialization makes the corrupt-state presence check coherent. */
158
- export function claimTellToInflight(projectionDirectory, tellId) {
159
- const from = path.join(projectionDirectory, "inbox", tellId);
160
- const to = path.join(projectionDirectory, "inflight", tellId);
161
- if (fs.readdirSync(path.dirname(to)).includes(tellId)) {
162
- throw new ProjectionStateError(`inflight tell already exists before claim: ${tellId}`);
163
- }
164
- try {
165
- fs.renameSync(from, to);
166
- return true;
167
- }
168
- catch (error) {
169
- if (isErrnoException(error) && error.code === "ENOENT") {
170
- return false;
171
- }
172
- throw error;
80
+ const PROJECTION_EXECUTION_ID_PATTERN = /^[0-9A-HJKMNP-TV-Z]{26}$/;
81
+ function assertProjectionExecutionId(executionId) {
82
+ if (!PROJECTION_EXECUTION_ID_PATTERN.test(executionId)) {
83
+ throw new ProjectionStateError(`invalid projection execution id: ${executionId}`);
173
84
  }
174
85
  }
175
- /** Claim every current inbox tell in filename order. */
176
- export function claimAllInbox(projectionDirectory) {
177
- const claimed = [];
178
- for (const tellId of listTellIds(projectionDirectory, "inbox")) {
179
- if (claimTellToInflight(projectionDirectory, tellId)) {
180
- claimed.push(tellId);
181
- }
182
- }
183
- return claimed;
86
+ export function executionEventsPath(projectionDirectory, executionId) {
87
+ assertProjectionExecutionId(executionId);
88
+ return path.join(projectionDirectory, `${executionId}.events.jsonl`);
184
89
  }
185
- /** Claim exactly one immutable generation fence in its recorded order. */
186
- export function claimFencedTells(projectionDirectory, fence, executionId) {
187
- const claimed = [];
188
- const seen = new Set();
189
- for (const tellId of fence) {
190
- if (seen.has(tellId))
191
- throw new ProjectionStateError(`duplicate tell in generation fence: ${tellId}`);
192
- seen.add(tellId);
193
- if (listTellIds(projectionDirectory, "inflight").includes(tellId)) {
194
- claimed.push(tellId);
195
- continue;
196
- }
197
- if (listTellIds(projectionDirectory, "inbox").includes(tellId) && claimTellToInflight(projectionDirectory, tellId)) {
198
- claimed.push(tellId);
199
- continue;
200
- }
201
- const consumedPath = path.join(projectionDirectory, "consumed", tellId);
202
- if (fs.existsSync(consumedPath)) {
203
- const consumed = JSON.parse(fs.readFileSync(consumedPath, "utf8"));
204
- if (consumed.executionId === executionId)
205
- continue;
206
- }
207
- throw new ProjectionStateError(`generation tell fence member is missing: ${tellId}`);
208
- }
209
- return claimed;
90
+ export function appendGenerationEvent(projectionDirectory, executionId, event) {
91
+ const filePath = executionEventsPath(projectionDirectory, executionId);
92
+ fs.appendFileSync(filePath, encodeProjectionJson(event), "utf8");
210
93
  }
211
94
  /**
212
- * inflight submitted enriched rewrite via atomicPublishFile + unlink inflight.
213
- * If submitted already exists (crash mid-transition), clear inflight and keep submitted.
95
+ * Read one generation's event ledger. A writer may be interrupted while
96
+ * appending its final line, so only an unterminated final residue is ignored.
97
+ * Earlier complete lines remain strict ledger records.
214
98
  */
215
- export function markTellSubmitted(projectionDirectory, tellId, fields) {
216
- const executionId = assertTellExecutionId(fields.executionId);
217
- const legacy = !fields.fence;
218
- if (legacy && (fields.submittedAtTurn === undefined) === (fields.submittedAfterResultOrdinal === undefined)) {
219
- throw new ProjectionStateError("submitted tell must contain exactly one legacy fence field");
220
- }
221
- const fence = fields.fence
222
- ? assertTellFence(fields.fence)
223
- : fields.submittedAtTurn !== undefined
224
- ? assertTellFence({ kind: "ordinal", ordinal: fields.submittedAtTurn })
225
- : assertTellFence({ kind: "ordinal", ordinal: fields.submittedAfterResultOrdinal });
226
- const inflightPath = path.join(projectionDirectory, "inflight", tellId);
227
- const submittedPath = path.join(projectionDirectory, "submitted", tellId);
228
- if (fs.existsSync(submittedPath)) {
229
- try {
230
- fs.unlinkSync(inflightPath);
231
- }
232
- catch (error) {
233
- if (!(isErrnoException(error) && error.code === "ENOENT")) {
234
- throw error;
235
- }
236
- }
237
- return;
238
- }
239
- const original = JSON.parse(fs.readFileSync(inflightPath, "utf8"));
240
- const submitted = {
241
- version: 1,
242
- text: original.text,
243
- createdAt: original.createdAt,
244
- ...(original.effort ? { effort: original.effort } : {}),
245
- executionId,
246
- ...(!legacy ? { fence } : {}),
247
- ...(legacy && fields.submittedAtTurn !== undefined ? { submittedAtTurn: fields.submittedAtTurn } : {}),
248
- ...(legacy && fields.submittedAfterResultOrdinal !== undefined ? { submittedAfterResultOrdinal: fields.submittedAfterResultOrdinal } : {}),
249
- ...((fields.replayedFrom ?? original.replayedFrom)?.length ? { replayedFrom: [...(fields.replayedFrom ?? original.replayedFrom)] } : {}),
250
- };
251
- const result = atomicPublishFile(submittedPath, encodeProjectionJson(submitted));
252
- if (result === "taken") {
253
- try {
254
- fs.unlinkSync(inflightPath);
255
- }
256
- catch (error) {
257
- if (!(isErrnoException(error) && error.code === "ENOENT")) {
258
- throw error;
259
- }
260
- }
261
- return;
262
- }
263
- try {
264
- fs.unlinkSync(inflightPath);
265
- }
266
- catch (error) {
267
- if (!(isErrnoException(error) && error.code === "ENOENT")) {
268
- throw error;
269
- }
270
- }
271
- }
272
- /** submitted → consumed pure rename; unlink delivered marker if present (ledger hygiene). */
273
- export function markTellConsumed(projectionDirectory, tellId) {
274
- const from = path.join(projectionDirectory, "submitted", tellId);
275
- const to = path.join(projectionDirectory, "consumed", tellId);
276
- fs.renameSync(from, to);
277
- clearDeliveredMarker(projectionDirectory, tellId);
278
- }
279
- export function publishDeliveredMarker(projectionDirectory, tellId, nowMs) {
280
- const document = {
281
- version: 1,
282
- at: isoFromNowMs(nowMs),
283
- };
284
- return atomicPublishFile(path.join(projectionDirectory, "delivered", tellId), encodeProjectionJson(document));
285
- }
286
- export function clearDeliveredMarker(projectionDirectory, tellId) {
287
- try {
288
- fs.unlinkSync(path.join(projectionDirectory, "delivered", tellId));
289
- }
290
- catch (error) {
291
- if (!(isErrnoException(error) && error.code === "ENOENT")) {
292
- throw error;
293
- }
294
- }
295
- }
296
- /** Clear delivered markers for the given tell ids (crash replay prep). */
297
- export function clearDeliveredMarkers(projectionDirectory, tellIds) {
298
- for (const tellId of tellIds) {
299
- clearDeliveredMarker(projectionDirectory, tellId);
300
- }
301
- }
302
- /** Replace an existing JSON ledger atomically. Publish-once remains atomicPublishFile's law. */
303
- export function atomicReplaceFile(target, content) {
304
- const directory = path.dirname(target);
305
- const temporary = path.join(directory, `.${path.basename(target)}.${process.pid}.${randomHex8()}.tmp`);
306
- let fd;
307
- try {
308
- fd = fs.openSync(temporary, "wx", 0o600);
309
- fs.writeFileSync(fd, content, "utf8");
310
- fs.fsyncSync(fd);
311
- fs.closeSync(fd);
312
- fd = undefined;
313
- fs.renameSync(temporary, target);
314
- }
315
- finally {
316
- if (fd !== undefined)
317
- fs.closeSync(fd);
318
- try {
319
- fs.unlinkSync(temporary);
320
- }
321
- catch (error) {
322
- if (!(isErrnoException(error) && error.code === "ENOENT"))
323
- throw error;
324
- }
325
- }
326
- }
327
- export function acquireAdmissionLock(projectionDirectory, pid, nowMs, executionId, parentExecutionId) {
328
- const document = {
329
- version: 1,
330
- pid,
331
- createdAt: isoFromNowMs(nowMs),
332
- ...(executionId ? { executionId } : {}),
333
- ...(parentExecutionId ? { parentExecutionId } : {}),
334
- };
335
- return atomicPublishFile(path.join(projectionDirectory, ".admission-lock"), encodeProjectionJson(document));
336
- }
337
- export function releaseAdmissionLock(projectionDirectory) {
338
- try {
339
- fs.unlinkSync(path.join(projectionDirectory, ".admission-lock"));
340
- }
341
- catch (error) {
342
- if (!(isErrnoException(error) && error.code === "ENOENT")) {
343
- throw error;
344
- }
345
- }
346
- }
347
- /** Release only the admission record owned by this generation. */
348
- export function releaseAdmissionLockOwned(projectionDirectory, executionId) {
349
- const admission = readAdmissionLock(projectionDirectory);
350
- if (!admission)
351
- return false;
352
- if (admission.executionId !== executionId) {
353
- throw new ProjectionStateError(`admission ownership mismatch: expected ${executionId}, found ${admission.executionId ?? "none"}`);
354
- }
355
- releaseAdmissionLock(projectionDirectory);
356
- return true;
357
- }
358
- export function readAdmissionLock(projectionDirectory) {
359
- try {
360
- const raw = fs.readFileSync(path.join(projectionDirectory, ".admission-lock"), "utf8");
361
- return JSON.parse(raw);
362
- }
363
- catch (error) {
364
- if (isErrnoException(error) && error.code === "ENOENT") {
365
- return null;
366
- }
367
- throw error;
368
- }
369
- }
370
- export function acquireResumeLock(projectionDirectory, childId, pid, nowMs) {
371
- const document = {
372
- version: 1,
373
- childId,
374
- pid,
375
- createdAt: isoFromNowMs(nowMs),
376
- };
377
- return atomicPublishFile(path.join(projectionDirectory, ".resume-lock"), encodeProjectionJson(document));
378
- }
379
- export function releaseResumeLock(projectionDirectory) {
380
- try {
381
- fs.unlinkSync(path.join(projectionDirectory, ".resume-lock"));
382
- }
383
- catch (error) {
384
- if (!(isErrnoException(error) && error.code === "ENOENT")) {
385
- throw error;
386
- }
387
- }
388
- }
389
- /** Release only a resume lock created by this launcher. */
390
- export function releaseResumeLockOwned(projectionDirectory, childId) {
391
- const lock = readResumeLock(projectionDirectory);
392
- if (!lock)
393
- return false;
394
- if (lock.childId !== childId) {
395
- throw new ProjectionStateError(`resume ownership mismatch: expected ${childId}, found ${lock.childId}`);
396
- }
397
- releaseResumeLock(projectionDirectory);
398
- return true;
399
- }
400
- export function readResumeLock(projectionDirectory) {
401
- try {
402
- const raw = fs.readFileSync(path.join(projectionDirectory, ".resume-lock"), "utf8");
403
- return JSON.parse(raw);
404
- }
405
- catch (error) {
406
- if (isErrnoException(error) && error.code === "ENOENT") {
407
- return null;
408
- }
409
- throw error;
410
- }
411
- }
412
- /** Publish terminal verdict. Dead is observational and never written. */
413
- export function publishVerdict(projectionDirectory, state, nowMs, detail) {
414
- const document = {
415
- version: 1,
416
- state,
417
- at: isoFromNowMs(nowMs),
418
- ...(detail ? { detail } : {}),
419
- };
420
- atomicReplaceFile(path.join(projectionDirectory, "verdict.json"), encodeProjectionJson(document));
421
- return "published";
422
- }
423
- export function generationLaunchPath(projectionDirectory, executionId) {
424
- return path.join(projectionDirectory, `generation-${executionId}.json`);
425
- }
426
- export function generationResultPath(projectionDirectory, executionId) {
427
- return path.join(projectionDirectory, `generation-${executionId}.result.json`);
428
- }
429
- export function generationLogPath(projectionDirectory, executionId) {
430
- return path.join(projectionDirectory, `generation-${executionId}.log`);
431
- }
432
- export function publishGenerationLaunch(projectionDirectory, document) {
433
- return atomicPublishFile(generationLaunchPath(projectionDirectory, document.executionId), encodeProjectionJson(document));
434
- }
435
- export function publishGenerationResult(projectionDirectory, document) {
436
- return atomicPublishFile(generationResultPath(projectionDirectory, document.executionId), encodeProjectionJson(document));
437
- }
438
- export function readGenerationLaunch(projectionDirectory, executionId) {
439
- return JSON.parse(fs.readFileSync(generationLaunchPath(projectionDirectory, executionId), "utf8"));
440
- }
441
- /** Lineage tip is launch time, not lexical UUID order. A dirty tip is never skipped. */
442
- export function latestGenerationLaunch(projectionDirectory) {
443
- const launches = fs.readdirSync(projectionDirectory)
444
- .filter((name) => /^generation-[^.]+\.json$/.test(name))
445
- .map((name) => JSON.parse(fs.readFileSync(path.join(projectionDirectory, name), "utf8")))
446
- .sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt) || left.executionId.localeCompare(right.executionId));
447
- return launches.at(-1) ?? null;
448
- }
449
- export function latestGenerationResult(projectionDirectory) {
450
- const launch = latestGenerationLaunch(projectionDirectory);
451
- if (!launch)
452
- return null;
99
+ export function readGenerationEvents(projectionDirectory, executionId) {
100
+ let content;
453
101
  try {
454
- return JSON.parse(fs.readFileSync(generationResultPath(projectionDirectory, launch.executionId), "utf8"));
102
+ content = fs.readFileSync(executionEventsPath(projectionDirectory, executionId), "utf8");
455
103
  }
456
104
  catch (error) {
457
105
  if (isErrnoException(error) && error.code === "ENOENT")
458
- return null;
106
+ return [];
459
107
  throw error;
460
108
  }
461
- }
462
- /** Shared status/tell/wake phase authority. */
463
- export function deriveProjectionPhase(projectionDirectory, nowMs, probePid = (pid) => { process.kill(pid, 0); }) {
464
- const admission = readAdmissionLock(projectionDirectory);
465
- const verdict = readVerdict(projectionDirectory);
466
- if (admission) {
467
- let alive = true;
109
+ const lines = content.split("\n");
110
+ const terminated = content.endsWith("\n");
111
+ const completeLines = lines.slice(0, -1);
112
+ const events = [];
113
+ for (const line of completeLines) {
114
+ events.push(JSON.parse(line));
115
+ }
116
+ if (!terminated && lines.at(-1)) {
468
117
  try {
469
- probePid(admission.pid);
118
+ events.push(JSON.parse(lines.at(-1)));
470
119
  }
471
120
  catch {
472
- alive = false;
473
- }
474
- let fresh = false;
475
- try {
476
- fresh = nowMs - fs.statSync(path.join(projectionDirectory, "heartbeat")).mtimeMs <= 15_000;
121
+ // A concurrent append can leave only this final record incomplete.
477
122
  }
478
- catch { /* stale */ }
479
- if (alive && fresh)
480
- return "active";
481
- if (!latestGenerationResult(projectionDirectory))
482
- return "lost";
483
123
  }
484
- if (verdict?.state === "dismissed")
485
- return "dismissed";
486
- if (verdict?.state === "failed")
487
- return "failed";
488
- if (verdict?.state === "done")
489
- return "done";
490
- return "quiescent";
124
+ return events;
491
125
  }
492
- /** The only wake eligibility predicate. */
493
- export function isProjectionWakeEligible(projectionDirectory) {
494
- return latestGenerationResult(projectionDirectory) !== null &&
495
- readAdmissionLock(projectionDirectory) === null &&
496
- (readVerdict(projectionDirectory) === null || readVerdict(projectionDirectory)?.state === "done");
497
- }
498
- /** New executions replay all predecessor submissions exactly once through inflight. */
499
- export function demoteSubmittedTellsForReplay(projectionDirectory, executionId) {
500
- const replayed = [];
501
- for (const tellId of listTellIds(projectionDirectory, "submitted")) {
502
- const submitted = readTellSubmitted(projectionDirectory, tellId);
503
- if (submitted.executionId === executionId)
504
- throw new ProjectionStateError(`submitted tell belongs to current execution: ${tellId}`);
505
- const target = path.join(projectionDirectory, "inflight", tellId);
506
- if (fs.existsSync(target))
507
- throw new ProjectionStateError(`inflight tell already exists before replay: ${tellId}`);
508
- fs.renameSync(path.join(projectionDirectory, "submitted", tellId), target);
509
- atomicReplaceFile(target, encodeProjectionJson({
510
- version: 1,
511
- text: submitted.text,
512
- createdAt: submitted.createdAt,
513
- ...(submitted.effort ? { effort: submitted.effort } : {}),
514
- replayedFrom: [...(submitted.replayedFrom ?? []), { executionId: submitted.executionId, fence: submitted.fence }],
515
- }));
516
- replayed.push(tellId);
517
- }
518
- return replayed;
519
- }
520
- export function readVerdict(projectionDirectory) {
126
+ export function readProjectionEvents(projectionDirectory) {
127
+ let entries;
521
128
  try {
522
- const raw = fs.readFileSync(path.join(projectionDirectory, "verdict.json"), "utf8");
523
- return JSON.parse(raw);
129
+ entries = fs.readdirSync(projectionDirectory, { withFileTypes: true });
524
130
  }
525
131
  catch (error) {
526
- if (isErrnoException(error) && error.code === "ENOENT") {
527
- return null;
528
- }
132
+ if (isErrnoException(error) && error.code === "ENOENT")
133
+ return [];
529
134
  throw error;
530
135
  }
531
- }
532
- export function readPact(projectionDirectory) {
533
- const raw = fs.readFileSync(path.join(projectionDirectory, "pact.json"), "utf8");
534
- return JSON.parse(raw);
535
- }
536
- /** Touch heartbeat mtime with injected nowMs (empty file created at mint). */
537
- export function touchHeartbeat(projectionDirectory, nowMs) {
538
- const heartbeatPath = path.join(projectionDirectory, "heartbeat");
539
- const at = new Date(nowMs);
540
- fs.utimesSync(heartbeatPath, at, at);
136
+ const names = entries
137
+ .filter((entry) => (entry.isFile()
138
+ && /^[0-9A-HJKMNP-TV-Z]{26}\.events\.jsonl$/.test(entry.name)))
139
+ .map((entry) => entry.name)
140
+ .sort();
141
+ const events = [];
142
+ for (const name of names) {
143
+ const executionId = name.slice(0, -".events.jsonl".length);
144
+ events.push(...readGenerationEvents(projectionDirectory, executionId));
145
+ }
146
+ return events;
541
147
  }
@@ -0,0 +1,31 @@
1
+ import { ProjectionStateError } from "./atomic-publish.js";
2
+ import { applySnapshotEffort, parseResolvedAkumaLaunchSnapshot, } from "../agents/launch-snapshot_v2.js";
3
+ import { listTellIds, readTellOriginal, readTellSubmitted, } from "./projection-core.js";
4
+ export function snapshotPendingTells(projectionDirectory) {
5
+ const pending = ["inbox", "inflight", "submitted"]
6
+ .flatMap((layer) => listTellIds(projectionDirectory, layer)
7
+ .map((tellId) => ({ layer, tellId })))
8
+ .sort((left, right) => left.tellId.localeCompare(right.tellId));
9
+ const effort = pending.reduce((selected, item) => {
10
+ const tell = item.layer === "submitted"
11
+ ? readTellSubmitted(projectionDirectory, item.tellId)
12
+ : readTellOriginal(projectionDirectory, item.layer, item.tellId);
13
+ return tell.effort ?? selected;
14
+ }, undefined);
15
+ return {
16
+ tellIds: pending.map((item) => item.tellId),
17
+ ...(effort ? { effort } : {}),
18
+ };
19
+ }
20
+ export function applyContinuationEffort(inputs, effort) {
21
+ if (!effort)
22
+ return inputs;
23
+ let launchSnapshot;
24
+ try {
25
+ launchSnapshot = parseResolvedAkumaLaunchSnapshot(inputs.launchSnapshot);
26
+ }
27
+ catch (error) {
28
+ throw new ProjectionStateError("generation launch has an invalid immutable launchSnapshot", { cause: error });
29
+ }
30
+ return { ...inputs, launchSnapshot: applySnapshotEffort(launchSnapshot, effort) };
31
+ }