@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.
- 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/harness/projection.js +8 -6
- 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 +238 -80
- package/build/cli/render/path-prefix-compaction.js +119 -76
- package/build/cli/render/projection-activity.js +65 -21
- package/build/cli/render/shared.js +8 -7
- package/build/cli/render/status.js +11 -23
- package/build/cli/render/wait.js +42 -64
- package/build/config/env-keys.js +1 -1
- package/build/config/env.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 +52 -38
- 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-activity.js +2 -0
- 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 +183 -60
- package/build/core/projection/projection-wake.js +171 -72
- package/build/core/status/board.js +55 -14
- package/build/core/status/drift.js +21 -5
- package/build/core/status/ledger-batch.js +1 -158
- package/build/core/task/settlement-git.js +2 -2
- 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";
|
|
@@ -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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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(
|
|
469
|
+
if (!fs.lstatSync(dir).isDirectory())
|
|
410
470
|
continue;
|
|
411
471
|
}
|
|
412
472
|
catch {
|
|
413
473
|
continue;
|
|
414
474
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
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,
|