@astrosheep/keiyaku 2.9.7 → 2.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/.tsbuildinfo +1 -1
- package/build/agents/harness/event-persistence.js +7 -5
- package/build/agents/harness/events.js +3 -2
- package/build/agents/providers/codex-app-server/adapter.js +6 -1
- package/build/agents/providers/codex-app-server/session.js +8 -7
- package/build/agents/selector.js +12 -1
- package/build/cli/commands/akuma/view/handler.js +3 -11
- package/build/cli/commands/contract/amend/handler.js +1 -1
- package/build/cli/commands/contract/amend/meta.js +4 -4
- package/build/cli/commands/projection/status/handler.js +5 -4
- package/build/cli/commands/projection/status/meta.js +2 -2
- package/build/cli/commands/task/add/meta.js +9 -1
- package/build/cli/commands/task/shared.js +2 -1
- package/build/cli/completion.js +8 -0
- package/build/cli/render/kanshi.js +2 -2
- package/build/cli/render/path-prefix-compaction.js +119 -76
- package/build/cli/render/projection-activity.js +10 -1
- package/build/cli/render/shared.js +8 -7
- package/build/cli/render/status.js +8 -5
- package/build/cli/render/wait.js +1 -1
- package/build/config/settings/disease.js +4 -4
- package/build/config/settings/loader.js +44 -21
- package/build/core/addressing.js +40 -9
- package/build/core/amend.js +21 -5
- package/build/core/call/context.js +19 -3
- package/build/core/call/execution.js +43 -20
- package/build/core/ledger-batch.js +194 -0
- package/build/core/projection/generation/database.js +22 -0
- package/build/core/projection/generation/projection-generation-execution.js +45 -37
- package/build/core/projection/generation/projection-generation-launcher.js +148 -20
- package/build/core/projection/generation/projection-generation-process.js +3 -1
- package/build/core/projection/generation/projection-generation-runner.js +76 -40
- package/build/core/projection/generation/projection-generation-runtime.js +82 -19
- package/build/core/projection/generation/store.js +17 -1
- package/build/core/projection/generation/transitions.js +89 -12
- package/build/core/projection/index.js +3 -3
- package/build/core/projection/projection-kill.js +22 -10
- package/build/core/projection/projection-runner-lock.js +177 -37
- package/build/core/projection/projection-status.js +143 -55
- package/build/core/projection/projection-wake.js +171 -72
- package/build/core/status/board.js +42 -4
- package/build/core/status/drift.js +21 -5
- package/build/core/status/ledger-batch.js +1 -158
- package/build/core/task/task-git-runtime.js +8 -10
- package/build/core/task/task-git-store.js +5 -3
- package/build/core/worktree-path.js +39 -25
- package/build/flow-error.js +1 -1
- package/build/generated/version.js +2 -2
- package/build/git/refs.js +47 -1
- package/package.json +1 -1
- package/skills/keiyaku-akuma/SKILL.md +18 -0
- package/skills/keiyaku-workflow/SKILL.md +68 -13
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { wrapGitError, runGitProcess } from "../git/core.js";
|
|
2
|
+
import { GitStreamingBatchFramingError, GitStreamingBatchResponseEndedError, openGitStreamingBatch, } from "../git/streaming-batch.js";
|
|
3
|
+
import { parseLedgerEntryCommitMessage } from "./entry.js";
|
|
4
|
+
import { ACTIVE_LEDGER_REF_PREFIX, activeLedgerRef, } from "./ledger.js";
|
|
5
|
+
const MAX_LEDGER_COMMIT_BYTES = 1024 * 1024;
|
|
6
|
+
async function runLedgerGit(cwd, args) {
|
|
7
|
+
const result = await runGitProcess(cwd, args);
|
|
8
|
+
if (result.error || result.status !== 0) {
|
|
9
|
+
throw wrapGitError(args.join(" "), {
|
|
10
|
+
message: result.error?.message
|
|
11
|
+
?? `git exited with status ${result.status ?? "unknown"}`,
|
|
12
|
+
stderr: result.stderr,
|
|
13
|
+
stdout: result.stdout,
|
|
14
|
+
}, cwd);
|
|
15
|
+
}
|
|
16
|
+
return Buffer.from(result.stdout, "utf8");
|
|
17
|
+
}
|
|
18
|
+
async function queryBatchObject(batch, expression) {
|
|
19
|
+
const record = await batch.request(expression, {
|
|
20
|
+
maxContentBytes: MAX_LEDGER_COMMIT_BYTES,
|
|
21
|
+
});
|
|
22
|
+
if (record.kind === "missing")
|
|
23
|
+
return null;
|
|
24
|
+
if (record.kind === "ambiguous") {
|
|
25
|
+
throw new Error(`[git cat-file ${expression}] object is ambiguous`);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
oid: record.record.oid,
|
|
29
|
+
type: record.record.type,
|
|
30
|
+
content: record.record.content,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function parseCommitObject(object) {
|
|
34
|
+
if (object.type !== "commit") {
|
|
35
|
+
throw new Error(`git cat-file expected commit ${object.oid}, received ${object.type}`);
|
|
36
|
+
}
|
|
37
|
+
const content = object.content.toString("utf8");
|
|
38
|
+
const messageStart = content.indexOf("\n\n");
|
|
39
|
+
if (messageStart < 0)
|
|
40
|
+
throw new Error(`git commit ${object.oid} has no message separator`);
|
|
41
|
+
const parents = [];
|
|
42
|
+
for (const line of content.slice(0, messageStart).split("\n")) {
|
|
43
|
+
if (!line.startsWith("parent "))
|
|
44
|
+
continue;
|
|
45
|
+
const parent = line.slice("parent ".length);
|
|
46
|
+
if (!/^[0-9a-f]{40,64}$/.test(parent)) {
|
|
47
|
+
throw new Error(`git commit ${object.oid} has an invalid parent header`);
|
|
48
|
+
}
|
|
49
|
+
parents.push(parent);
|
|
50
|
+
}
|
|
51
|
+
return { oid: object.oid, parents, message: content.slice(messageStart + 2) };
|
|
52
|
+
}
|
|
53
|
+
function parseLedgerRefs(output) {
|
|
54
|
+
return output
|
|
55
|
+
.toString("utf8")
|
|
56
|
+
.split(/\r?\n/)
|
|
57
|
+
.filter((ref) => ref.startsWith(ACTIVE_LEDGER_REF_PREFIX))
|
|
58
|
+
.map((ref) => ({ contractId: ref.slice(ACTIVE_LEDGER_REF_PREFIX.length), ref }));
|
|
59
|
+
}
|
|
60
|
+
async function readLedgerFromBatch(batch, ref, commits) {
|
|
61
|
+
const headObject = await queryBatchObject(batch, `${ref.ref}^{commit}`);
|
|
62
|
+
if (!headObject)
|
|
63
|
+
return null;
|
|
64
|
+
const head = parseCommitObject(headObject);
|
|
65
|
+
commits.set(head.oid, Promise.resolve(head));
|
|
66
|
+
const ordered = [];
|
|
67
|
+
const visited = new Set();
|
|
68
|
+
const visiting = new Set();
|
|
69
|
+
const visit = async (oid) => {
|
|
70
|
+
if (visited.has(oid))
|
|
71
|
+
return;
|
|
72
|
+
if (visiting.has(oid))
|
|
73
|
+
throw new Error(`git ledger ancestry contains a cycle at ${oid}`);
|
|
74
|
+
visiting.add(oid);
|
|
75
|
+
let pending = commits.get(oid);
|
|
76
|
+
if (!pending) {
|
|
77
|
+
pending = queryBatchObject(batch, oid).then((object) => {
|
|
78
|
+
if (!object)
|
|
79
|
+
throw new Error(`[git cat-file ${oid}] object is missing`);
|
|
80
|
+
return parseCommitObject(object);
|
|
81
|
+
});
|
|
82
|
+
commits.set(oid, pending);
|
|
83
|
+
}
|
|
84
|
+
const commit = await pending;
|
|
85
|
+
for (const parent of commit.parents)
|
|
86
|
+
await visit(parent);
|
|
87
|
+
visiting.delete(oid);
|
|
88
|
+
visited.add(oid);
|
|
89
|
+
ordered.push(commit);
|
|
90
|
+
};
|
|
91
|
+
await visit(head.oid);
|
|
92
|
+
return {
|
|
93
|
+
contractId: ref.contractId,
|
|
94
|
+
ref: ref.ref,
|
|
95
|
+
head: head.oid,
|
|
96
|
+
entries: ordered.map((commit) => parseLedgerEntryCommitMessage(commit.message)),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async function closeBatch(cwd, batch) {
|
|
100
|
+
const close = await batch.close();
|
|
101
|
+
if (!close.aborted && (close.timedOut || close.signal || close.code !== 0)) {
|
|
102
|
+
throw wrapGitError("cat-file --batch", {
|
|
103
|
+
message: close.timedOut
|
|
104
|
+
? "git timed out"
|
|
105
|
+
: close.signal
|
|
106
|
+
? `git exited from signal ${close.signal}`
|
|
107
|
+
: `git exited with status ${close.code ?? "unknown"}`,
|
|
108
|
+
stderr: close.stderr.toString("utf8"),
|
|
109
|
+
}, cwd);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function readLedgerRefs(cwd, refs) {
|
|
113
|
+
if (refs.length === 0)
|
|
114
|
+
return [];
|
|
115
|
+
const batch = openGitStreamingBatch(cwd);
|
|
116
|
+
const commits = new Map();
|
|
117
|
+
const results = [];
|
|
118
|
+
let readError;
|
|
119
|
+
try {
|
|
120
|
+
for (let index = 0; index < refs.length; index += 1) {
|
|
121
|
+
const ref = refs[index];
|
|
122
|
+
try {
|
|
123
|
+
const ledger = await readLedgerFromBatch(batch, ref, commits);
|
|
124
|
+
if (ledger)
|
|
125
|
+
results.push(ledger);
|
|
126
|
+
else
|
|
127
|
+
results.push({ id: ref.contractId, error: new Error(`ledger ref disappeared: ${ref.ref}`) });
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const diagnostic = error instanceof Error ? error : new Error(String(error));
|
|
131
|
+
results.push({ id: ref.contractId, error: diagnostic });
|
|
132
|
+
if (error instanceof GitStreamingBatchFramingError
|
|
133
|
+
|| error instanceof GitStreamingBatchResponseEndedError) {
|
|
134
|
+
const isolationReason = error instanceof GitStreamingBatchResponseEndedError
|
|
135
|
+
? "response ended"
|
|
136
|
+
: "malformed frame";
|
|
137
|
+
for (const pending of refs.slice(index + 1)) {
|
|
138
|
+
results.push({
|
|
139
|
+
id: pending.contractId,
|
|
140
|
+
error: new Error(`git cat-file batch terminated after ${isolationReason}: ${diagnostic.message}`),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
readError = error;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
await closeBatch(cwd, batch);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (!readError)
|
|
156
|
+
readError = error;
|
|
157
|
+
}
|
|
158
|
+
if (readError)
|
|
159
|
+
throw readError;
|
|
160
|
+
return results;
|
|
161
|
+
}
|
|
162
|
+
/** Read every ledger with two Git processes regardless of ledger or entry count. */
|
|
163
|
+
export async function readAllLedgerSnapshots(cwd) {
|
|
164
|
+
const refOutput = await runLedgerGit(cwd, [
|
|
165
|
+
"for-each-ref",
|
|
166
|
+
"--sort=refname",
|
|
167
|
+
"--format=%(refname)",
|
|
168
|
+
ACTIVE_LEDGER_REF_PREFIX,
|
|
169
|
+
]);
|
|
170
|
+
return readLedgerRefs(cwd, parseLedgerRefs(refOutput));
|
|
171
|
+
}
|
|
172
|
+
/** Read one exact ledger with one Git process and without enumerating other refs. */
|
|
173
|
+
export async function readLedgerSnapshot(cwd, contractId) {
|
|
174
|
+
const ref = activeLedgerRef(contractId);
|
|
175
|
+
const batch = openGitStreamingBatch(cwd);
|
|
176
|
+
let ledger = null;
|
|
177
|
+
let readError;
|
|
178
|
+
try {
|
|
179
|
+
ledger = await readLedgerFromBatch(batch, { contractId, ref }, new Map());
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
readError = error;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
await closeBatch(cwd, batch);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
if (!readError)
|
|
189
|
+
readError = error;
|
|
190
|
+
}
|
|
191
|
+
if (readError)
|
|
192
|
+
throw readError;
|
|
193
|
+
return ledger;
|
|
194
|
+
}
|
|
@@ -295,6 +295,8 @@ export function writeTransition(database, readOnly, run) {
|
|
|
295
295
|
if (readOnly) {
|
|
296
296
|
throw new ProjectionGenerationStoreError("projection generation store is read-only");
|
|
297
297
|
}
|
|
298
|
+
if (database.isTransaction)
|
|
299
|
+
return run();
|
|
298
300
|
database.exec("BEGIN IMMEDIATE");
|
|
299
301
|
try {
|
|
300
302
|
const result = run();
|
|
@@ -306,6 +308,26 @@ export function writeTransition(database, readOnly, run) {
|
|
|
306
308
|
throw error;
|
|
307
309
|
}
|
|
308
310
|
}
|
|
311
|
+
export function beginGenerationWriteTransaction(database, readOnly) {
|
|
312
|
+
assertDatabaseOpen(database);
|
|
313
|
+
if (readOnly)
|
|
314
|
+
throw new ProjectionGenerationStoreError("projection generation store is read-only");
|
|
315
|
+
if (database.isTransaction)
|
|
316
|
+
throw new ProjectionGenerationStoreError("projection generation write transaction is already open");
|
|
317
|
+
database.exec("BEGIN IMMEDIATE");
|
|
318
|
+
}
|
|
319
|
+
export function commitGenerationWriteTransaction(database) {
|
|
320
|
+
assertDatabaseOpen(database);
|
|
321
|
+
if (!database.isTransaction)
|
|
322
|
+
throw new ProjectionGenerationStoreError("projection generation write transaction is not open");
|
|
323
|
+
database.exec("COMMIT");
|
|
324
|
+
}
|
|
325
|
+
export function rollbackGenerationWriteTransaction(database) {
|
|
326
|
+
assertDatabaseOpen(database);
|
|
327
|
+
if (!database.isTransaction)
|
|
328
|
+
return;
|
|
329
|
+
rollbackQuietly(database);
|
|
330
|
+
}
|
|
309
331
|
export function closeDatabase(database) {
|
|
310
332
|
if (closedDatabases.has(database))
|
|
311
333
|
return;
|
|
@@ -136,7 +136,7 @@ async function completionFacts(projectionDirectory, executionId, inputs, outcome
|
|
|
136
136
|
* Construct the public Projection once and expose a provider-neutral execution
|
|
137
137
|
* handle. Provider adapters and harness public types are unchanged.
|
|
138
138
|
*/
|
|
139
|
-
export function
|
|
139
|
+
export function prepareProjectionGenerationExecution(projectionDirectory, launch, providerLoaderPorts = {}) {
|
|
140
140
|
const inputs = parseLaunchInputs(launch);
|
|
141
141
|
initializeConfig(process.env, inputs.cwd);
|
|
142
142
|
const executionId = launch.executionId;
|
|
@@ -160,43 +160,51 @@ export function executeProjectionGeneration(projectionDirectory, launch, provide
|
|
|
160
160
|
executionId,
|
|
161
161
|
initialTellIds,
|
|
162
162
|
});
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
163
|
+
return {
|
|
164
|
+
start() {
|
|
165
|
+
let projection;
|
|
166
|
+
try {
|
|
167
|
+
projection = inputs.session
|
|
168
|
+
? revive({
|
|
169
|
+
...terms,
|
|
170
|
+
session: inputs.session,
|
|
171
|
+
resumePath: inputs.resumePath ?? projectionDirectory,
|
|
172
|
+
}, { observer: observerController.observer })
|
|
173
|
+
: call(terms, { observer: observerController.observer });
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
void observerController.close();
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
const outcome = projection.outcome.then(async (result) => {
|
|
180
|
+
try {
|
|
181
|
+
return {
|
|
182
|
+
verdict: result.status === "completed"
|
|
183
|
+
? "completed"
|
|
184
|
+
: result.status === "dismissed"
|
|
185
|
+
? "dismissed"
|
|
186
|
+
: result.status === "interrupted"
|
|
187
|
+
? "interrupted"
|
|
188
|
+
: "failed",
|
|
189
|
+
facts: await completionFacts(projectionDirectory, executionId, inputs, result),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
await observerController.close();
|
|
194
|
+
}
|
|
195
|
+
}, async (error) => {
|
|
196
|
+
await observerController.close();
|
|
197
|
+
throw error;
|
|
198
|
+
});
|
|
179
199
|
return {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
? "dismissed"
|
|
184
|
-
: result.status === "interrupted"
|
|
185
|
-
? "interrupted"
|
|
186
|
-
: "failed",
|
|
187
|
-
facts: await completionFacts(projectionDirectory, executionId, inputs, result),
|
|
200
|
+
events: projection.events,
|
|
201
|
+
outcome,
|
|
202
|
+
abort: (mode) => projection.abort(mode),
|
|
188
203
|
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
await observerController.close();
|
|
192
|
-
}
|
|
193
|
-
}, async (error) => {
|
|
194
|
-
await observerController.close();
|
|
195
|
-
throw error;
|
|
196
|
-
});
|
|
197
|
-
return {
|
|
198
|
-
events: projection.events,
|
|
199
|
-
outcome,
|
|
200
|
-
abort: (mode) => projection.abort(mode),
|
|
204
|
+
},
|
|
205
|
+
dispose: () => observerController.close(),
|
|
201
206
|
};
|
|
202
207
|
}
|
|
208
|
+
export function executeProjectionGeneration(projectionDirectory, launch, providerLoaderPorts = {}) {
|
|
209
|
+
return prepareProjectionGenerationExecution(projectionDirectory, launch, providerLoaderPorts).start();
|
|
210
|
+
}
|
|
@@ -1,37 +1,179 @@
|
|
|
1
1
|
import { setTimeout as delay } from "node:timers/promises";
|
|
2
2
|
import { openProjectionGenerationStore } from "./store.js";
|
|
3
3
|
import { spawnProjectionGenerationRunner, } from "./projection-generation-process.js";
|
|
4
|
+
import { acquireProjectionRunnerLock, observeProjectionRunnerLock, } from "../projection-runner-lock.js";
|
|
4
5
|
export const PROJECTION_ADOPTION_TIMEOUT_MS = 4_000;
|
|
5
6
|
export const PROJECTION_ADOPTION_POLL_MS = 20;
|
|
6
7
|
function errorDetail(error) {
|
|
7
8
|
return error instanceof Error ? error.message : String(error);
|
|
8
9
|
}
|
|
9
|
-
function
|
|
10
|
+
function recordDeadStartup(projectionDirectory, executionId, detail, stage, terminalAt) {
|
|
10
11
|
const store = openProjectionGenerationStore(projectionDirectory);
|
|
11
12
|
try {
|
|
12
|
-
return store.
|
|
13
|
+
return store.settleStartupFailure({
|
|
13
14
|
executionId,
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
releasedLockEvidence: { stage, detail },
|
|
16
|
+
diagnostic: { stage, detail },
|
|
17
|
+
terminalAt,
|
|
16
18
|
}).status === "committed";
|
|
17
19
|
}
|
|
18
20
|
finally {
|
|
19
21
|
store.close();
|
|
20
22
|
}
|
|
21
23
|
}
|
|
24
|
+
function observeBootstrapMessages(child, executionId) {
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
const waiters = new Map();
|
|
27
|
+
let failure;
|
|
28
|
+
const onMessage = (value) => {
|
|
29
|
+
const message = value;
|
|
30
|
+
if (message.executionId !== executionId)
|
|
31
|
+
return;
|
|
32
|
+
if (message.kind !== "leash-inherited" && message.kind !== "leash-acquired")
|
|
33
|
+
return;
|
|
34
|
+
seen.add(message.kind);
|
|
35
|
+
const waiter = waiters.get(message.kind);
|
|
36
|
+
if (waiter) {
|
|
37
|
+
waiter.resolve();
|
|
38
|
+
waiters.delete(message.kind);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const onError = (error) => {
|
|
42
|
+
failure = error;
|
|
43
|
+
for (const waiter of waiters.values())
|
|
44
|
+
waiter.reject(error);
|
|
45
|
+
waiters.clear();
|
|
46
|
+
};
|
|
47
|
+
const onExit = () => onError(new Error("runner exited during bootstrap"));
|
|
48
|
+
child.on("message", onMessage);
|
|
49
|
+
child.on("error", onError);
|
|
50
|
+
child.on("exit", onExit);
|
|
51
|
+
return {
|
|
52
|
+
waitFor(kind) {
|
|
53
|
+
if (seen.has(kind))
|
|
54
|
+
return Promise.resolve();
|
|
55
|
+
if (failure)
|
|
56
|
+
return Promise.reject(failure);
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const timer = setTimeout(() => {
|
|
59
|
+
waiters.delete(kind);
|
|
60
|
+
reject(failure ?? new Error(`runner did not report ${kind} before startup timeout`));
|
|
61
|
+
}, PROJECTION_ADOPTION_TIMEOUT_MS);
|
|
62
|
+
waiters.set(kind, {
|
|
63
|
+
resolve: () => {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
waiters.delete(kind);
|
|
66
|
+
resolve();
|
|
67
|
+
},
|
|
68
|
+
reject: (error) => {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
waiters.delete(kind);
|
|
71
|
+
reject(error);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
close() {
|
|
77
|
+
child.off("message", onMessage);
|
|
78
|
+
child.off("error", onError);
|
|
79
|
+
child.off("exit", onExit);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function sendBootstrapCommand(child, command) {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
if (!child.send || !child.connected) {
|
|
86
|
+
reject(new Error("runner bootstrap channel is unavailable"));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
child.send(command, (error) => error ? reject(error) : resolve());
|
|
90
|
+
});
|
|
91
|
+
}
|
|
22
92
|
/** Spawn one committed launch and return only after its exact adoption row. */
|
|
23
93
|
export async function startProjectionGeneration(projectionDirectory, executionId, dependencies = {}) {
|
|
24
94
|
const now = dependencies.now ?? Date.now;
|
|
25
95
|
const sleep = dependencies.sleep ?? ((milliseconds) => delay(milliseconds));
|
|
96
|
+
const launcherAcquiredOwnership = dependencies.launchOwnership === undefined;
|
|
97
|
+
let launchOwnership;
|
|
98
|
+
try {
|
|
99
|
+
launchOwnership = dependencies.launchOwnership
|
|
100
|
+
?? acquireProjectionRunnerLock(projectionDirectory);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (observeProjectionRunnerLock(projectionDirectory).state === "held") {
|
|
104
|
+
return { status: "pending" };
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
status: "failed",
|
|
108
|
+
cause: "adoption",
|
|
109
|
+
detail: `runner construction failed: ${errorDetail(error)}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
let launchCommitted = false;
|
|
113
|
+
let childPromoted = false;
|
|
26
114
|
let child;
|
|
27
115
|
try {
|
|
28
116
|
child = spawnProjectionGenerationRunner(projectionDirectory, executionId, dependencies);
|
|
29
117
|
}
|
|
30
118
|
catch (error) {
|
|
31
119
|
const detail = `runner spawn failed: ${errorDetail(error)}`;
|
|
32
|
-
|
|
120
|
+
recordDeadStartup(projectionDirectory, executionId, detail, "spawn", new Date(now()).toISOString());
|
|
121
|
+
launchOwnership.close();
|
|
33
122
|
return { status: "failed", cause: "spawn", detail };
|
|
34
123
|
}
|
|
124
|
+
const bootstrap = observeBootstrapMessages(child, executionId);
|
|
125
|
+
try {
|
|
126
|
+
await bootstrap.waitFor("leash-inherited");
|
|
127
|
+
if (dependencies.commitLaunch && !dependencies.commitLaunch()) {
|
|
128
|
+
await sendBootstrapCommand(child, { kind: "launch-rejected", executionId });
|
|
129
|
+
if (launcherAcquiredOwnership)
|
|
130
|
+
launchOwnership.close();
|
|
131
|
+
return { status: "failed", cause: "adoption", detail: "generation launch transaction was rejected" };
|
|
132
|
+
}
|
|
133
|
+
launchCommitted = true;
|
|
134
|
+
launchOwnership.releaseReservationForTransfer();
|
|
135
|
+
await sendBootstrapCommand(child, { kind: "promote-leash", executionId });
|
|
136
|
+
await bootstrap.waitFor("leash-acquired");
|
|
137
|
+
childPromoted = true;
|
|
138
|
+
await sendBootstrapCommand(child, { kind: "launch-committed", executionId });
|
|
139
|
+
try {
|
|
140
|
+
launchOwnership.close();
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// The promoted child owns lifecycle truth; parent guard cleanup is diagnostic only.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
void sendBootstrapCommand(child, { kind: "launch-rejected", executionId }).catch(() => { });
|
|
148
|
+
if (childPromoted) {
|
|
149
|
+
try {
|
|
150
|
+
launchOwnership.close();
|
|
151
|
+
}
|
|
152
|
+
catch { }
|
|
153
|
+
return { status: "pending", child };
|
|
154
|
+
}
|
|
155
|
+
if (!launchCommitted) {
|
|
156
|
+
launchOwnership.close();
|
|
157
|
+
return { status: "failed", cause: "adoption", detail: `runner construction failed: ${errorDetail(error)}` };
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
launchOwnership.reclaimReservation();
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
launchOwnership.close();
|
|
164
|
+
return { status: "pending", child };
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
recordDeadStartup(projectionDirectory, executionId, `runner construction failed: ${errorDetail(error)}`, "adoption", new Date(now()).toISOString());
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
launchOwnership.close();
|
|
171
|
+
}
|
|
172
|
+
return { status: "failed", cause: "adoption", detail: `runner construction failed: ${errorDetail(error)}` };
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
bootstrap.close();
|
|
176
|
+
}
|
|
35
177
|
const deadline = now() + PROJECTION_ADOPTION_TIMEOUT_MS;
|
|
36
178
|
for (;;) {
|
|
37
179
|
const observedAt = now();
|
|
@@ -74,19 +216,5 @@ export async function startProjectionGeneration(projectionDirectory, executionId
|
|
|
74
216
|
finally {
|
|
75
217
|
store.close();
|
|
76
218
|
}
|
|
77
|
-
|
|
78
|
-
if (!recordLaunchFailure(projectionDirectory, executionId, detail, "adoption", new Date(now()).toISOString())) {
|
|
79
|
-
const raced = openProjectionGenerationStore(projectionDirectory);
|
|
80
|
-
try {
|
|
81
|
-
const current = raced.readCurrentGeneration();
|
|
82
|
-
if (current?.launch.executionId === executionId && current.adoption) {
|
|
83
|
-
return { status: "adopted", child };
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
finally {
|
|
87
|
-
raced.close();
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
child.kill("SIGTERM");
|
|
91
|
-
return { status: "failed", cause: "adoption", detail };
|
|
219
|
+
return { status: "pending", child };
|
|
92
220
|
}
|
|
@@ -17,6 +17,7 @@ export function projectionGenerationRunnerArgv(projectionDirectory, executionId)
|
|
|
17
17
|
executionId,
|
|
18
18
|
];
|
|
19
19
|
}
|
|
20
|
+
export const PROJECTION_RUNNER_BOOTSTRAP_ENV = "KEIYAKU_PROJECTION_RUNNER_BOOTSTRAP";
|
|
20
21
|
export function spawnProjectionGenerationRunner(projectionDirectory, executionId, dependencies = {}) {
|
|
21
22
|
const spawn = dependencies.spawn ?? nodeSpawn;
|
|
22
23
|
const openSync = dependencies.openSync ?? fs.openSync;
|
|
@@ -30,7 +31,8 @@ export function spawnProjectionGenerationRunner(projectionDirectory, executionId
|
|
|
30
31
|
try {
|
|
31
32
|
const child = spawn(command, args, {
|
|
32
33
|
detached: true,
|
|
33
|
-
stdio: ["ignore", logFd, logFd],
|
|
34
|
+
stdio: ["ignore", logFd, logFd, "ipc"],
|
|
35
|
+
env: { ...process.env, [PROJECTION_RUNNER_BOOTSTRAP_ENV]: "1" },
|
|
34
36
|
});
|
|
35
37
|
if (child.pid === undefined) {
|
|
36
38
|
throw new Error("projection runner did not expose a process id");
|