@astrosheep/keiyaku 2.9.6 → 2.9.7
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/outcome.js +10 -0
- package/build/cli/commands/contract/bind/handler.js +10 -4
- package/build/cli/commands/contract/bind/meta.js +6 -6
- package/build/cli/commands/contract/petition/handler.js +16 -3
- package/build/cli/commands/contract/petition/meta.js +4 -4
- package/build/cli/commands/metadata.js +3 -2
- package/build/cli/commands/projection/tell/handler.js +10 -2
- package/build/cli/commands/projection/tell/meta.js +3 -3
- package/build/cli/commands/task/catalog.js +2 -0
- package/build/cli/commands/task/log/handler.js +12 -0
- package/build/cli/commands/task/log/meta.js +8 -0
- package/build/cli/flags.js +8 -0
- package/build/cli/index.js +10 -6
- package/build/cli/parse-flags.js +6 -0
- package/build/cli/parse-metadata.js +1 -1
- package/build/cli/render/line-width.js +33 -0
- package/build/cli/render/path-prefix-compaction.js +88 -0
- package/build/cli/render/petition.js +4 -0
- package/build/cli/render/projection-activity.js +93 -20
- package/build/cli/render/shared.js +34 -12
- package/build/cli/render/success-response.js +2 -0
- package/build/cli/render/tool-presentation.js +3 -3
- package/build/cli/render/wait.js +68 -48
- package/build/cli/subagent-guard.js +3 -0
- package/build/cli/types.js +1 -1
- package/build/core/bind.js +111 -6
- package/build/core/draft.js +1 -1
- package/build/core/projection/generation/database.js +21 -2
- package/build/core/projection/generation/model.js +24 -3
- package/build/core/projection/generation/projection-generation-continuation.js +75 -10
- package/build/core/projection/generation/projection-generation-execution.js +4 -3
- package/build/core/projection/generation/projection-generation-runner.js +96 -53
- package/build/core/projection/generation/projection-generation-runtime.js +19 -0
- package/build/core/projection/generation/store.js +9 -0
- package/build/core/projection/generation/transitions.js +105 -3
- package/build/core/projection/index.js +2 -2
- package/build/core/projection/projection-core.js +1 -1
- package/build/core/projection/projection-kill.js +31 -0
- package/build/core/projection/projection-life-protocol.js +10 -0
- package/build/core/projection/projection-wait.js +53 -15
- package/build/core/projection/projection-wake.js +35 -3
- package/build/core/projection/tell/database.js +18 -0
- package/build/core/projection/tell/model.js +1 -0
- package/build/core/projection/tell/store.js +102 -55
- package/build/core/registry.js +82 -69
- package/build/core/scope.js +9 -9
- package/build/core/task/index.js +2 -2
- package/build/core/task/task-contract.js +18 -0
- package/build/core/task/task-git-store.js +50 -0
- package/build/generated/version.js +2 -2
- package/package.json +1 -1
- package/skills/keiyaku-akuma/SKILL.md +10 -0
- package/skills/keiyaku-workflow/SKILL.md +76 -7
|
@@ -4,7 +4,7 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
4
4
|
import { configureHeartBusyTimeout } from "../heart.js";
|
|
5
5
|
import { ProjectionGenerationStoreError, parseStoredRow, validateHistory, } from "./model.js";
|
|
6
6
|
export const PROJECTION_GENERATION_STORE_FILE = "heart";
|
|
7
|
-
export const PROJECTION_GENERATION_SCHEMA_VERSION =
|
|
7
|
+
export const PROJECTION_GENERATION_SCHEMA_VERSION = 3;
|
|
8
8
|
const SCHEMA_TABLE_SQL = `
|
|
9
9
|
CREATE TABLE generation_schema (
|
|
10
10
|
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
@@ -14,7 +14,7 @@ const SCHEMA_TABLE_SQL = `
|
|
|
14
14
|
const RECORD_TABLE_SQL = `
|
|
15
15
|
CREATE TABLE generation_records (
|
|
16
16
|
seq INTEGER PRIMARY KEY CHECK (seq > 0),
|
|
17
|
-
kind TEXT NOT NULL CHECK (kind IN ('launch', 'adoption', 'kill-intent', 'verdict')),
|
|
17
|
+
kind TEXT NOT NULL CHECK (kind IN ('launch', 'adoption', 'kill-intent', 'interrupt-intent', 'verdict')),
|
|
18
18
|
execution_id TEXT NOT NULL CHECK (length(trim(execution_id)) > 0),
|
|
19
19
|
facts_json TEXT NOT NULL CHECK (json_valid(facts_json))
|
|
20
20
|
) STRICT
|
|
@@ -250,6 +250,25 @@ export function openReadOnlyDatabase(projectionDirectory) {
|
|
|
250
250
|
throw projectionGenerationStoreReadError(databasePath, error);
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
|
+
/** Read the validated heart through one consistent SQLite snapshot. */
|
|
254
|
+
export function withGenerationReadSnapshot(projectionDirectory, read) {
|
|
255
|
+
const { database } = openReadOnlyDatabase(projectionDirectory);
|
|
256
|
+
try {
|
|
257
|
+
database.exec("BEGIN");
|
|
258
|
+
try {
|
|
259
|
+
const result = read(database);
|
|
260
|
+
database.exec("COMMIT");
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
rollbackQuietly(database);
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
closeDatabase(database);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
253
272
|
export function assertDatabaseOpen(database) {
|
|
254
273
|
if (closedDatabases.has(database))
|
|
255
274
|
throw new ProjectionGenerationStoreError("projection generation store is closed");
|
|
@@ -133,6 +133,16 @@ export function validateKillIntentFacts(value, location) {
|
|
|
133
133
|
throw new ProjectionGenerationStoreError(`${location}.state is reserved for verdict records`);
|
|
134
134
|
return facts;
|
|
135
135
|
}
|
|
136
|
+
export function validateInterruptIntentFacts(value, location) {
|
|
137
|
+
const facts = cloneFacts(value, location);
|
|
138
|
+
facts.interruptedAt = assertIsoTimestamp(facts.interruptedAt, `${location}.interruptedAt`);
|
|
139
|
+
if ("operatorAction" in facts) {
|
|
140
|
+
throw new ProjectionGenerationStoreError(`${location}.operatorAction is not part of interrupt-intent`);
|
|
141
|
+
}
|
|
142
|
+
if ("state" in facts)
|
|
143
|
+
throw new ProjectionGenerationStoreError(`${location}.state is reserved for verdict records`);
|
|
144
|
+
return facts;
|
|
145
|
+
}
|
|
136
146
|
export function validateVerdictFacts(value, location) {
|
|
137
147
|
const facts = cloneFacts(value, location);
|
|
138
148
|
if (typeof facts.state !== "string" || !VERDICTS.has(facts.state)) {
|
|
@@ -182,6 +192,7 @@ export function parseStoredRow(row) {
|
|
|
182
192
|
if (row.kind !== "launch"
|
|
183
193
|
&& row.kind !== "adoption"
|
|
184
194
|
&& row.kind !== "kill-intent"
|
|
195
|
+
&& row.kind !== "interrupt-intent"
|
|
185
196
|
&& row.kind !== "verdict") {
|
|
186
197
|
throw new ProjectionGenerationStoreError(`generation row ${seq} has unknown kind '${row.kind}'`);
|
|
187
198
|
}
|
|
@@ -200,7 +211,9 @@ export function parseStoredRow(row) {
|
|
|
200
211
|
? validateAdoptionFacts(decoded, location)
|
|
201
212
|
: row.kind === "kill-intent"
|
|
202
213
|
? validateKillIntentFacts(decoded, location)
|
|
203
|
-
:
|
|
214
|
+
: row.kind === "interrupt-intent"
|
|
215
|
+
? validateInterruptIntentFacts(decoded, location)
|
|
216
|
+
: validateVerdictFacts(decoded, location);
|
|
204
217
|
return { seq, kind: row.kind, executionId, facts };
|
|
205
218
|
}
|
|
206
219
|
export function validateHistory(records) {
|
|
@@ -237,13 +250,21 @@ export function validateHistory(records) {
|
|
|
237
250
|
continue;
|
|
238
251
|
}
|
|
239
252
|
if (record.kind === "kill-intent") {
|
|
240
|
-
if (current.killIntent)
|
|
241
|
-
throw new ProjectionGenerationStoreError(`generation row ${record.seq} duplicates
|
|
253
|
+
if (current.killIntent || current.interruptIntent)
|
|
254
|
+
throw new ProjectionGenerationStoreError(`generation row ${record.seq} duplicates generation stop intent`);
|
|
242
255
|
if (current.verdict)
|
|
243
256
|
throw new ProjectionGenerationStoreError(`generation row ${record.seq} records kill-intent after verdict`);
|
|
244
257
|
current.killIntent = record;
|
|
245
258
|
continue;
|
|
246
259
|
}
|
|
260
|
+
if (record.kind === "interrupt-intent") {
|
|
261
|
+
if (current.killIntent || current.interruptIntent)
|
|
262
|
+
throw new ProjectionGenerationStoreError(`generation row ${record.seq} duplicates generation stop intent`);
|
|
263
|
+
if (current.verdict)
|
|
264
|
+
throw new ProjectionGenerationStoreError(`generation row ${record.seq} records interrupt-intent after verdict`);
|
|
265
|
+
current.interruptIntent = record;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
247
268
|
if (current.verdict)
|
|
248
269
|
throw new ProjectionGenerationStoreError(`generation row ${record.seq} duplicates verdict`);
|
|
249
270
|
current.verdict = record;
|
|
@@ -1,22 +1,87 @@
|
|
|
1
1
|
import { ProjectionStateError } from "../../atomic-publish.js";
|
|
2
2
|
import { applySnapshotEffort, parseResolvedAkumaLaunchSnapshot, } from "../../../agents/launch-snapshot/model.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
import { listTellMetadataFromDatabase, readPendingTellWindowFromDatabase, } from "../tell/store.js";
|
|
4
|
+
import { PENDING_PROJECTION_TELL_STATES } from "../tell/model.js";
|
|
5
|
+
import { readValidatedHistory, withGenerationReadSnapshot } from "./database.js";
|
|
6
|
+
function retainedFenceFor(current) {
|
|
7
|
+
return current?.verdict?.facts.state === "launch-failed"
|
|
8
|
+
? current.launch.facts.tellFence ?? []
|
|
9
|
+
: [];
|
|
10
|
+
}
|
|
11
|
+
function interruptedForReplay(current) {
|
|
12
|
+
return current?.verdict?.facts.state === "interrupted";
|
|
13
|
+
}
|
|
14
|
+
function snapshotTells(database, layers, admit) {
|
|
15
|
+
const pending = listTellMetadataFromDatabase(database, layers)
|
|
16
|
+
.filter((item) => admit?.({
|
|
17
|
+
layer: item.state,
|
|
18
|
+
tellId: item.tellId,
|
|
19
|
+
...(item.executionId ? { executionId: item.executionId } : {}),
|
|
20
|
+
}) ?? true);
|
|
9
21
|
const effort = pending.reduce((selected, item) => {
|
|
10
|
-
|
|
11
|
-
? readTellSubmitted(projectionDirectory, item.tellId)
|
|
12
|
-
: readTellOriginal(projectionDirectory, item.layer, item.tellId);
|
|
13
|
-
return tell.effort ?? selected;
|
|
22
|
+
return item.effort ?? selected;
|
|
14
23
|
}, undefined);
|
|
15
24
|
return {
|
|
16
25
|
tellIds: pending.map((item) => item.tellId),
|
|
17
26
|
...(effort ? { effort } : {}),
|
|
18
27
|
};
|
|
19
28
|
}
|
|
29
|
+
/** Ordinary completed-generation handoff retains only tells not yet delivered. */
|
|
30
|
+
export function snapshotPendingTells(projectionDirectory) {
|
|
31
|
+
return withGenerationReadSnapshot(projectionDirectory, (database) => snapshotTells(database, PENDING_PROJECTION_TELL_STATES));
|
|
32
|
+
}
|
|
33
|
+
/** Interrupted generations replay pending mail and their own delivered tells. */
|
|
34
|
+
export function snapshotInterruptTells(projectionDirectory, executionId) {
|
|
35
|
+
return withGenerationReadSnapshot(projectionDirectory, (database) => snapshotTells(database, [...PENDING_PROJECTION_TELL_STATES, "delivered"], (item) => item.layer !== "delivered" || item.executionId === executionId));
|
|
36
|
+
}
|
|
37
|
+
/** Retry ordinary pending mail plus exactly the durable fence of a failed launch. */
|
|
38
|
+
export function snapshotRetainedSuccessorTells(projectionDirectory, retainedFence) {
|
|
39
|
+
const retained = new Set(retainedFence);
|
|
40
|
+
return withGenerationReadSnapshot(projectionDirectory, (database) => snapshotTells(database, [...PENDING_PROJECTION_TELL_STATES, "delivered"], ({ layer, tellId }) => layer !== "delivered" || retained.has(tellId)));
|
|
41
|
+
}
|
|
42
|
+
/** Build the durable successor fence only after continuation is admitted. */
|
|
43
|
+
export function snapshotProjectionContinuationTells(projectionDirectory, current) {
|
|
44
|
+
const snapshot = readProjectionGenerationContinuationSnapshot(projectionDirectory, 0);
|
|
45
|
+
if (snapshot.current?.launch.executionId !== current?.launch.executionId) {
|
|
46
|
+
throw new ProjectionStateError("projection generation changed while selecting continuation tells");
|
|
47
|
+
}
|
|
48
|
+
return snapshot.successor;
|
|
49
|
+
}
|
|
50
|
+
function readContinuationFromDatabase(database, current, limit = 10) {
|
|
51
|
+
const tells = readPendingTellWindowFromDatabase(database, limit);
|
|
52
|
+
const retainedFence = retainedFenceFor(current);
|
|
53
|
+
const successor = interruptedForReplay(current)
|
|
54
|
+
? snapshotTells(database, [...PENDING_PROJECTION_TELL_STATES, "delivered"], (item) => item.layer !== "delivered" || item.executionId === current.launch.executionId)
|
|
55
|
+
: retainedFence.length > 0
|
|
56
|
+
? snapshotTells(database, [...PENDING_PROJECTION_TELL_STATES, "delivered"], ({ layer, tellId }) => layer !== "delivered" || retainedFence.includes(tellId))
|
|
57
|
+
: snapshotTells(database, PENDING_PROJECTION_TELL_STATES);
|
|
58
|
+
return {
|
|
59
|
+
tells,
|
|
60
|
+
successor,
|
|
61
|
+
requiresWake: successor.tellIds.length > 0,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** One heart snapshot owns generation selection and all continuation facts. */
|
|
65
|
+
export function readProjectionGenerationContinuationSnapshot(projectionDirectory, limit = 10) {
|
|
66
|
+
return withGenerationReadSnapshot(projectionDirectory, (database) => {
|
|
67
|
+
const { records, current } = readValidatedHistory(database);
|
|
68
|
+
const continuation = readContinuationFromDatabase(database, current, limit);
|
|
69
|
+
return {
|
|
70
|
+
records,
|
|
71
|
+
current,
|
|
72
|
+
generationSequence: records.at(-1)?.seq ?? 0,
|
|
73
|
+
...continuation,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** One continuation eligibility fact shared by tell, wait, and wake. */
|
|
78
|
+
export function readProjectionContinuation(projectionDirectory, current, limit = 10) {
|
|
79
|
+
const snapshot = readProjectionGenerationContinuationSnapshot(projectionDirectory, limit);
|
|
80
|
+
if (snapshot.current?.launch.executionId !== current?.launch.executionId) {
|
|
81
|
+
throw new ProjectionStateError("projection generation changed while reading continuation");
|
|
82
|
+
}
|
|
83
|
+
return snapshot;
|
|
84
|
+
}
|
|
20
85
|
export function applyContinuationEffort(inputs, effort) {
|
|
21
86
|
if (!effort)
|
|
22
87
|
return inputs;
|
|
@@ -2,7 +2,7 @@ import { initializeConfig } from "../../../config/env.js";
|
|
|
2
2
|
import { buildCallTerms } from "../../../agents/call-terms.js";
|
|
3
3
|
import { parseResolvedAkumaLaunchSnapshot } from "../../../agents/launch-snapshot/model.js";
|
|
4
4
|
import { revive, call, } from "../../../agents/harness/index.js";
|
|
5
|
-
import { claimFencedTells,
|
|
5
|
+
import { claimFencedTells, readTellOriginal, readProjectionEvents, } from "../projection-core.js";
|
|
6
6
|
import { createProjectionExecutionObserver, renderProjectionTellFrame, } from "../projection-execution-observer.js";
|
|
7
7
|
import { readProjectionIdentity } from "../projection-identity.js";
|
|
8
8
|
import { selectValidStoredAgentEvents } from "../../stored-agent-event.js";
|
|
@@ -140,7 +140,6 @@ export function executeProjectionGeneration(projectionDirectory, launch, provide
|
|
|
140
140
|
const inputs = parseLaunchInputs(launch);
|
|
141
141
|
initializeConfig(process.env, inputs.cwd);
|
|
142
142
|
const executionId = launch.executionId;
|
|
143
|
-
demoteSubmittedTellsForReplay(projectionDirectory, executionId);
|
|
144
143
|
const initialTellIds = claimFencedTells(projectionDirectory, launch.facts.tellFence ?? [], executionId);
|
|
145
144
|
const tellPrompt = initialTellIds.map((tellId) => {
|
|
146
145
|
const tell = readTellOriginal(projectionDirectory, "inflight", tellId);
|
|
@@ -182,7 +181,9 @@ export function executeProjectionGeneration(projectionDirectory, launch, provide
|
|
|
182
181
|
? "completed"
|
|
183
182
|
: result.status === "dismissed"
|
|
184
183
|
? "dismissed"
|
|
185
|
-
: "
|
|
184
|
+
: result.status === "interrupted"
|
|
185
|
+
? "interrupted"
|
|
186
|
+
: "failed",
|
|
186
187
|
facts: await completionFacts(projectionDirectory, executionId, inputs, result),
|
|
187
188
|
};
|
|
188
189
|
}
|
|
@@ -76,6 +76,7 @@ export async function runProjectionGeneration(input) {
|
|
|
76
76
|
let settled = false;
|
|
77
77
|
let tellObserver;
|
|
78
78
|
let graceTimer;
|
|
79
|
+
let intentTimer;
|
|
79
80
|
const clearGraceTimer = () => {
|
|
80
81
|
if (graceTimer === undefined)
|
|
81
82
|
return;
|
|
@@ -103,7 +104,8 @@ export async function runProjectionGeneration(input) {
|
|
|
103
104
|
if (current?.launch.executionId !== input.executionId) {
|
|
104
105
|
throw new Error("adopted generation stopped being current");
|
|
105
106
|
}
|
|
106
|
-
// Pre-admission kill
|
|
107
|
+
// Pre-admission kill retains its established settlement law. Interrupt
|
|
108
|
+
// intent must enter provider execution so only an actual stop can settle it.
|
|
107
109
|
if (current.killIntent) {
|
|
108
110
|
const killed = store.runnerKilledIfRequested({ executionId: input.executionId });
|
|
109
111
|
if (killed.status !== "committed")
|
|
@@ -113,10 +115,46 @@ export async function runProjectionGeneration(input) {
|
|
|
113
115
|
}
|
|
114
116
|
const handle = input.execute(current.launch);
|
|
115
117
|
tellObserver = openProjectionTellObserver(input.projectionDirectory);
|
|
118
|
+
const settleInterrupted = (facts = {}) => {
|
|
119
|
+
const transition = store.runnerInterruptedIfRequested({
|
|
120
|
+
executionId: input.executionId,
|
|
121
|
+
facts,
|
|
122
|
+
terminalAt: new Date(now()).toISOString(),
|
|
123
|
+
});
|
|
124
|
+
if (transition.status !== "committed")
|
|
125
|
+
return { status: "rejected" };
|
|
126
|
+
settled = true;
|
|
127
|
+
runnerLock.close();
|
|
128
|
+
runnerLock = undefined;
|
|
129
|
+
void input.wakeAfterInterrupt?.().catch(() => {
|
|
130
|
+
// The ordinary wake path persists its typed failure. A later tell can retry.
|
|
131
|
+
});
|
|
132
|
+
return { status: "completed" };
|
|
133
|
+
};
|
|
134
|
+
const settleRunnerOutcome = (execution) => {
|
|
135
|
+
const transition = store.runnerVerdictIfOpen({
|
|
136
|
+
executionId: input.executionId,
|
|
137
|
+
verdict: execution.verdict,
|
|
138
|
+
facts: execution.facts,
|
|
139
|
+
terminalAt: new Date(now()).toISOString(),
|
|
140
|
+
});
|
|
141
|
+
if (transition.status !== "committed")
|
|
142
|
+
return { status: "rejected" };
|
|
143
|
+
settled = true;
|
|
144
|
+
if (transition.records[0]?.facts.state === "interrupted") {
|
|
145
|
+
runnerLock.close();
|
|
146
|
+
runnerLock = undefined;
|
|
147
|
+
void input.wakeAfterInterrupt?.().catch(() => {
|
|
148
|
+
// The ordinary wake path persists its typed failure. A later observer retries.
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return { status: "completed" };
|
|
152
|
+
};
|
|
116
153
|
let gracefulAbortInvoked = false;
|
|
117
154
|
let forceAbortInvoked = false;
|
|
118
155
|
let sawKillIntent = false;
|
|
119
|
-
|
|
156
|
+
let sawInterruptIntent = false;
|
|
157
|
+
const observeStopBoundary = () => {
|
|
120
158
|
try {
|
|
121
159
|
if (!tellObserver.hasDataVersionChanged())
|
|
122
160
|
return;
|
|
@@ -125,9 +163,10 @@ export async function runProjectionGeneration(input) {
|
|
|
125
163
|
return;
|
|
126
164
|
if (live.verdict)
|
|
127
165
|
return;
|
|
128
|
-
|
|
166
|
+
sawKillIntent = live.killIntent !== undefined;
|
|
167
|
+
sawInterruptIntent = live.interruptIntent !== undefined;
|
|
168
|
+
if (!sawKillIntent && !sawInterruptIntent)
|
|
129
169
|
return;
|
|
130
|
-
sawKillIntent = true;
|
|
131
170
|
if (gracefulAbortInvoked)
|
|
132
171
|
return;
|
|
133
172
|
gracefulAbortInvoked = true;
|
|
@@ -153,12 +192,12 @@ export async function runProjectionGeneration(input) {
|
|
|
153
192
|
const outcomeWatch = handle.outcome.then((execution) => {
|
|
154
193
|
outcomeBox.status = "fulfilled";
|
|
155
194
|
outcomeBox.execution = execution;
|
|
156
|
-
|
|
195
|
+
observeStopBoundary();
|
|
157
196
|
bumpActivity();
|
|
158
197
|
}, (error) => {
|
|
159
198
|
outcomeBox.status = "rejected";
|
|
160
199
|
outcomeBox.error = error;
|
|
161
|
-
|
|
200
|
+
observeStopBoundary();
|
|
162
201
|
bumpActivity();
|
|
163
202
|
});
|
|
164
203
|
// Always rejection-observe so abandon paths never surface unhandled rejections.
|
|
@@ -168,7 +207,7 @@ export async function runProjectionGeneration(input) {
|
|
|
168
207
|
for await (const _event of handle.events) {
|
|
169
208
|
if (forceAbortInvoked)
|
|
170
209
|
return;
|
|
171
|
-
|
|
210
|
+
observeStopBoundary();
|
|
172
211
|
bumpActivity();
|
|
173
212
|
}
|
|
174
213
|
}
|
|
@@ -178,10 +217,17 @@ export async function runProjectionGeneration(input) {
|
|
|
178
217
|
bumpActivity();
|
|
179
218
|
})();
|
|
180
219
|
observeAbandoned(eventDrain);
|
|
181
|
-
|
|
220
|
+
// A provider may be silently executing when an operator interrupts it.
|
|
221
|
+
// The durable observer is the sole stop-intent authority in that interval.
|
|
222
|
+
intentTimer = setInterval(() => {
|
|
223
|
+
observeStopBoundary();
|
|
224
|
+
bumpActivity();
|
|
225
|
+
}, 100);
|
|
226
|
+
intentTimer.unref?.();
|
|
227
|
+
while (outcomeBox.status === "pending" && !sawKillIntent && !sawInterruptIntent) {
|
|
182
228
|
await activityChanged();
|
|
183
229
|
}
|
|
184
|
-
if (sawKillIntent) {
|
|
230
|
+
if (sawKillIntent || sawInterruptIntent) {
|
|
185
231
|
// Grace race: outcome settlement inside the window is stop proof.
|
|
186
232
|
if (outcomeBox.status === "pending") {
|
|
187
233
|
const graceExpired = new Promise((resolve) => {
|
|
@@ -201,6 +247,16 @@ export async function runProjectionGeneration(input) {
|
|
|
201
247
|
const facts = killOutcome.status === "fulfilled"
|
|
202
248
|
? nonReservedRunnerKillFacts(killOutcome.execution.facts)
|
|
203
249
|
: undefined;
|
|
250
|
+
if (sawInterruptIntent && killOutcome.status === "fulfilled" && killOutcome.execution.verdict === "interrupted") {
|
|
251
|
+
return settleInterrupted(facts);
|
|
252
|
+
}
|
|
253
|
+
if (sawInterruptIntent) {
|
|
254
|
+
if (killOutcome.status === "fulfilled")
|
|
255
|
+
return settleRunnerOutcome(killOutcome.execution);
|
|
256
|
+
if (killOutcome.status === "rejected")
|
|
257
|
+
throw killOutcome.error;
|
|
258
|
+
throw new Error("interrupt outcome did not establish provider stop");
|
|
259
|
+
}
|
|
204
260
|
const killed = store.runnerKilledIfRequested({
|
|
205
261
|
executionId: input.executionId,
|
|
206
262
|
...(facts && Object.keys(facts).length > 0 ? { facts } : {}),
|
|
@@ -214,6 +270,8 @@ export async function runProjectionGeneration(input) {
|
|
|
214
270
|
forceAbortInvoked = true;
|
|
215
271
|
try {
|
|
216
272
|
await handle.abort("force");
|
|
273
|
+
if (sawInterruptIntent)
|
|
274
|
+
return settleInterrupted();
|
|
217
275
|
const killed = store.runnerKilledIfRequested({ executionId: input.executionId });
|
|
218
276
|
if (killed.status !== "committed")
|
|
219
277
|
return { status: "rejected" };
|
|
@@ -225,7 +283,7 @@ export async function runProjectionGeneration(input) {
|
|
|
225
283
|
executionId: input.executionId,
|
|
226
284
|
verdict: "failed",
|
|
227
285
|
facts: {
|
|
228
|
-
operatorAction: "kill",
|
|
286
|
+
...(sawKillIntent ? { operatorAction: "kill" } : {}),
|
|
229
287
|
diagnostic: "escalation-failed",
|
|
230
288
|
detail: errorDetail(error),
|
|
231
289
|
failedAt: new Date(now()).toISOString(),
|
|
@@ -268,53 +326,36 @@ export async function runProjectionGeneration(input) {
|
|
|
268
326
|
successorFacts: successorLaunchFacts(current.launch, execution.facts, pending.effort, now),
|
|
269
327
|
tellFence,
|
|
270
328
|
});
|
|
271
|
-
if (transition.status !== "committed")
|
|
272
|
-
|
|
273
|
-
settled = true;
|
|
274
|
-
// Handoff makes the predecessor terminal before life authority moves.
|
|
275
|
-
// A held projection lock must always mean that a current runner exists,
|
|
276
|
-
// so release it before the successor makes its one-shot acquisition.
|
|
277
|
-
runnerLock.close();
|
|
278
|
-
runnerLock = undefined;
|
|
279
|
-
try {
|
|
280
|
-
input.spawnSuccessor?.(successorExecutionId);
|
|
281
|
-
}
|
|
282
|
-
catch (error) {
|
|
283
|
-
store.verdictIfOpen({
|
|
284
|
-
executionId: successorExecutionId,
|
|
285
|
-
verdict: "launch-failed",
|
|
286
|
-
facts: {
|
|
287
|
-
stage: "spawn",
|
|
288
|
-
detail: `successor runner spawn failed: ${errorDetail(error)}`,
|
|
289
|
-
terminalAt: new Date(now()).toISOString(),
|
|
290
|
-
},
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
return { status: "handed-off", successorExecutionId };
|
|
294
|
-
}
|
|
295
|
-
const verdict = store.verdictIfOpen({
|
|
296
|
-
executionId: input.executionId,
|
|
297
|
-
verdict: execution.verdict,
|
|
298
|
-
facts: execution.facts,
|
|
299
|
-
});
|
|
300
|
-
if (verdict.status !== "committed") {
|
|
301
|
-
// Kill intent may commit after the last outcome-boundary observation and
|
|
302
|
-
// before this transaction. The settled outcome is already stop proof.
|
|
303
|
-
if (verdict.reason === "kill-requested") {
|
|
304
|
-
const facts = nonReservedRunnerKillFacts(execution.facts);
|
|
305
|
-
const killed = store.runnerKilledIfRequested({
|
|
306
|
-
executionId: input.executionId,
|
|
307
|
-
...(Object.keys(facts).length > 0 ? { facts } : {}),
|
|
308
|
-
});
|
|
309
|
-
if (killed.status !== "committed")
|
|
329
|
+
if (transition.status !== "committed") {
|
|
330
|
+
if (transition.reason !== "kill-requested" && transition.reason !== "interrupt-requested") {
|
|
310
331
|
return { status: "rejected" };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
311
335
|
settled = true;
|
|
312
|
-
|
|
336
|
+
// Handoff makes the predecessor terminal before life authority moves.
|
|
337
|
+
// A held projection lock must always mean that a current runner exists,
|
|
338
|
+
// so release it before the successor makes its one-shot acquisition.
|
|
339
|
+
runnerLock.close();
|
|
340
|
+
runnerLock = undefined;
|
|
341
|
+
try {
|
|
342
|
+
input.spawnSuccessor?.(successorExecutionId);
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
store.verdictIfOpen({
|
|
346
|
+
executionId: successorExecutionId,
|
|
347
|
+
verdict: "launch-failed",
|
|
348
|
+
facts: {
|
|
349
|
+
stage: "spawn",
|
|
350
|
+
detail: `successor runner spawn failed: ${errorDetail(error)}`,
|
|
351
|
+
terminalAt: new Date(now()).toISOString(),
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
return { status: "handed-off", successorExecutionId };
|
|
313
356
|
}
|
|
314
|
-
return { status: "rejected" };
|
|
315
357
|
}
|
|
316
|
-
|
|
317
|
-
return { status: "completed" };
|
|
358
|
+
return settleRunnerOutcome(execution);
|
|
318
359
|
}
|
|
319
360
|
catch (error) {
|
|
320
361
|
if (adopted && !settled) {
|
|
@@ -338,6 +379,8 @@ export async function runProjectionGeneration(input) {
|
|
|
338
379
|
}
|
|
339
380
|
finally {
|
|
340
381
|
clearGraceTimer();
|
|
382
|
+
if (intentTimer !== undefined)
|
|
383
|
+
clearInterval(intentTimer);
|
|
341
384
|
try {
|
|
342
385
|
tellObserver?.close();
|
|
343
386
|
}
|
|
@@ -2,6 +2,24 @@ import { executeProjectionGeneration } from "./projection-generation-execution.j
|
|
|
2
2
|
import { spawnProjectionGenerationRunner } from "./projection-generation-process.js";
|
|
3
3
|
import { runProjectionGeneration } from "./projection-generation-runner.js";
|
|
4
4
|
import { snapshotPendingTells } from "./projection-generation-continuation.js";
|
|
5
|
+
import { recordInterruptWakeFailure, tellProjection } from "../projection-wake.js";
|
|
6
|
+
import { readProjectionIdentity } from "../projection-identity.js";
|
|
7
|
+
import { FlowError } from "../../../flow-error.js";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
export async function wakeProjectionAfterInterrupt(projectionDirectory, wake = tellProjection) {
|
|
10
|
+
const pact = readProjectionIdentity(projectionDirectory);
|
|
11
|
+
try {
|
|
12
|
+
await wake(projectionDirectory, `${pact.akuma}/${path.basename(projectionDirectory)}`, undefined);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
const cause = error instanceof FlowError
|
|
16
|
+
&& error.facts?.kind === "projection_wake_failure"
|
|
17
|
+
? error.facts.cause
|
|
18
|
+
: "spawn";
|
|
19
|
+
recordInterruptWakeFailure(projectionDirectory, cause);
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
5
23
|
export async function runProjectionGenerationRuntime(projectionDirectory, executionId) {
|
|
6
24
|
await runProjectionGeneration({
|
|
7
25
|
projectionDirectory,
|
|
@@ -11,5 +29,6 @@ export async function runProjectionGenerationRuntime(projectionDirectory, execut
|
|
|
11
29
|
spawnSuccessor: (successorExecutionId) => {
|
|
12
30
|
spawnProjectionGenerationRunner(projectionDirectory, successorExecutionId);
|
|
13
31
|
},
|
|
32
|
+
wakeAfterInterrupt: () => wakeProjectionAfterInterrupt(projectionDirectory),
|
|
14
33
|
});
|
|
15
34
|
}
|
|
@@ -46,9 +46,18 @@ export class ProjectionGenerationStore {
|
|
|
46
46
|
requestKillIfOpen(input) {
|
|
47
47
|
return transitions.requestKillIfOpen(this.#database, this.readOnly, input);
|
|
48
48
|
}
|
|
49
|
+
requestInterruptIfOpen(input) {
|
|
50
|
+
return transitions.requestInterruptIfOpen(this.#database, this.readOnly, input);
|
|
51
|
+
}
|
|
49
52
|
runnerKilledIfRequested(input) {
|
|
50
53
|
return transitions.runnerKilledIfRequested(this.#database, this.readOnly, input);
|
|
51
54
|
}
|
|
55
|
+
runnerInterruptedIfRequested(input) {
|
|
56
|
+
return transitions.runnerInterruptedIfRequested(this.#database, this.readOnly, input);
|
|
57
|
+
}
|
|
58
|
+
runnerVerdictIfOpen(input) {
|
|
59
|
+
return transitions.runnerVerdictIfOpen(this.#database, this.readOnly, input);
|
|
60
|
+
}
|
|
52
61
|
settleKillIfRunnerDead(input) {
|
|
53
62
|
return transitions.settleKillIfRunnerDead(this.#database, this.readOnly, input);
|
|
54
63
|
}
|