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