@astrosheep/keiyaku 2.9.7 → 2.9.9

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 (57) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/harness/event-persistence.js +7 -5
  3. package/build/agents/harness/events.js +3 -2
  4. package/build/agents/harness/projection.js +8 -6
  5. package/build/agents/providers/codex-app-server/adapter.js +6 -1
  6. package/build/agents/providers/codex-app-server/session.js +8 -7
  7. package/build/agents/selector.js +12 -1
  8. package/build/cli/commands/akuma/view/handler.js +3 -11
  9. package/build/cli/commands/contract/amend/handler.js +1 -1
  10. package/build/cli/commands/contract/amend/meta.js +4 -4
  11. package/build/cli/commands/projection/status/handler.js +5 -4
  12. package/build/cli/commands/projection/status/meta.js +2 -2
  13. package/build/cli/commands/task/add/meta.js +9 -1
  14. package/build/cli/commands/task/shared.js +2 -1
  15. package/build/cli/completion.js +8 -0
  16. package/build/cli/render/kanshi.js +238 -80
  17. package/build/cli/render/path-prefix-compaction.js +119 -76
  18. package/build/cli/render/projection-activity.js +65 -21
  19. package/build/cli/render/shared.js +8 -7
  20. package/build/cli/render/status.js +11 -23
  21. package/build/cli/render/wait.js +42 -64
  22. package/build/config/env-keys.js +1 -1
  23. package/build/config/env.js +1 -1
  24. package/build/config/settings/disease.js +4 -4
  25. package/build/config/settings/loader.js +44 -21
  26. package/build/core/addressing.js +40 -9
  27. package/build/core/amend.js +21 -5
  28. package/build/core/call/context.js +19 -3
  29. package/build/core/call/execution.js +43 -20
  30. package/build/core/ledger-batch.js +194 -0
  31. package/build/core/projection/generation/database.js +22 -0
  32. package/build/core/projection/generation/projection-generation-execution.js +52 -38
  33. package/build/core/projection/generation/projection-generation-launcher.js +148 -20
  34. package/build/core/projection/generation/projection-generation-process.js +3 -1
  35. package/build/core/projection/generation/projection-generation-runner.js +76 -40
  36. package/build/core/projection/generation/projection-generation-runtime.js +82 -19
  37. package/build/core/projection/generation/store.js +17 -1
  38. package/build/core/projection/generation/transitions.js +89 -12
  39. package/build/core/projection/index.js +3 -3
  40. package/build/core/projection/projection-activity.js +2 -0
  41. package/build/core/projection/projection-kill.js +22 -10
  42. package/build/core/projection/projection-runner-lock.js +177 -37
  43. package/build/core/projection/projection-status.js +183 -60
  44. package/build/core/projection/projection-wake.js +171 -72
  45. package/build/core/status/board.js +55 -14
  46. package/build/core/status/drift.js +21 -5
  47. package/build/core/status/ledger-batch.js +1 -158
  48. package/build/core/task/settlement-git.js +2 -2
  49. package/build/core/task/task-git-runtime.js +8 -10
  50. package/build/core/task/task-git-store.js +5 -3
  51. package/build/core/worktree-path.js +39 -25
  52. package/build/flow-error.js +1 -1
  53. package/build/generated/version.js +2 -2
  54. package/build/git/refs.js +47 -1
  55. package/package.json +1 -1
  56. package/skills/keiyaku-akuma/SKILL.md +18 -0
  57. package/skills/keiyaku-workflow/SKILL.md +68 -13
