@astrosheep/keiyaku 2.9.7 → 2.9.8
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.
- package/build/.tsbuildinfo +1 -1
- package/build/agents/harness/event-persistence.js +7 -5
- package/build/agents/harness/events.js +3 -2
- package/build/agents/providers/codex-app-server/adapter.js +6 -1
- package/build/agents/providers/codex-app-server/session.js +8 -7
- package/build/agents/selector.js +12 -1
- package/build/cli/commands/akuma/view/handler.js +3 -11
- package/build/cli/commands/contract/amend/handler.js +1 -1
- package/build/cli/commands/contract/amend/meta.js +4 -4
- package/build/cli/commands/projection/status/handler.js +5 -4
- package/build/cli/commands/projection/status/meta.js +2 -2
- package/build/cli/commands/task/add/meta.js +9 -1
- package/build/cli/commands/task/shared.js +2 -1
- package/build/cli/completion.js +8 -0
- package/build/cli/render/kanshi.js +2 -2
- package/build/cli/render/path-prefix-compaction.js +119 -76
- package/build/cli/render/projection-activity.js +10 -1
- package/build/cli/render/shared.js +8 -7
- package/build/cli/render/status.js +8 -5
- package/build/cli/render/wait.js +1 -1
- package/build/config/settings/disease.js +4 -4
- package/build/config/settings/loader.js +44 -21
- package/build/core/addressing.js +40 -9
- package/build/core/amend.js +21 -5
- package/build/core/call/context.js +19 -3
- package/build/core/call/execution.js +43 -20
- package/build/core/ledger-batch.js +194 -0
- package/build/core/projection/generation/database.js +22 -0
- package/build/core/projection/generation/projection-generation-execution.js +45 -37
- package/build/core/projection/generation/projection-generation-launcher.js +148 -20
- package/build/core/projection/generation/projection-generation-process.js +3 -1
- package/build/core/projection/generation/projection-generation-runner.js +76 -40
- package/build/core/projection/generation/projection-generation-runtime.js +82 -19
- package/build/core/projection/generation/store.js +17 -1
- package/build/core/projection/generation/transitions.js +89 -12
- package/build/core/projection/index.js +3 -3
- package/build/core/projection/projection-kill.js +22 -10
- package/build/core/projection/projection-runner-lock.js +177 -37
- package/build/core/projection/projection-status.js +143 -55
- package/build/core/projection/projection-wake.js +171 -72
- package/build/core/status/board.js +42 -4
- package/build/core/status/drift.js +21 -5
- package/build/core/status/ledger-batch.js +1 -158
- package/build/core/task/task-git-runtime.js +8 -10
- package/build/core/task/task-git-store.js +5 -3
- package/build/core/worktree-path.js +39 -25
- package/build/flow-error.js +1 -1
- package/build/generated/version.js +2 -2
- package/build/git/refs.js +47 -1
- package/package.json +1 -1
- package/skills/keiyaku-akuma/SKILL.md +18 -0
- 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
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
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
|
-
|
|
207
|
+
assertBusyTimeout(busyTimeoutMs);
|
|
208
|
+
let guard;
|
|
120
209
|
try {
|
|
121
|
-
|
|
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
|
-
|
|
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.
|
|
290
|
+
database.close();
|
|
137
291
|
}
|
|
138
|
-
catch {
|
|
139
|
-
|
|
140
|
-
|
|
292
|
+
catch { }
|
|
293
|
+
if (isSqliteBusy(error)) {
|
|
294
|
+
throw new ProjectionRunnerLockUnavailableError("projection runner lock is already held", { cause: error });
|
|
141
295
|
}
|
|
142
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
172
|
-
*
|
|
173
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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";
|
|
@@ -9,7 +9,7 @@ import { readProjectionIdentity } from "./projection-identity.js";
|
|
|
9
9
|
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
|
-
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
|
|
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) {
|
|
@@ -284,12 +285,40 @@ function readActivityFacts(projectionDir, executionId, nowMs, diagnostics, fallb
|
|
|
284
285
|
function terminalTimestamp(current) {
|
|
285
286
|
return extractTerminalTimestamp(current?.verdict?.facts);
|
|
286
287
|
}
|
|
287
|
-
function
|
|
288
|
+
function readProjectionSummary(projectionDir, id, alias, nowMs) {
|
|
288
289
|
const diagnostics = [];
|
|
289
290
|
const snapshot = observeProjectionLifeSnapshot(projectionDir, {
|
|
290
291
|
nowMs,
|
|
291
292
|
startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
|
|
292
293
|
});
|
|
294
|
+
if (snapshot.compatibility === "incompatible") {
|
|
295
|
+
return {
|
|
296
|
+
id,
|
|
297
|
+
...(alias ? { alias } : {}),
|
|
298
|
+
projectionDir,
|
|
299
|
+
snapshot,
|
|
300
|
+
identityFacts: null,
|
|
301
|
+
diagnostics,
|
|
302
|
+
state: "incompatible",
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const identityFacts = readIdentityFacts(projectionDir, diagnostics);
|
|
306
|
+
const life = snapshot.life;
|
|
307
|
+
const phase = life.phase;
|
|
308
|
+
const state = phase === "active" || phase === "quiescent" ? "out" : phase;
|
|
309
|
+
return {
|
|
310
|
+
id,
|
|
311
|
+
...(alias ? { alias } : {}),
|
|
312
|
+
projectionDir,
|
|
313
|
+
snapshot,
|
|
314
|
+
identityFacts,
|
|
315
|
+
diagnostics,
|
|
316
|
+
state,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function readProjectionRow(summary, nowMs, readDetails) {
|
|
320
|
+
const { id, alias, projectionDir, snapshot } = summary;
|
|
321
|
+
const diagnostics = [...summary.diagnostics];
|
|
293
322
|
if (snapshot.compatibility === "incompatible") {
|
|
294
323
|
return {
|
|
295
324
|
id,
|
|
@@ -300,10 +329,7 @@ function readProjectionRow(projectionDir, id, alias, nowMs) {
|
|
|
300
329
|
diagnostics,
|
|
301
330
|
};
|
|
302
331
|
}
|
|
303
|
-
const identityFacts =
|
|
304
|
-
const tellCounts = identityFacts.identity
|
|
305
|
-
? readHeartTellCounts(projectionDir, diagnostics)
|
|
306
|
-
: emptyTellCounts();
|
|
332
|
+
const identityFacts = summary.identityFacts;
|
|
307
333
|
if (!identityFacts.identity) {
|
|
308
334
|
const diagnostic = diagnostics.map((entry) => entry.detail).find((detail) => detail !== undefined)
|
|
309
335
|
?? "projection identity could not be observed";
|
|
@@ -315,19 +341,22 @@ function readProjectionRow(projectionDir, id, alias, nowMs) {
|
|
|
315
341
|
compatibility: "current",
|
|
316
342
|
lifecycle: { phase: "unknown", diagnostic },
|
|
317
343
|
},
|
|
318
|
-
tellCounts,
|
|
344
|
+
tellCounts: emptyTellCounts(),
|
|
319
345
|
diagnostics,
|
|
320
346
|
};
|
|
321
347
|
}
|
|
322
348
|
const life = snapshot.life;
|
|
323
|
-
const current = snapshot.records
|
|
324
|
-
? selectCurrentGeneration(snapshot.records)
|
|
325
|
-
: null;
|
|
349
|
+
const current = snapshot.records ? selectCurrentGeneration(snapshot.records) : null;
|
|
326
350
|
const adoptedAt = current?.adoption?.facts.adoptedAt;
|
|
327
351
|
const adoptedAtMs = typeof adoptedAt === "string"
|
|
328
352
|
? Date.parse(adoptedAt)
|
|
329
353
|
: Number.NaN;
|
|
330
|
-
const
|
|
354
|
+
const tellCounts = readDetails
|
|
355
|
+
? readHeartTellCounts(projectionDir, diagnostics)
|
|
356
|
+
: emptyTellCounts();
|
|
357
|
+
const activityFacts = readDetails
|
|
358
|
+
? readActivityFacts(projectionDir, life.executionId, nowMs, diagnostics, typeof adoptedAt === "string" && Number.isFinite(adoptedAtMs) ? adoptedAt : identityFacts.mintedAt)
|
|
359
|
+
: {};
|
|
331
360
|
const mintedAtMs = Date.parse(identityFacts.mintedAt ?? "");
|
|
332
361
|
const terminalAt = terminalTimestamp(current);
|
|
333
362
|
const durationAnchorMs = current?.verdict
|
|
@@ -365,26 +394,7 @@ function readProjectionRow(projectionDir, id, alias, nowMs) {
|
|
|
365
394
|
diagnostics,
|
|
366
395
|
};
|
|
367
396
|
}
|
|
368
|
-
|
|
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
|
-
}
|
|
397
|
+
async function readProjectionStatusAliases(cwd, root) {
|
|
388
398
|
const aliases = new Map();
|
|
389
399
|
const aliasRoot = path.join(path.dirname(path.dirname(root)), ".keiyaku", "projection-alias");
|
|
390
400
|
try {
|
|
@@ -400,34 +410,112 @@ export async function readProjectionStatusBoard(cwd, nowMs, options = {}) {
|
|
|
400
410
|
if (!(isErrnoException(error) && error.code === "ENOENT"))
|
|
401
411
|
throw error;
|
|
402
412
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
413
|
+
return aliases;
|
|
414
|
+
}
|
|
415
|
+
function readProjectionTopologyEntries(directory) {
|
|
416
|
+
try {
|
|
417
|
+
if (!fs.lstatSync(directory).isDirectory())
|
|
418
|
+
return null;
|
|
419
|
+
return fs.readdirSync(directory).sort();
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
if (isErrnoException(error) && error.code === "ENOENT")
|
|
423
|
+
return null;
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async function readProjectionRootTopology(cwd) {
|
|
428
|
+
const root = await projectionRoot(cwd);
|
|
429
|
+
const akumas = readProjectionTopologyEntries(root);
|
|
430
|
+
return akumas ? { root, akumas } : null;
|
|
431
|
+
}
|
|
432
|
+
function readAkumaProjectionStatusSummaries(root, akuma, aliases, nowMs) {
|
|
433
|
+
const akumaDir = path.join(root, akuma);
|
|
434
|
+
const hexes = readProjectionTopologyEntries(akumaDir);
|
|
435
|
+
if (!hexes)
|
|
436
|
+
return [];
|
|
437
|
+
const summaries = [];
|
|
438
|
+
for (const hex of hexes) {
|
|
439
|
+
const id = `${akuma}/${hex}`;
|
|
440
|
+
const dir = path.join(akumaDir, hex);
|
|
441
|
+
if (!isProjectionId(id))
|
|
406
442
|
continue;
|
|
407
|
-
const akumaDir = path.join(root, akuma);
|
|
408
443
|
try {
|
|
409
|
-
if (!fs.lstatSync(
|
|
444
|
+
if (!fs.lstatSync(dir).isDirectory())
|
|
410
445
|
continue;
|
|
411
446
|
}
|
|
412
447
|
catch {
|
|
413
448
|
continue;
|
|
414
449
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
450
|
+
summaries.push(readProjectionSummary(dir, id, aliases.get(id), nowMs));
|
|
451
|
+
}
|
|
452
|
+
return summaries;
|
|
453
|
+
}
|
|
454
|
+
function isNonterminalProjectionStatusSummary(summary) {
|
|
455
|
+
return summary.snapshot.compatibility === "current"
|
|
456
|
+
&& requiresProjectionStatusDetails(summary.state);
|
|
457
|
+
}
|
|
458
|
+
export async function readOperationalProjectionStatusBoard(cwd, nowMs) {
|
|
459
|
+
const topology = await readProjectionRootTopology(cwd);
|
|
460
|
+
if (!topology)
|
|
461
|
+
return null;
|
|
462
|
+
const { root, akumas } = topology;
|
|
463
|
+
const aliases = await readProjectionStatusAliases(cwd, root);
|
|
464
|
+
const summaries = [];
|
|
465
|
+
for (const akuma of akumas) {
|
|
466
|
+
if (akuma.startsWith("."))
|
|
467
|
+
continue;
|
|
468
|
+
summaries.push(...readAkumaProjectionStatusSummaries(root, akuma, aliases, nowMs));
|
|
469
|
+
}
|
|
470
|
+
const rows = summaries.map((summary) => readProjectionRow(summary, nowMs, requiresProjectionStatusDetails(summary.state)));
|
|
471
|
+
return rows.length > 0 ? prioritizeProjectionStatusRows(rows) : null;
|
|
472
|
+
}
|
|
473
|
+
export async function readExactProjectionStatus(cwd, nowMs, projectionId) {
|
|
474
|
+
if (!isProjectionId(projectionId))
|
|
475
|
+
return null;
|
|
476
|
+
const root = await projectionRoot(cwd);
|
|
477
|
+
try {
|
|
478
|
+
if (!fs.lstatSync(root).isDirectory())
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
if (isErrnoException(error) && error.code === "ENOENT")
|
|
483
|
+
return null;
|
|
484
|
+
throw error;
|
|
485
|
+
}
|
|
486
|
+
const [akuma, hex] = projectionId.split("/");
|
|
487
|
+
const dir = path.join(root, akuma, hex);
|
|
488
|
+
try {
|
|
489
|
+
if (!fs.lstatSync(dir).isDirectory())
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
if (isErrnoException(error) && error.code === "ENOENT")
|
|
494
|
+
return null;
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
const aliases = await readProjectionStatusAliases(cwd, root);
|
|
498
|
+
return readProjectionRow(readProjectionSummary(dir, projectionId, aliases.get(projectionId), nowMs), nowMs, true);
|
|
499
|
+
}
|
|
500
|
+
export async function readAkumaNonterminalProjectionStatus(cwd, nowMs, akuma) {
|
|
501
|
+
if (!isProjectionId(`${akuma}/00000000`))
|
|
502
|
+
return null;
|
|
503
|
+
const root = await projectionRoot(cwd);
|
|
504
|
+
try {
|
|
505
|
+
if (!fs.lstatSync(root).isDirectory())
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
if (isErrnoException(error) && error.code === "ENOENT")
|
|
510
|
+
return null;
|
|
511
|
+
throw error;
|
|
429
512
|
}
|
|
430
|
-
|
|
513
|
+
const aliases = await readProjectionStatusAliases(cwd, root);
|
|
514
|
+
const rows = readAkumaProjectionStatusSummaries(root, akuma, aliases, nowMs)
|
|
515
|
+
.filter(isNonterminalProjectionStatusSummary)
|
|
516
|
+
.map((summary) => readProjectionRow(summary, nowMs, true))
|
|
517
|
+
.sort(compareProjectionStatusRows);
|
|
518
|
+
return rows.length > 0 ? { rows } : null;
|
|
431
519
|
}
|
|
432
520
|
/**
|
|
433
521
|
* Renew-only lifecycle guard. Immutable identity is checked before lifecycle,
|