@@ -40,6 +40,95 @@ function readJournalMode(database) {
40
40
  function assertCurrentLockSchema(database) {
41
41
  database.prepare("SELECT id FROM lock_anchor LIMIT 1").get();
42
42
  }
43
+ function assertBusyTimeout(busyTimeoutMs) {
44
+ if (!Number.isSafeInteger(busyTimeoutMs) || busyTimeoutMs < 0) {
45
+ throw new ProjectionRunnerLockUnavailableError("projection runner lock busy timeout must be a non-negative safe integer");
46
+ }
47
+ }
48
+ function openGuard(lockPath, busyTimeoutMs) {
49
+ const database = new DatabaseSync(lockPath);
50
+ try {
51
+ database.exec(`PRAGMA busy_timeout=${busyTimeoutMs}`);
52
+ database.exec("BEGIN DEFERRED");
53
+ assertCurrentLockSchema(database);
54
+ if (readJournalMode(database) !== "delete") {
55
+ throw new ProjectionRunnerLockUnavailableError("projection runner lock database does not use rollback-journal mode");
56
+ }
57
+ return database;
58
+ }
59
+ catch (error) {
60
+ closeTransaction(database);
61
+ throw error;
62
+ }
63
+ }
64
+ function openReservation(lockPath, busyTimeoutMs) {
65
+ const database = new DatabaseSync(lockPath);
66
+ try {
67
+ database.exec(`PRAGMA busy_timeout=${busyTimeoutMs}`);
68
+ database.exec("BEGIN IMMEDIATE");
69
+ return database;
70
+ }
71
+ catch (error) {
72
+ closeTransaction(database);
73
+ throw error;
74
+ }
75
+ }
76
+ function closeTransaction(database) {
77
+ let failure;
78
+ try {
79
+ database.exec("ROLLBACK");
80
+ }
81
+ catch (error) {
82
+ failure = error;
83
+ }
84
+ try {
85
+ database.close();
86
+ }
87
+ catch (error) {
88
+ failure ??= error;
89
+ }
90
+ return failure;
91
+ }
92
+ function createHeldLock(lockPath, busyTimeoutMs, guard, initialReservation) {
93
+ let closed = false;
94
+ let reservation = initialReservation;
95
+ return {
96
+ path: lockPath,
97
+ releaseReservationForTransfer() {
98
+ if (closed || reservation === undefined)
99
+ return;
100
+ const released = reservation;
101
+ reservation = undefined;
102
+ const failure = closeTransaction(released);
103
+ if (failure !== undefined) {
104
+ throw new ProjectionRunnerLockUnavailableError(`projection runner reservation release failed: ${errorDetail(failure)}`, { cause: failure });
105
+ }
106
+ },
107
+ reclaimReservation() {
108
+ if (closed || reservation !== undefined)
109
+ return;
110
+ try {
111
+ reservation = openReservation(lockPath, busyTimeoutMs);
112
+ }
113
+ catch (error) {
114
+ throw new ProjectionRunnerLockUnavailableError(`projection runner reservation reclaim failed: ${errorDetail(error)}`, { cause: error });
115
+ }
116
+ },
117
+ close() {
118
+ if (closed)
119
+ return;
120
+ closed = true;
121
+ let failure;
122
+ if (reservation !== undefined)
123
+ failure = closeTransaction(reservation);
124
+ const guardFailure = closeTransaction(guard);
125
+ failure ??= guardFailure;
126
+ if (failure !== undefined) {
127
+ throw new ProjectionRunnerLockUnavailableError(`projection runner lock release failed: ${errorDetail(failure)}`, { cause: failure });
128
+ }
129
+ },
130
+ };
131
+ }
43
132
  function closeAfterFailedAcquire(database, error) {
44
133
  try {
45
134
  database.close();
@@ -95,13 +184,12 @@ export function initializeProjectionRunnerLockDatabase(projectionDirectory, plat
95
184
  return lockPath;
96
185
  }
97
186
  /**
98
- * Open the already initialized projection-local lock database and hold one
99
- * rollback-mode EXCLUSIVE transaction until close() or process death. SQLite
100
- * requires a writable connection for this write-grade lock; the preceding
101
- * regular-file check prevents ordinary missing storage from being created.
102
- * Database bytes and timestamps are deliberately absent from the returned authority.
187
+ * Hold one rollback-mode SHARED guard plus the sole RESERVED claim until
188
+ * close() or process death. An EXCLUSIVE probe cannot pass either component.
189
+ * During handoff the child overlaps the guard before the RESERVED claim moves,
190
+ * so kernel exclusion remains continuous without transferring a connection.
103
191
  */
104
- export function acquireProjectionRunnerLock(projectionDirectory, platform = process.platform) {
192
+ export function acquireProjectionRunnerLock(projectionDirectory, platform = process.platform, busyTimeoutMs = 0) {
105
193
  if (!isProjectionRunnerLockPlatformSupported(platform)) {
106
194
  throw new ProjectionRunnerLockUnsupportedError(platform);
107
195
  }
@@ -116,30 +204,96 @@ export function acquireProjectionRunnerLock(projectionDirectory, platform = proc
116
204
  throw error;
117
205
  throw new ProjectionRunnerLockUnavailableError(`projection runner lock database is not initialized: ${errorDetail(error)}`, { cause: error });
118
206
  }
119
- let database;
207
+ assertBusyTimeout(busyTimeoutMs);
208
+ let guard;
120
209
  try {
121
- database = new DatabaseSync(lockPath);
210
+ guard = openGuard(lockPath, busyTimeoutMs);
122
211
  }
123
212
  catch (error) {
124
213
  throw new ProjectionRunnerLockUnavailableError(`projection runner lock database could not be opened: ${errorDetail(error)}`, { cause: error });
125
214
  }
215
+ let reservation;
126
216
  try {
127
- database.exec("PRAGMA busy_timeout=0");
217
+ reservation = openReservation(lockPath, busyTimeoutMs);
218
+ }
219
+ catch (error) {
220
+ if (reservation)
221
+ closeTransaction(reservation);
222
+ closeTransaction(guard);
223
+ return closeAfterFailedAcquire(guard, error);
224
+ }
225
+ return createHeldLock(lockPath, busyTimeoutMs, guard, reservation);
226
+ }
227
+ export function acquireProjectionRunnerTransferTarget(projectionDirectory, platform = process.platform, busyTimeoutMs = 0) {
228
+ if (!isProjectionRunnerLockPlatformSupported(platform)) {
229
+ throw new ProjectionRunnerLockUnsupportedError(platform);
230
+ }
231
+ assertBusyTimeout(busyTimeoutMs);
232
+ const lockPath = projectionRunnerLockPath(projectionDirectory);
233
+ let guard;
234
+ try {
235
+ guard = openGuard(lockPath, busyTimeoutMs);
236
+ }
237
+ catch (error) {
238
+ if (guard)
239
+ closeTransaction(guard);
240
+ throw new ProjectionRunnerLockUnavailableError(`projection runner transfer acquisition failed: ${errorDetail(error)}`, { cause: error });
241
+ }
242
+ let closed = false;
243
+ return {
244
+ path: lockPath,
245
+ promote() {
246
+ if (closed)
247
+ throw new ProjectionRunnerLockUnavailableError("projection runner transfer target is closed");
248
+ let reservation;
249
+ try {
250
+ reservation = openReservation(lockPath, busyTimeoutMs);
251
+ }
252
+ catch (error) {
253
+ if (reservation)
254
+ closeTransaction(reservation);
255
+ throw new ProjectionRunnerLockUnavailableError(`projection runner transfer promotion failed: ${errorDetail(error)}`, { cause: error });
256
+ }
257
+ closed = true;
258
+ return createHeldLock(lockPath, busyTimeoutMs, guard, reservation);
259
+ },
260
+ close() {
261
+ if (closed)
262
+ return;
263
+ closed = true;
264
+ const failure = closeTransaction(guard);
265
+ if (failure !== undefined) {
266
+ throw new ProjectionRunnerLockUnavailableError(`projection runner transfer release failed: ${errorDetail(failure)}`, { cause: failure });
267
+ }
268
+ },
269
+ };
270
+ }
271
+ /** Acquire positive death evidence. Any active guard or transfer blocks this. */
272
+ export function acquireProjectionRunnerSettlementLock(projectionDirectory, platform = process.platform, busyTimeoutMs = 0) {
273
+ if (!isProjectionRunnerLockPlatformSupported(platform)) {
274
+ throw new ProjectionRunnerLockUnsupportedError(platform);
275
+ }
276
+ assertBusyTimeout(busyTimeoutMs);
277
+ const lockPath = projectionRunnerLockPath(projectionDirectory);
278
+ let database;
279
+ try {
280
+ database = new DatabaseSync(lockPath);
281
+ database.exec(`PRAGMA busy_timeout=${busyTimeoutMs}`);
128
282
  database.exec("BEGIN EXCLUSIVE");
283
+ assertCurrentLockSchema(database);
129
284
  if (readJournalMode(database) !== "delete") {
130
285
  throw new ProjectionRunnerLockUnavailableError("projection runner lock database does not use rollback-journal mode");
131
286
  }
132
- assertCurrentLockSchema(database);
133
287
  }
134
288
  catch (error) {
135
289
  try {
136
- database.exec("ROLLBACK");
290
+ database.close();
137
291
  }
138
- catch {
139
- // A failure before BEGIN has no transaction; a failure after BEGIN is
140
- // still released by closeAfterFailedAcquire if rollback cannot run.
292
+ catch { }
293
+ if (isSqliteBusy(error)) {
294
+ throw new ProjectionRunnerLockUnavailableError("projection runner lock is already held", { cause: error });
141
295
  }
142
- return closeAfterFailedAcquire(database, error);
296
+ throw new ProjectionRunnerLockUnavailableError(`projection runner settlement lock acquisition failed: ${errorDetail(error)}`, { cause: error });
143
297
  }
144
298
  let closed = false;
145
299
  return {
@@ -148,31 +302,17 @@ export function acquireProjectionRunnerLock(projectionDirectory, platform = proc
148
302
  if (closed)
149
303
  return;
150
304
  closed = true;
151
- let rollbackError;
152
- try {
153
- database.exec("ROLLBACK");
154
- }
155
- catch (error) {
156
- rollbackError = error;
157
- }
158
- try {
159
- database.close();
160
- }
161
- catch (error) {
162
- rollbackError ??= error;
163
- }
164
- if (rollbackError !== undefined) {
165
- throw new ProjectionRunnerLockUnavailableError(`projection runner lock release failed: ${errorDetail(rollbackError)}`, { cause: rollbackError });
305
+ const failure = closeTransaction(database);
306
+ if (failure !== undefined) {
307
+ throw new ProjectionRunnerLockUnavailableError(`projection runner settlement lock release failed: ${errorDetail(failure)}`, { cause: failure });
166
308
  }
167
309
  },
168
310
  };
169
311
  }
170
312
  /**
171
- * Probe without creating the lock database. BEGIN DEFERRED itself acquires no
172
- * file lock, so the sqlite_schema SELECT is mandatory: SQLITE_BUSY there is
173
- * positive evidence of the runner's EXCLUSIVE transaction. Successful reads
174
- * hold a compatible SHARED transaction only long enough to verify DELETE
175
- * journal mode, then roll back. Every other failure remains unknown.
313
+ * Probe without creating the lock database. BEGIN EXCLUSIVE is the positive
314
+ * death test: SQLITE_BUSY proves a SHARED guard or RESERVED owner remains;
315
+ * success proves both are gone. Every other failure remains unknown.
176
316
  */
177
317
  export function observeProjectionRunnerLock(projectionDirectory, platform = process.platform) {
178
318
  if (!isProjectionRunnerLockPlatformSupported(platform)) {
@@ -185,7 +325,7 @@ export function observeProjectionRunnerLock(projectionDirectory, platform = proc
185
325
  const lockPath = projectionRunnerLockPath(projectionDirectory);
186
326
  let database;
187
327
  try {
188
- database = new DatabaseSync(lockPath, { readOnly: true });
328
+ database = new DatabaseSync(lockPath);
189
329
  }
190
330
  catch (error) {
191
331
  return { state: "unknown", reason: "open-failed", detail: errorDetail(error) };
@@ -193,7 +333,7 @@ export function observeProjectionRunnerLock(projectionDirectory, platform = proc
193
333
  let transactionOpen = false;
194
334
  try {
195
335
  database.exec("PRAGMA busy_timeout=0");
196
- database.exec("BEGIN DEFERRED");
336
+ database.exec("BEGIN EXCLUSIVE");
197
337
  transactionOpen = true;
198
338
  assertCurrentLockSchema(database);
199
339
  const journalMode = readJournalMode(database);
@@ -1,4 +1,4 @@
1
- import * as fs from "node:fs";
1
+ import fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { isErrnoException } from "../../errno.js";
4
4
  import { executionEventsPath, isProjectionId, projectionRoot, } from "./projection-core.js";
@@ -6,10 +6,10 @@ import { readProjectionAlias } from "./projection-alias.js";
6
6
  import { ProjectionStateError } from "../atomic-publish.js";
7
7
  import { foldPublicProjectionActivity, } from "./projection-activity.js";
8
8
  import { readProjectionIdentity } from "./projection-identity.js";
9
- import { readProjectionTellCounts } from "./tell/store.js";
9
+ import { readPendingTellWindow, 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
- import { observeProjectionLifeSnapshot } from "./projection-life-observer.js";
12
+ import { observeProjectionLifeSnapshot, } from "./projection-life-observer.js";
13
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";
@@ -63,10 +63,8 @@ function compareProjectionStatusRows(a, b) {
63
63
  }
64
64
  return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
65
65
  }
66
- export function prioritizeProjectionStatusRows(rows, showAll) {
66
+ export function prioritizeProjectionStatusRows(rows) {
67
67
  const ordered = [...rows].sort(compareProjectionStatusRows);
68
- if (showAll)
69
- return { rows: ordered };
70
68
  const visible = ordered.filter((row) => !isOmissibleProjectionStatusState(row.state));
71
69
  const byState = {
72
70
  failed: 0,
@@ -83,6 +81,9 @@ export function prioritizeProjectionStatusRows(rows, showAll) {
83
81
  const total = Object.values(byState).reduce((sum, count) => sum + count, 0);
84
82
  return total > 0 ? { rows: visible, omitted: { total, byState } } : { rows: visible };
85
83
  }
84
+ export function requiresProjectionStatusDetails(state) {
85
+ return !isOmissibleProjectionStatusState(state);
86
+ }
86
87
  class ProjectionStatusFileError extends Error {
87
88
  diagnostic;
88
89
  constructor(kind, filePath) {
@@ -168,6 +169,7 @@ function readIdentityFacts(projectionDir, diagnostics) {
168
169
  pid: identity.pid,
169
170
  akuma: identity.akuma,
170
171
  provider: identity.provider,
172
+ binding: identity.binding,
171
173
  mintedAt: identity.mintedAt,
172
174
  workspaceRoot: identity.workspaceRoot,
173
175
  };
@@ -175,18 +177,18 @@ function readIdentityFacts(projectionDir, diagnostics) {
175
177
  catch (error) {
176
178
  if (isErrnoException(error) && error.code === "ENOENT") {
177
179
  diagnostics.push({ kind: "missing-identity", detail: "heart identity missing" });
178
- return { identity: null, pid: undefined, akuma: undefined, provider: undefined, mintedAt: undefined, workspaceRoot: undefined };
180
+ return { identity: null, pid: undefined, akuma: undefined, provider: undefined, binding: undefined, mintedAt: undefined, workspaceRoot: undefined };
179
181
  }
180
182
  if (error instanceof ProjectionGenerationStoreError) {
181
183
  diagnostics.push({ kind: "invalid-identity", detail: error.message });
182
- return { identity: null, pid: undefined, akuma: undefined, provider: undefined, mintedAt: undefined, workspaceRoot: undefined };
184
+ return { identity: null, pid: undefined, akuma: undefined, provider: undefined, binding: undefined, mintedAt: undefined, workspaceRoot: undefined };
183
185
  }
184
186
  if (error instanceof ProjectionStateError) {
185
187
  diagnostics.push({ kind: "invalid-identity", detail: error.message });
186
- return { identity: null, pid: undefined, akuma: undefined, provider: undefined, mintedAt: undefined, workspaceRoot: undefined };
188
+ return { identity: null, pid: undefined, akuma: undefined, provider: undefined, binding: undefined, mintedAt: undefined, workspaceRoot: undefined };
187
189
  }
188
190
  diagnostics.push({ kind: "unreadable-identity", detail: `heart identity unreadable: ${String(error)}` });
189
- return { identity: null, pid: undefined, akuma: undefined, provider: undefined, mintedAt: undefined, workspaceRoot: undefined };
191
+ return { identity: null, pid: undefined, akuma: undefined, provider: undefined, binding: undefined, mintedAt: undefined, workspaceRoot: undefined };
190
192
  }
191
193
  }
192
194
  function readHeartTellCounts(projectionDir, diagnostics) {
@@ -202,6 +204,22 @@ function readHeartTellCounts(projectionDir, diagnostics) {
202
204
  return emptyTellCounts();
203
205
  }
204
206
  }
207
+ function readPendingTells(projectionDir, diagnostics) {
208
+ try {
209
+ // Kanshi is an operator surface: unconsumed tells are actionable facts, not
210
+ // a count to hide behind. This is one heart read and does not enumerate a
211
+ // second projection topology.
212
+ return readPendingTellWindow(projectionDir, Number.MAX_SAFE_INTEGER).window;
213
+ }
214
+ catch (error) {
215
+ if (error instanceof ProjectionGenerationStoreError || error instanceof ProjectionStateError) {
216
+ diagnostics.push({ kind: "unreadable-tells", detail: error.message });
217
+ return [];
218
+ }
219
+ diagnostics.push({ kind: "unreadable-tells", detail: `heart tells unreadable: ${String(error)}` });
220
+ return [];
221
+ }
222
+ }
205
223
  function readStoredEvents(projectionDir, executionId, diagnostics) {
206
224
  let content;
207
225
  let truncated = false;
@@ -284,7 +302,7 @@ function readActivityFacts(projectionDir, executionId, nowMs, diagnostics, fallb
284
302
  function terminalTimestamp(current) {
285
303
  return extractTerminalTimestamp(current?.verdict?.facts);
286
304
  }
287
- function readProjectionRow(projectionDir, id, alias, nowMs) {
305
+ function readProjectionSummary(projectionDir, id, alias, nowMs) {
288
306
  const diagnostics = [];
289
307
  const snapshot = observeProjectionLifeSnapshot(projectionDir, {
290
308
  nowMs,
@@ -294,40 +312,73 @@ function readProjectionRow(projectionDir, id, alias, nowMs) {
294
312
  return {
295
313
  id,
296
314
  ...(alias ? { alias } : {}),
315
+ projectionDir,
316
+ snapshot,
317
+ identityFacts: null,
318
+ diagnostics,
319
+ state: "incompatible",
320
+ };
321
+ }
322
+ const identityFacts = readIdentityFacts(projectionDir, diagnostics);
323
+ const life = snapshot.life;
324
+ const phase = life.phase;
325
+ const state = phase === "active" || phase === "quiescent" ? "out" : phase;
326
+ return {
327
+ id,
328
+ ...(alias ? { alias } : {}),
329
+ projectionDir,
330
+ snapshot,
331
+ identityFacts,
332
+ diagnostics,
333
+ state,
334
+ };
335
+ }
336
+ function readProjectionRow(summary, nowMs, readDetails) {
337
+ const { id, alias, projectionDir, snapshot } = summary;
338
+ const diagnostics = [...summary.diagnostics];
339
+ if (snapshot.compatibility === "incompatible") {
340
+ return {
341
+ id,
342
+ observedAtMs: nowMs,
343
+ ...(alias ? { alias } : {}),
297
344
  state: "incompatible",
298
345
  observation: snapshot,
299
346
  tellCounts: emptyTellCounts(),
300
347
  diagnostics,
301
348
  };
302
349
  }
303
- const identityFacts = readIdentityFacts(projectionDir, diagnostics);
304
- const tellCounts = identityFacts.identity
305
- ? readHeartTellCounts(projectionDir, diagnostics)
306
- : emptyTellCounts();
350
+ const identityFacts = summary.identityFacts;
307
351
  if (!identityFacts.identity) {
308
352
  const diagnostic = diagnostics.map((entry) => entry.detail).find((detail) => detail !== undefined)
309
353
  ?? "projection identity could not be observed";
310
354
  return {
311
355
  id,
356
+ observedAtMs: nowMs,
312
357
  ...(alias ? { alias } : {}),
313
358
  state: "unknown",
314
359
  observation: {
315
360
  compatibility: "current",
316
361
  lifecycle: { phase: "unknown", diagnostic },
317
362
  },
318
- tellCounts,
363
+ tellCounts: emptyTellCounts(),
319
364
  diagnostics,
320
365
  };
321
366
  }
322
367
  const life = snapshot.life;
323
- const current = snapshot.records
324
- ? selectCurrentGeneration(snapshot.records)
325
- : null;
368
+ const current = snapshot.records ? selectCurrentGeneration(snapshot.records) : null;
326
369
  const adoptedAt = current?.adoption?.facts.adoptedAt;
327
370
  const adoptedAtMs = typeof adoptedAt === "string"
328
371
  ? Date.parse(adoptedAt)
329
372
  : Number.NaN;
330
- const activityFacts = readActivityFacts(projectionDir, life.executionId, nowMs, diagnostics, typeof adoptedAt === "string" && Number.isFinite(adoptedAtMs) ? adoptedAt : identityFacts.mintedAt);
373
+ const tellCounts = readDetails
374
+ ? readHeartTellCounts(projectionDir, diagnostics)
375
+ : emptyTellCounts();
376
+ const pendingTells = readDetails
377
+ ? readPendingTells(projectionDir, diagnostics)
378
+ : [];
379
+ const activityFacts = readDetails
380
+ ? readActivityFacts(projectionDir, life.executionId, nowMs, diagnostics, typeof adoptedAt === "string" && Number.isFinite(adoptedAtMs) ? adoptedAt : identityFacts.mintedAt)
381
+ : {};
331
382
  const mintedAtMs = Date.parse(identityFacts.mintedAt ?? "");
332
383
  const terminalAt = terminalTimestamp(current);
333
384
  const durationAnchorMs = current?.verdict
@@ -348,11 +399,13 @@ function readProjectionRow(projectionDir, id, alias, nowMs) {
348
399
  : undefined;
349
400
  return {
350
401
  id,
402
+ observedAtMs: nowMs,
351
403
  ...(alias ? { alias } : {}),
352
404
  state,
353
405
  observation: { compatibility: "current", lifecycle: life },
354
406
  akuma: identityFacts.akuma,
355
407
  provider: identityFacts.provider,
408
+ binding: identityFacts.binding,
356
409
  pid: identityFacts.pid,
357
410
  mintedAt: identityFacts.mintedAt,
358
411
  ...(terminalAt ? { terminalAt } : {}),
@@ -362,29 +415,11 @@ function readProjectionRow(projectionDir, id, alias, nowMs) {
362
415
  ...(displayAgeMs !== undefined ? { activityAgeMs: displayAgeMs } : {}),
363
416
  ...(terminalFailure ? { terminalFailure } : {}),
364
417
  tellCounts,
418
+ ...(pendingTells.length > 0 ? { pendingTells } : {}),
365
419
  diagnostics,
366
420
  };
367
421
  }
368
- export async function readProjectionStatusBoard(cwd, nowMs, options = {}) {
369
- const root = await projectionRoot(cwd);
370
- try {
371
- if (!fs.lstatSync(root).isDirectory())
372
- return null;
373
- }
374
- catch (error) {
375
- if (isErrnoException(error) && error.code === "ENOENT")
376
- return null;
377
- throw error;
378
- }
379
- let entries;
380
- try {
381
- entries = fs.readdirSync(root);
382
- }
383
- catch (error) {
384
- if (isErrnoException(error) && error.code === "ENOENT")
385
- return null;
386
- throw error;
387
- }
422
+ async function readProjectionStatusAliases(cwd, root) {
388
423
  const aliases = new Map();
389
424
  const aliasRoot = path.join(path.dirname(path.dirname(root)), ".keiyaku", "projection-alias");
390
425
  try {
@@ -400,34 +435,122 @@ export async function readProjectionStatusBoard(cwd, nowMs, options = {}) {
400
435
  if (!(isErrnoException(error) && error.code === "ENOENT"))
401
436
  throw error;
402
437
  }
403
- const rows = [];
404
- for (const akuma of entries.sort()) {
405
- if (akuma.startsWith("."))
438
+ return aliases;
439
+ }
440
+ function readProjectionTopologyEntries(directory) {
441
+ try {
442
+ if (!fs.lstatSync(directory).isDirectory())
443
+ return null;
444
+ return fs.readdirSync(directory).sort();
445
+ }
446
+ catch (error) {
447
+ if (isErrnoException(error) && error.code === "ENOENT")
448
+ return null;
449
+ throw error;
450
+ }
451
+ }
452
+ async function readProjectionRootTopology(cwd) {
453
+ const root = await projectionRoot(cwd);
454
+ const akumas = readProjectionTopologyEntries(root);
455
+ return akumas ? { root, akumas } : null;
456
+ }
457
+ function readAkumaProjectionStatusSummaries(root, akuma, aliases, nowMs) {
458
+ const akumaDir = path.join(root, akuma);
459
+ const hexes = readProjectionTopologyEntries(akumaDir);
460
+ if (!hexes)
461
+ return null;
462
+ const summaries = [];
463
+ for (const hex of hexes) {
464
+ const id = `${akuma}/${hex}`;
465
+ const dir = path.join(akumaDir, hex);
466
+ if (!isProjectionId(id))
406
467
  continue;
407
- const akumaDir = path.join(root, akuma);
408
468
  try {
409
- if (!fs.lstatSync(akumaDir).isDirectory())
469
+ if (!fs.lstatSync(dir).isDirectory())
410
470
  continue;
411
471
  }
412
472
  catch {
413
473
  continue;
414
474
  }
415
- for (const hex of fs.readdirSync(akumaDir).sort()) {
416
- const id = `${akuma}/${hex}`;
417
- const dir = path.join(akumaDir, hex);
418
- if (!isProjectionId(id))
419
- continue;
420
- try {
421
- if (!fs.lstatSync(dir).isDirectory())
422
- continue;
423
- }
424
- catch {
425
- continue;
426
- }
427
- rows.push(readProjectionRow(dir, id, aliases.get(id), nowMs));
428
- }
475
+ summaries.push(readProjectionSummary(dir, id, aliases.get(id), nowMs));
476
+ }
477
+ return summaries;
478
+ }
479
+ function isNonterminalProjectionStatusSummary(summary) {
480
+ return summary.snapshot.compatibility === "current"
481
+ && requiresProjectionStatusDetails(summary.state);
482
+ }
483
+ export async function readOperationalProjectionStatusBoard(cwd, nowMs) {
484
+ const topology = await readProjectionRootTopology(cwd);
485
+ if (!topology)
486
+ return null;
487
+ const { root, akumas } = topology;
488
+ const aliases = await readProjectionStatusAliases(cwd, root);
489
+ const summaries = [];
490
+ for (const akuma of akumas) {
491
+ if (akuma.startsWith("."))
492
+ continue;
493
+ const scoped = readAkumaProjectionStatusSummaries(root, akuma, aliases, nowMs);
494
+ // Seeing an entry and then being unable to observe its namespace is not an
495
+ // observable zero. Keep the whole aggregate surface unavailable.
496
+ if (scoped === null)
497
+ return null;
498
+ summaries.push(...scoped);
499
+ }
500
+ const rows = summaries.map((summary) => readProjectionRow(summary, nowMs, requiresProjectionStatusDetails(summary.state)));
501
+ // A published root is an observable surface even when it currently has no
502
+ // rows. The caller must distinguish that fact from an unavailable root.
503
+ return prioritizeProjectionStatusRows(rows);
504
+ }
505
+ export async function readExactProjectionStatus(cwd, nowMs, projectionId) {
506
+ if (!isProjectionId(projectionId))
507
+ return null;
508
+ const root = await projectionRoot(cwd);
509
+ try {
510
+ if (!fs.lstatSync(root).isDirectory())
511
+ return null;
512
+ }
513
+ catch (error) {
514
+ if (isErrnoException(error) && error.code === "ENOENT")
515
+ return null;
516
+ throw error;
517
+ }
518
+ const [akuma, hex] = projectionId.split("/");
519
+ const dir = path.join(root, akuma, hex);
520
+ try {
521
+ if (!fs.lstatSync(dir).isDirectory())
522
+ return null;
523
+ }
524
+ catch (error) {
525
+ if (isErrnoException(error) && error.code === "ENOENT")
526
+ return null;
527
+ throw error;
528
+ }
529
+ const aliases = await readProjectionStatusAliases(cwd, root);
530
+ return readProjectionRow(readProjectionSummary(dir, projectionId, aliases.get(projectionId), nowMs), nowMs, true);
531
+ }
532
+ export async function readAkumaNonterminalProjectionStatus(cwd, nowMs, akuma) {
533
+ if (!isProjectionId(`${akuma}/00000000`))
534
+ return null;
535
+ const root = await projectionRoot(cwd);
536
+ try {
537
+ if (!fs.lstatSync(root).isDirectory())
538
+ return null;
539
+ }
540
+ catch (error) {
541
+ if (isErrnoException(error) && error.code === "ENOENT")
542
+ return null;
543
+ throw error;
429
544
  }
430
- return rows.length > 0 ? prioritizeProjectionStatusRows(rows, options.showAll === true) : null;
545
+ const aliases = await readProjectionStatusAliases(cwd, root);
546
+ const summaries = readAkumaProjectionStatusSummaries(root, akuma, aliases, nowMs);
547
+ if (summaries === null)
548
+ return null;
549
+ const rows = summaries
550
+ .filter(isNonterminalProjectionStatusSummary)
551
+ .map((summary) => readProjectionRow(summary, nowMs, true))
552
+ .sort(compareProjectionStatusRows);
553
+ return rows.length > 0 ? { rows } : null;
431
554
  }
432
555
  /**
433
556
  * Renew-only lifecycle guard. Immutable identity is checked before lifecycle,