@astrosheep/keiyaku 4.1.2 → 4.1.4
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/src/akuma/akuma-handle.js +2 -2
- package/build/src/akuma/akuma-product.js +2 -2
- package/build/src/akuma/body.js +4 -1
- package/build/src/akuma/heart/index.d.ts +1 -1
- package/build/src/akuma/heart/index.js +1 -1
- package/build/src/akuma/heart/schema.d.ts +1 -0
- package/build/src/akuma/heart/schema.js +9 -0
- package/build/src/akuma/heart/storage.d.ts +1 -0
- package/build/src/akuma/heart/storage.js +18 -1
- package/build/src/akuma/nuke.js +6 -2
- package/build/src/akuma/providers/claude/index.js +101 -89
- package/build/src/akuma/providers/codex-app-server/index.js +9 -6
- package/build/src/akuma/providers/opencode-sdk/index.js +22 -15
- package/build/src/akuma/providers/pi/index.js +45 -43
- package/build/src/akuma/publication.d.ts +2 -1
- package/build/src/akuma/publication.js +72 -11
- package/build/src/akuma/request-execution.d.ts +4 -2
- package/build/src/akuma/request-execution.js +82 -70
- package/build/src/akuma/request-serve.d.ts +2 -1
- package/build/src/akuma/request-serve.js +1 -1
- package/build/src/akuma/turn-drive.d.ts +2 -1
- package/build/src/akuma/turn-drive.js +10 -1
- package/build/src/git/read-observation.js +15 -10
- package/build/src/kanshi/read.js +32 -27
- package/build/src/library/fleet.js +2 -1
- package/build/src/runtime/proc/run.js +23 -16
- package/build/src/runtime/proc/windows-launch.exe +0 -0
- package/package.json +1 -1
|
@@ -220,7 +220,7 @@ export class AkumaHandle {
|
|
|
220
220
|
archetype: source.archetype,
|
|
221
221
|
awaitAsleep: true,
|
|
222
222
|
launch: async (allocated) => {
|
|
223
|
-
|
|
223
|
+
return await spawnAkumaBody({
|
|
224
224
|
paths: allocated.paths,
|
|
225
225
|
seed: {
|
|
226
226
|
id: allocated.id,
|
|
@@ -234,7 +234,7 @@ export class AkumaHandle {
|
|
|
234
234
|
origin: { kind: "fork", parent: this.id, at: input.at },
|
|
235
235
|
},
|
|
236
236
|
birthSession,
|
|
237
|
-
})
|
|
237
|
+
});
|
|
238
238
|
},
|
|
239
239
|
});
|
|
240
240
|
return { kind: "forked", child: child.id };
|
|
@@ -92,7 +92,7 @@ export class Akuma {
|
|
|
92
92
|
const published = await publishAkuma({
|
|
93
93
|
worldPath: this.path,
|
|
94
94
|
archetype: archetype.name,
|
|
95
|
-
launch: async (allocated) =>
|
|
95
|
+
launch: async (allocated) => await spawnAkumaBody({
|
|
96
96
|
paths: allocated.paths,
|
|
97
97
|
seed: {
|
|
98
98
|
id: allocated.id,
|
|
@@ -102,7 +102,7 @@ export class Akuma {
|
|
|
102
102
|
origin: { kind: "direct" },
|
|
103
103
|
},
|
|
104
104
|
initialBody: input.body,
|
|
105
|
-
})
|
|
105
|
+
}),
|
|
106
106
|
});
|
|
107
107
|
return new AkumaHandle(published.id, this.path, {
|
|
108
108
|
cwd,
|
package/build/src/akuma/body.js
CHANGED
|
@@ -84,8 +84,11 @@ export async function bodyProcessInput(launch, bodyModuleUrl = import.meta.url)
|
|
|
84
84
|
throw new Error("Akuma wake has no born soul");
|
|
85
85
|
const source = bodyModuleUrl.endsWith(".ts");
|
|
86
86
|
const entry = fileURLToPath(new URL(source ? "../akuma-body.ts" : "../akuma-body.js", bodyModuleUrl));
|
|
87
|
+
const argv = source
|
|
88
|
+
? [process.execPath, "--import", import.meta.resolve("tsx"), entry, encoded]
|
|
89
|
+
: [process.execPath, entry, encoded];
|
|
87
90
|
return {
|
|
88
|
-
argv
|
|
91
|
+
argv,
|
|
89
92
|
cwd: await launchCwd(launch),
|
|
90
93
|
env: { ...process.env, KEIYAKU_ACTOR_ID: actorId },
|
|
91
94
|
log: launch.paths.log,
|
|
@@ -3,7 +3,7 @@ import type { BodyEnd, BodyFact, ForkPoint, HeartSnapshot, KillFact, SealFact, S
|
|
|
3
3
|
export type { CallFact } from "./facts.js";
|
|
4
4
|
import type { ActivityFact } from "./rows.js";
|
|
5
5
|
import { type ActivityFactSlice } from "./timeline.js";
|
|
6
|
-
export { HeartAbsentError, HeldAkumaLeash, initializeHeart, isHeartAbsent, probeLeash, watchHeart } from "./storage.js";
|
|
6
|
+
export { HeartAbsentError, HeldAkumaLeash, classifyHeartSchema, initializeHeart, isHeartAbsent, probeLeash, watchHeart, } from "./storage.js";
|
|
7
7
|
export { admitRequest, readNonterminalRequests, readRequest, refuseRequest, reserveRequest, serveRequest, serveUpstreamRequest, voidRequest, } from "./request-authority.js";
|
|
8
8
|
export { life, lifeAt } from "./facts.js";
|
|
9
9
|
export type { AkumaLife, AkumaOrigin, BodyEnd, BodyFact, HeartSnapshot, ForkPoint, KillEvidence, KillFact, LeashProbe, PauseFact, RequestFact, RequestInput, RequestRecipe, UpstreamRequestService, SealFact, SessionFact, Soul, StopFact, TellFact, TellDelivery, TellDeliveryInput, TellReceiptInput, TurnEndFact, TurnFact, TurnOutcome, TurnStartFact, } from "./facts.js";
|
|
@@ -3,7 +3,7 @@ import { insertTellDeliveryFact, insertTellFact, insertTellReceiptFact, pendingT
|
|
|
3
3
|
import { activityFactSlice, lastActivityAt as readLastActivityAt, pruneActivityFacts, } from "./timeline.js";
|
|
4
4
|
import { isHeartAbsent, readSealFromLeash, readTransaction, transaction, withHeart } from "./storage.js";
|
|
5
5
|
import { soulFact } from "./soul.js";
|
|
6
|
-
export { HeartAbsentError, HeldAkumaLeash, initializeHeart, isHeartAbsent, probeLeash, watchHeart } from "./storage.js";
|
|
6
|
+
export { HeartAbsentError, HeldAkumaLeash, classifyHeartSchema, initializeHeart, isHeartAbsent, probeLeash, watchHeart, } from "./storage.js";
|
|
7
7
|
export { admitRequest, readNonterminalRequests, readRequest, refuseRequest, reserveRequest, serveRequest, serveUpstreamRequest, voidRequest, } from "./request-authority.js";
|
|
8
8
|
export { life, lifeAt } from "./facts.js";
|
|
9
9
|
const ACTIVITY_LIMIT = 5_000;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DatabaseSync } from "node:sqlite";
|
|
2
2
|
export declare function assertHeartSchemaVersion(database: DatabaseSync): void;
|
|
3
3
|
export declare function assertLeashSchemaVersion(database: DatabaseSync): void;
|
|
4
|
+
export declare function heartSchemaIsCurrent(database: DatabaseSync): boolean;
|
|
4
5
|
export declare const HEART_SCHEMA = "\n CREATE TABLE IF NOT EXISTS akuma_schema (\n singleton INTEGER PRIMARY KEY CHECK (singleton = 1),\n version INTEGER NOT NULL CHECK (version = 20)\n ) STRICT;\n INSERT OR IGNORE INTO akuma_schema(singleton, version) VALUES (1, 20);\n CREATE TABLE IF NOT EXISTS soul (\n singleton INTEGER PRIMARY KEY CHECK (singleton = 1),\n soul_json TEXT NOT NULL CHECK (json_valid(soul_json))\n ) STRICT;\n CREATE TABLE IF NOT EXISTS bodies (\n sequence INTEGER PRIMARY KEY AUTOINCREMENT,\n leash_taken_at TEXT NOT NULL,\n hung_diagnostic TEXT,\n hung_at TEXT,\n end TEXT CHECK (end IN ('exited', 'broke-off', 'put-down')),\n ended_at TEXT,\n CHECK ((hung_diagnostic IS NULL AND hung_at IS NULL)\n OR (hung_diagnostic IS NOT NULL AND hung_at IS NOT NULL))\n ) STRICT;\n CREATE TABLE IF NOT EXISTS sessions (\n sequence INTEGER PRIMARY KEY AUTOINCREMENT,\n provider TEXT NOT NULL,\n coordinate_json TEXT NOT NULL CHECK (json_valid(coordinate_json)),\n cwd TEXT NOT NULL,\n options_json TEXT NOT NULL CHECK (json_valid(options_json)),\n admitted_at TEXT NOT NULL\n ) STRICT;\n CREATE TABLE IF NOT EXISTS turns (\n sequence INTEGER PRIMARY KEY REFERENCES timeline(sequence) ON DELETE CASCADE,\n body_sequence INTEGER NOT NULL REFERENCES bodies(sequence),\n started_at TEXT NOT NULL,\n end_sequence INTEGER UNIQUE REFERENCES timeline(sequence) ON DELETE SET NULL,\n outcome TEXT CHECK (outcome IN ('answered', 'failed')),\n history_id TEXT UNIQUE,\n session_json TEXT CHECK (session_json IS NULL OR json_valid(session_json)),\n answer TEXT,\n diagnostic TEXT,\n completed_at TEXT,\n CHECK (\n (outcome = 'answered' AND session_json IS NOT NULL AND answer IS NOT NULL AND diagnostic IS NULL)\n OR (outcome = 'failed' AND history_id IS NULL AND session_json IS NULL AND answer IS NULL AND diagnostic IS NOT NULL)\n OR (outcome IS NULL AND end_sequence IS NULL AND history_id IS NULL AND session_json IS NULL AND answer IS NULL AND diagnostic IS NULL AND completed_at IS NULL)\n )\n ) STRICT;\n CREATE TABLE IF NOT EXISTS timeline (\n sequence INTEGER PRIMARY KEY AUTOINCREMENT,\n kind TEXT NOT NULL CHECK (kind IN ('turn-start', 'call', 'activity', 'tell', 'turn-end'))\n ) STRICT;\n CREATE TABLE IF NOT EXISTS calls (\n sequence INTEGER PRIMARY KEY REFERENCES timeline(sequence) ON DELETE CASCADE,\n turn_sequence INTEGER NOT NULL REFERENCES turns(sequence) ON DELETE CASCADE,\n body TEXT NOT NULL,\n at TEXT NOT NULL\n ) STRICT;\n CREATE TABLE IF NOT EXISTS activity (\n sequence INTEGER PRIMARY KEY REFERENCES timeline(sequence) ON DELETE CASCADE,\n turn_sequence INTEGER NOT NULL REFERENCES turns(sequence) ON DELETE CASCADE,\n event_json TEXT NOT NULL CHECK (json_valid(event_json)),\n at TEXT NOT NULL\n ) STRICT;\n CREATE TABLE IF NOT EXISTS tells (\n id TEXT PRIMARY KEY,\n sequence INTEGER NOT NULL UNIQUE REFERENCES timeline(sequence) ON DELETE CASCADE,\n body TEXT NOT NULL,\n recorded_at TEXT NOT NULL\n ) STRICT;\n CREATE TABLE IF NOT EXISTS tell_deliveries (\n sequence INTEGER PRIMARY KEY AUTOINCREMENT,\n tell_id TEXT NOT NULL REFERENCES tells(id) ON DELETE CASCADE,\n route TEXT NOT NULL CHECK (route IN ('launch', 'live')),\n turn_sequence INTEGER NOT NULL REFERENCES turns(sequence) ON DELETE CASCADE,\n fence TEXT NOT NULL,\n receipt TEXT CHECK (receipt IN ('unavailable', 'required')),\n delivered_at TEXT NOT NULL,\n CHECK ((route = 'launch' AND receipt IS NULL) OR (route = 'live' AND receipt IS NOT NULL)),\n UNIQUE (tell_id, turn_sequence, fence)\n ) STRICT;\n CREATE TABLE IF NOT EXISTS tell_receipts (\n sequence INTEGER PRIMARY KEY AUTOINCREMENT,\n evidence TEXT NOT NULL CHECK (evidence IN ('exact', 'fence')),\n tell_id TEXT REFERENCES tells(id) ON DELETE CASCADE,\n turn_sequence INTEGER REFERENCES turns(sequence) ON DELETE CASCADE,\n fence TEXT,\n kind TEXT NOT NULL,\n received_at TEXT NOT NULL,\n CHECK (\n (evidence = 'exact' AND tell_id IS NOT NULL AND turn_sequence IS NULL AND fence IS NULL)\n OR (evidence = 'fence' AND tell_id IS NULL AND turn_sequence IS NOT NULL AND fence IS NOT NULL)\n )\n ) STRICT;\n CREATE UNIQUE INDEX IF NOT EXISTS tell_receipts_exact\n ON tell_receipts(tell_id, kind) WHERE evidence = 'exact';\n CREATE UNIQUE INDEX IF NOT EXISTS tell_receipts_fence\n ON tell_receipts(turn_sequence, fence, kind) WHERE evidence = 'fence';\n CREATE TABLE IF NOT EXISTS requests (\n sequence INTEGER PRIMARY KEY AUTOINCREMENT,\n id TEXT NOT NULL UNIQUE,\n requester TEXT NOT NULL,\n action TEXT NOT NULL CHECK (\n action IN (\n 'akuma.call', 'akuma.wait', 'akuma.tell', 'akuma.kill',\n 'contract.deliver', 'contract.review',\n 'task.add', 'task.addDocument', 'task.compose', 'task.update', 'task.start',\n 'task.stop', 'task.hold', 'task.resume', 'task.done', 'task.drop'\n )\n ),\n payload_json TEXT NOT NULL CHECK (json_valid(payload_json)),\n admitted_at TEXT NOT NULL,\n state TEXT NOT NULL CHECK (state IN ('admitted', 'reserved', 'served', 'refused', 'voided')),\n child TEXT,\n service_json TEXT CHECK (service_json IS NULL OR json_valid(service_json)),\n diagnostic TEXT,\n evidence TEXT,\n CHECK (\n (state = 'admitted' AND child IS NULL AND service_json IS NULL AND diagnostic IS NULL AND evidence IS NULL)\n OR (state = 'reserved' AND action = 'akuma.call'\n AND child IS NOT NULL AND service_json IS NULL\n AND diagnostic IS NULL AND evidence IS NULL)\n OR (state = 'served'\n AND ((action = 'akuma.call' AND child IS NOT NULL AND service_json IS NULL)\n OR (action != 'akuma.call' AND child IS NULL AND service_json IS NOT NULL))\n AND diagnostic IS NULL AND evidence IS NULL)\n OR (state = 'refused' AND child IS NULL AND service_json IS NULL AND diagnostic IS NOT NULL AND evidence IS NULL)\n OR (state = 'voided' AND child IS NULL AND service_json IS NULL AND diagnostic IS NULL AND evidence IS NOT NULL)\n )\n ) STRICT;\n CREATE TABLE IF NOT EXISTS control (\n kind TEXT PRIMARY KEY CHECK (kind IN ('stop', 'pause')),\n value_json TEXT NOT NULL CHECK (json_valid(value_json)),\n at TEXT NOT NULL\n ) STRICT;\n CREATE TABLE IF NOT EXISTS kills (\n sequence INTEGER PRIMARY KEY AUTOINCREMENT,\n body_sequence INTEGER NOT NULL UNIQUE REFERENCES bodies(sequence),\n evidence TEXT NOT NULL CHECK (evidence = 'killed'),\n at TEXT NOT NULL\n ) STRICT;\n";
|
|
5
6
|
export declare const LEASH_SCHEMA = "\n PRAGMA journal_mode=DELETE;\n CREATE TABLE IF NOT EXISTS leash_schema (\n singleton INTEGER PRIMARY KEY CHECK (singleton = 1),\n version INTEGER NOT NULL CHECK (version = 4)\n ) STRICT;\n INSERT OR IGNORE INTO leash_schema(singleton, version) VALUES (1, 4);\n CREATE TABLE IF NOT EXISTS seal (\n singleton INTEGER PRIMARY KEY CHECK (singleton = 1),\n evidence TEXT NOT NULL,\n at TEXT NOT NULL\n ) STRICT;\n";
|
|
@@ -11,6 +11,15 @@ export function assertHeartSchemaVersion(database) {
|
|
|
11
11
|
export function assertLeashSchemaVersion(database) {
|
|
12
12
|
assertSchemaVersion(database, "leash_schema", LEASH_SCHEMA_VERSION);
|
|
13
13
|
}
|
|
14
|
+
export function heartSchemaIsCurrent(database) {
|
|
15
|
+
try {
|
|
16
|
+
const row = database.prepare(`SELECT version FROM akuma_schema WHERE singleton = 1`).get();
|
|
17
|
+
return row?.version === HEART_SCHEMA_VERSION;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
14
23
|
export const HEART_SCHEMA = `
|
|
15
24
|
CREATE TABLE IF NOT EXISTS akuma_schema (
|
|
16
25
|
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
@@ -8,6 +8,7 @@ export declare class HeartAbsentError extends Error {
|
|
|
8
8
|
constructor(path: string, options?: ErrorOptions);
|
|
9
9
|
}
|
|
10
10
|
export declare function isHeartAbsent(error: unknown): error is HeartAbsentError;
|
|
11
|
+
export declare function classifyHeartSchema(paths: AkumaPaths): Promise<"current" | "unsupported">;
|
|
11
12
|
export declare function withHeart<T>(paths: AkumaPaths, body: (database: DatabaseSync) => T): Promise<T>;
|
|
12
13
|
export declare function transaction<T>(database: DatabaseSync, body: () => T): T;
|
|
13
14
|
export declare function readTransaction<T>(database: DatabaseSync, body: () => T): T;
|
|
@@ -3,7 +3,7 @@ import { watch as watchDirectory } from "node:fs";
|
|
|
3
3
|
import { lstat } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
|
-
import { HEART_SCHEMA, LEASH_SCHEMA, assertHeartSchemaVersion, assertLeashSchemaVersion } from "./schema.js";
|
|
6
|
+
import { HEART_SCHEMA, LEASH_SCHEMA, assertHeartSchemaVersion, assertLeashSchemaVersion, heartSchemaIsCurrent, } from "./schema.js";
|
|
7
7
|
import { deletePauseControl, deleteStopControl, insertBodyFact, insertKillFact, insertSealFact, insertSessionFact, killFactForBody, latestBodyFact, latestKillFact, markBodyHung, sealExists, sealFact, stopFact, } from "./rows.js";
|
|
8
8
|
import { insertTellFact } from "./tells.js";
|
|
9
9
|
import { pruneActivityFacts } from "./timeline.js";
|
|
@@ -115,6 +115,23 @@ async function openHeart(path, verify = true) {
|
|
|
115
115
|
throw error;
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
+
export async function classifyHeartSchema(paths) {
|
|
119
|
+
let database;
|
|
120
|
+
try {
|
|
121
|
+
database = await openExistingDatabase(paths.heart);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (isHeartAbsent(error))
|
|
125
|
+
return "unsupported";
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
return heartSchemaIsCurrent(database) ? "current" : "unsupported";
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
database.close();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
118
135
|
export async function withHeart(paths, body) {
|
|
119
136
|
const database = await openHeart(paths.heart);
|
|
120
137
|
try {
|
package/build/src/akuma/nuke.js
CHANGED
|
@@ -2,7 +2,7 @@ import { lstat, readdir, rm, rmdir } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { nukeAliases } from "../alias/index.js";
|
|
4
4
|
import { CONTROL_RESPONSE_MS } from "./body.js";
|
|
5
|
-
import { HeldAkumaLeash, readHeart, readKill, requestStop } from "./heart/index.js";
|
|
5
|
+
import { HeldAkumaLeash, classifyHeartSchema, readHeart, readKill, requestStop } from "./heart/index.js";
|
|
6
6
|
import { akuIdFromDirectoryName, akumaPaths, akumaRunRoot } from "./identity.js";
|
|
7
7
|
const POLL_MS = 100;
|
|
8
8
|
async function hasAkumaCustody(paths) {
|
|
@@ -201,8 +201,11 @@ async function nukeAkumaEntries(world) {
|
|
|
201
201
|
export async function stopAkuma(world) {
|
|
202
202
|
const entries = await nukeAkumaEntries(world);
|
|
203
203
|
const held = [];
|
|
204
|
+
let prepared = false;
|
|
204
205
|
try {
|
|
205
206
|
for (const entry of entries) {
|
|
207
|
+
if ((await classifyHeartSchema(entry.paths)) === "unsupported")
|
|
208
|
+
continue;
|
|
206
209
|
const snapshot = await readHeart(entry.paths);
|
|
207
210
|
const leash = snapshot.soul !== null && snapshot.latestBody?.end === undefined
|
|
208
211
|
? await stopRunningAkuma(entry)
|
|
@@ -222,6 +225,7 @@ export async function stopAkuma(world) {
|
|
|
222
225
|
throw error;
|
|
223
226
|
}
|
|
224
227
|
}
|
|
228
|
+
prepared = true;
|
|
225
229
|
return async () => {
|
|
226
230
|
try {
|
|
227
231
|
await nukeAliases(world);
|
|
@@ -236,7 +240,7 @@ export async function stopAkuma(world) {
|
|
|
236
240
|
};
|
|
237
241
|
}
|
|
238
242
|
finally {
|
|
239
|
-
if (
|
|
243
|
+
if (!prepared) {
|
|
240
244
|
for (const leash of held.reverse())
|
|
241
245
|
leash.release();
|
|
242
246
|
}
|
|
@@ -118,8 +118,91 @@ function finishClaudeInput(input, terminal) {
|
|
|
118
118
|
else
|
|
119
119
|
input.close();
|
|
120
120
|
}
|
|
121
|
+
async function consumeClaudeQuery(context) {
|
|
122
|
+
const { query, input, events, observation, progress, admit, rejectAdmission, settle, controls } = context;
|
|
123
|
+
let admitted = false;
|
|
124
|
+
let terminal = null;
|
|
125
|
+
let historyId;
|
|
126
|
+
try {
|
|
127
|
+
for await (const message of query) {
|
|
128
|
+
if (!admitted && "session_id" in message && typeof message.session_id === "string") {
|
|
129
|
+
admitted = true;
|
|
130
|
+
events.emit({ type: "session", coordinate: { sessionId: message.session_id } });
|
|
131
|
+
admit();
|
|
132
|
+
}
|
|
133
|
+
emitClaudeMessage(message, events, observation);
|
|
134
|
+
if (message.type === "assistant" &&
|
|
135
|
+
message.parent_tool_use_id === null &&
|
|
136
|
+
typeof message.uuid === "string" &&
|
|
137
|
+
message.uuid.length > 0)
|
|
138
|
+
historyId = message.uuid;
|
|
139
|
+
if (message.type !== "result")
|
|
140
|
+
continue;
|
|
141
|
+
if (message.subtype === "success") {
|
|
142
|
+
terminal = successfulResult(message, historyId);
|
|
143
|
+
progress.checkpoint += 1;
|
|
144
|
+
controls.flushReceipts();
|
|
145
|
+
queueMicrotask(controls.closeWhenIdle);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
terminal = { kind: "failed", diagnostic: message.errors.join("; ") || message.subtype };
|
|
149
|
+
input.close();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
progress.ended = true;
|
|
153
|
+
const result = terminal ?? { kind: "failed", diagnostic: "Claude query ended without a result" };
|
|
154
|
+
if (!admitted)
|
|
155
|
+
rejectAdmission(new Error(result.kind === "failed" ? result.diagnostic : "Claude query ended before session admission"));
|
|
156
|
+
settle(result);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
progress.ended = true;
|
|
160
|
+
input.fail(error);
|
|
161
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
162
|
+
if (!admitted)
|
|
163
|
+
rejectAdmission(failure);
|
|
164
|
+
settle({ kind: "failed", diagnostic: failure.message });
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
finishClaudeInput(input, terminal);
|
|
168
|
+
events.end();
|
|
169
|
+
controls.settleReceipts();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function claudeTell(input, observed, accepted, state, run) {
|
|
173
|
+
return (tell) => {
|
|
174
|
+
if (observed.ended || input.closed)
|
|
175
|
+
return Promise.resolve({ kind: "turn-ended" });
|
|
176
|
+
state.openSubmissions += 1;
|
|
177
|
+
const ordinal = ++state.submission;
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
179
|
+
const pending = { id: tell.id, afterCheckpoint: 0, visible: false };
|
|
180
|
+
void input
|
|
181
|
+
.push(claudeUserMessage(tell.text), () => {
|
|
182
|
+
pending.afterCheckpoint = observed.checkpoint;
|
|
183
|
+
accepted.push(pending);
|
|
184
|
+
})
|
|
185
|
+
.then(() => {
|
|
186
|
+
state.openSubmissions -= 1;
|
|
187
|
+
resolve({ kind: "accepted", fence: `claude:${run}:${ordinal}` });
|
|
188
|
+
queueMicrotask(() => {
|
|
189
|
+
pending.visible = true;
|
|
190
|
+
observed.flushReceipts();
|
|
191
|
+
observed.settleReceipts();
|
|
192
|
+
});
|
|
193
|
+
}, (error) => {
|
|
194
|
+
state.openSubmissions -= 1;
|
|
195
|
+
if (isClaudeTurnEnded(error))
|
|
196
|
+
resolve({ kind: "turn-ended" });
|
|
197
|
+
else
|
|
198
|
+
reject(error);
|
|
199
|
+
observed.settleReceipts();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
}
|
|
121
204
|
function observeClaudeQuery(context) {
|
|
122
|
-
const {
|
|
205
|
+
const { input, receipts, accepted, state } = context;
|
|
123
206
|
const observation = { tools: new Map() };
|
|
124
207
|
let admit;
|
|
125
208
|
let rejectAdmission;
|
|
@@ -131,15 +214,15 @@ function observeClaudeQuery(context) {
|
|
|
131
214
|
const completion = new Promise((resolve) => {
|
|
132
215
|
settle = resolve;
|
|
133
216
|
});
|
|
134
|
-
|
|
217
|
+
const progress = { checkpoint: 0, ended: false };
|
|
135
218
|
const settleReceipts = () => {
|
|
136
|
-
if (ended && state.openSubmissions === 0 && accepted.every((tell) => tell.visible))
|
|
219
|
+
if (progress.ended && state.openSubmissions === 0 && accepted.every((tell) => tell.visible))
|
|
137
220
|
receipts.end();
|
|
138
221
|
};
|
|
139
222
|
const flushReceipts = () => {
|
|
140
223
|
for (let index = 0; index < accepted.length;) {
|
|
141
224
|
const tell = accepted[index];
|
|
142
|
-
if (!tell.visible || tell.afterCheckpoint >= checkpoint) {
|
|
225
|
+
if (!tell.visible || tell.afterCheckpoint >= progress.checkpoint) {
|
|
143
226
|
index += 1;
|
|
144
227
|
continue;
|
|
145
228
|
}
|
|
@@ -153,64 +236,23 @@ function observeClaudeQuery(context) {
|
|
|
153
236
|
if (state.openSubmissions === 0 && accepted.length === 0 && input.pending === 0)
|
|
154
237
|
input.close();
|
|
155
238
|
};
|
|
156
|
-
void (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
admit();
|
|
166
|
-
}
|
|
167
|
-
emitClaudeMessage(message, events, observation);
|
|
168
|
-
if (message.type === "assistant" &&
|
|
169
|
-
message.parent_tool_use_id === null &&
|
|
170
|
-
typeof message.uuid === "string" &&
|
|
171
|
-
message.uuid.length > 0)
|
|
172
|
-
historyId = message.uuid;
|
|
173
|
-
if (message.type !== "result")
|
|
174
|
-
continue;
|
|
175
|
-
if (message.subtype === "success") {
|
|
176
|
-
terminal = successfulResult(message, historyId);
|
|
177
|
-
checkpoint += 1;
|
|
178
|
-
flushReceipts();
|
|
179
|
-
queueMicrotask(closeWhenIdle);
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
terminal = { kind: "failed", diagnostic: message.errors.join("; ") || message.subtype };
|
|
183
|
-
input.close();
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
ended = true;
|
|
187
|
-
const result = terminal ?? { kind: "failed", diagnostic: "Claude query ended without a result" };
|
|
188
|
-
if (!admitted)
|
|
189
|
-
rejectAdmission(new Error(result.kind === "failed" ? result.diagnostic : "Claude query ended before session admission"));
|
|
190
|
-
settle(result);
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
ended = true;
|
|
194
|
-
input.fail(error);
|
|
195
|
-
const failure = error instanceof Error ? error : new Error(String(error));
|
|
196
|
-
if (!admitted)
|
|
197
|
-
rejectAdmission(failure);
|
|
198
|
-
settle({ kind: "failed", diagnostic: failure.message });
|
|
199
|
-
}
|
|
200
|
-
finally {
|
|
201
|
-
finishClaudeInput(input, terminal);
|
|
202
|
-
events.end();
|
|
203
|
-
settleReceipts();
|
|
204
|
-
}
|
|
205
|
-
})();
|
|
239
|
+
void consumeClaudeQuery({
|
|
240
|
+
...context,
|
|
241
|
+
observation,
|
|
242
|
+
progress,
|
|
243
|
+
admit,
|
|
244
|
+
rejectAdmission,
|
|
245
|
+
settle,
|
|
246
|
+
controls: { flushReceipts, settleReceipts, closeWhenIdle },
|
|
247
|
+
});
|
|
206
248
|
return {
|
|
207
249
|
admission,
|
|
208
250
|
completion,
|
|
209
251
|
get ended() {
|
|
210
|
-
return ended;
|
|
252
|
+
return progress.ended;
|
|
211
253
|
},
|
|
212
254
|
get checkpoint() {
|
|
213
|
-
return checkpoint;
|
|
255
|
+
return progress.checkpoint;
|
|
214
256
|
},
|
|
215
257
|
flushReceipts,
|
|
216
258
|
settleReceipts,
|
|
@@ -229,8 +271,7 @@ async function driveClaude(load, execution, drive) {
|
|
|
229
271
|
const abortController = new AbortController();
|
|
230
272
|
const run = randomUUID();
|
|
231
273
|
const accepted = [];
|
|
232
|
-
|
|
233
|
-
let openSubmissions = 0;
|
|
274
|
+
const driveState = { submission: 0, openSubmissions: 0 };
|
|
234
275
|
const launchAcknowledged = input.push(claudeUserMessage(launchText(drive)));
|
|
235
276
|
const query = sdk.query({
|
|
236
277
|
prompt: input.iterable,
|
|
@@ -251,7 +292,7 @@ async function driveClaude(load, execution, drive) {
|
|
|
251
292
|
accepted,
|
|
252
293
|
state: {
|
|
253
294
|
get openSubmissions() {
|
|
254
|
-
return openSubmissions;
|
|
295
|
+
return driveState.openSubmissions;
|
|
255
296
|
},
|
|
256
297
|
},
|
|
257
298
|
});
|
|
@@ -266,36 +307,7 @@ async function driveClaude(load, execution, drive) {
|
|
|
266
307
|
events,
|
|
267
308
|
receipts,
|
|
268
309
|
completion: observed.completion,
|
|
269
|
-
tell(
|
|
270
|
-
if (observed.ended || input.closed)
|
|
271
|
-
return Promise.resolve({ kind: "turn-ended" });
|
|
272
|
-
openSubmissions += 1;
|
|
273
|
-
const ordinal = ++submission;
|
|
274
|
-
return new Promise((resolve, reject) => {
|
|
275
|
-
const pending = { id: tell.id, afterCheckpoint: 0, visible: false };
|
|
276
|
-
void input
|
|
277
|
-
.push(claudeUserMessage(tell.text), () => {
|
|
278
|
-
pending.afterCheckpoint = observed.checkpoint;
|
|
279
|
-
accepted.push(pending);
|
|
280
|
-
})
|
|
281
|
-
.then(() => {
|
|
282
|
-
openSubmissions -= 1;
|
|
283
|
-
resolve({ kind: "accepted", fence: `claude:${run}:${ordinal}` });
|
|
284
|
-
queueMicrotask(() => {
|
|
285
|
-
pending.visible = true;
|
|
286
|
-
observed.flushReceipts();
|
|
287
|
-
observed.settleReceipts();
|
|
288
|
-
});
|
|
289
|
-
}, (error) => {
|
|
290
|
-
openSubmissions -= 1;
|
|
291
|
-
if (isClaudeTurnEnded(error))
|
|
292
|
-
resolve({ kind: "turn-ended" });
|
|
293
|
-
else
|
|
294
|
-
reject(error);
|
|
295
|
-
observed.settleReceipts();
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
},
|
|
310
|
+
tell: claudeTell(input, observed, accepted, driveState, run),
|
|
299
311
|
async abort() {
|
|
300
312
|
shutDown(new Error("Claude query aborted"));
|
|
301
313
|
await observed.completion;
|
|
@@ -124,12 +124,8 @@ async function abortTurn(server, state, settle) {
|
|
|
124
124
|
settle({ kind: "failed", diagnostic: "codex app-server interrupted" });
|
|
125
125
|
await server.close(true);
|
|
126
126
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (input.session.kind === "resume" && !("sessionId" in input.session.coordinate))
|
|
130
|
-
throw new Error("Codex app-server resume requires sessionId");
|
|
131
|
-
const events = new AgentEventChannel();
|
|
132
|
-
const server = new LineRpcProcess({
|
|
127
|
+
function codexServer(execution, input) {
|
|
128
|
+
return new LineRpcProcess({
|
|
133
129
|
argv: [execution.executable ?? "codex", "app-server", "--listen", "stdio://"],
|
|
134
130
|
cwd: input.cwd,
|
|
135
131
|
...(execution.env === undefined && input.requests === undefined
|
|
@@ -142,6 +138,13 @@ async function startCodex(execution, input) {
|
|
|
142
138
|
},
|
|
143
139
|
}),
|
|
144
140
|
});
|
|
141
|
+
}
|
|
142
|
+
async function startCodex(execution, input) {
|
|
143
|
+
const signal = input.signal ?? new AbortController().signal;
|
|
144
|
+
if (input.session.kind === "resume" && !("sessionId" in input.session.coordinate))
|
|
145
|
+
throw new Error("Codex app-server resume requires sessionId");
|
|
146
|
+
const events = new AgentEventChannel();
|
|
147
|
+
const server = codexServer(execution, input);
|
|
145
148
|
const state = { settled: false, tools: new Map() };
|
|
146
149
|
let settle;
|
|
147
150
|
const completion = new Promise((resolve) => {
|
|
@@ -201,6 +201,26 @@ async function forceDisposeOpencode(abortController, close, finish) {
|
|
|
201
201
|
await close();
|
|
202
202
|
await finish({ kind: "failed", diagnostic: "OpenCode session force-disposed" });
|
|
203
203
|
}
|
|
204
|
+
async function loadDriveRuntime(execution, input, signal, loader) {
|
|
205
|
+
return await abortable(loadOpencode({
|
|
206
|
+
...execution,
|
|
207
|
+
env: {
|
|
208
|
+
...execution.env,
|
|
209
|
+
...(input.requests === undefined ? {} : { [AKUMA_REQUESTS_ENV]: input.requests.dir }),
|
|
210
|
+
},
|
|
211
|
+
}, input.cwd, signal, loader), signal, async (late) => await late.close());
|
|
212
|
+
}
|
|
213
|
+
async function openDriveSession(runtime, input, resumeSessionId, signal) {
|
|
214
|
+
const session = runtime.client.session;
|
|
215
|
+
const response = await abortable(input.session.kind === "fresh"
|
|
216
|
+
? session.create({ query: { directory: input.cwd }, throwOnError: true })
|
|
217
|
+
: session.get({ path: { id: resumeSessionId }, query: { directory: input.cwd }, throwOnError: true }), signal);
|
|
218
|
+
const info = object(object(response)?.data) ?? object(response);
|
|
219
|
+
const sessionId = text(info?.id) ?? resumeSessionId;
|
|
220
|
+
if (sessionId === undefined)
|
|
221
|
+
throw new Error("OpenCode did not return a session id");
|
|
222
|
+
return { session, sessionId };
|
|
223
|
+
}
|
|
204
224
|
async function drive(execution, input, loader) {
|
|
205
225
|
admit(input.options);
|
|
206
226
|
const signal = input.signal ?? new AbortController().signal;
|
|
@@ -209,13 +229,7 @@ async function drive(execution, input, loader) {
|
|
|
209
229
|
const abortSetup = () => abortController.abort(signal.reason);
|
|
210
230
|
signal.addEventListener("abort", abortSetup, { once: true });
|
|
211
231
|
signal.throwIfAborted();
|
|
212
|
-
const runtime = await
|
|
213
|
-
...execution,
|
|
214
|
-
env: {
|
|
215
|
-
...execution.env,
|
|
216
|
-
...(input.requests === undefined ? {} : { [AKUMA_REQUESTS_ENV]: input.requests.dir }),
|
|
217
|
-
},
|
|
218
|
-
}, input.cwd, abortController.signal, loader), abortController.signal, async (late) => await late.close());
|
|
232
|
+
const runtime = await loadDriveRuntime(execution, input, abortController.signal, loader);
|
|
219
233
|
let closing;
|
|
220
234
|
let iterator;
|
|
221
235
|
const closeOnce = () => {
|
|
@@ -223,14 +237,7 @@ async function drive(execution, input, loader) {
|
|
|
223
237
|
return closing;
|
|
224
238
|
};
|
|
225
239
|
try {
|
|
226
|
-
const session = runtime.
|
|
227
|
-
const sessionResponse = await abortable(input.session.kind === "fresh"
|
|
228
|
-
? session.create({ query: { directory: input.cwd }, throwOnError: true })
|
|
229
|
-
: session.get({ path: { id: resumeSessionId }, query: { directory: input.cwd }, throwOnError: true }), abortController.signal);
|
|
230
|
-
const info = object(object(sessionResponse)?.data) ?? object(sessionResponse);
|
|
231
|
-
const sessionId = text(info?.id) ?? resumeSessionId;
|
|
232
|
-
if (sessionId === undefined)
|
|
233
|
-
throw new Error("OpenCode did not return a session id");
|
|
240
|
+
const { session, sessionId } = await openDriveSession(runtime, input, resumeSessionId, abortController.signal);
|
|
234
241
|
const events = new AgentEventChannel();
|
|
235
242
|
const state = createEventState(sessionId);
|
|
236
243
|
const messageID = `msg_${randomUUID().replaceAll("-", "")}`;
|
|
@@ -83,6 +83,32 @@ function forceDisposePi(dispose, settle, setAborting) {
|
|
|
83
83
|
settle({ kind: "failed", diagnostic: "Pi session force-disposed" });
|
|
84
84
|
return Promise.resolve();
|
|
85
85
|
}
|
|
86
|
+
async function runPiPrompt(native, input, events, state, settle) {
|
|
87
|
+
try {
|
|
88
|
+
await native.prompt([input.body, ...input.launchTells.map((tell) => tell.text)].join("\n\n"));
|
|
89
|
+
let result;
|
|
90
|
+
if (state.aborting)
|
|
91
|
+
result = { kind: "failed", diagnostic: "Pi session aborted" };
|
|
92
|
+
else if (state.terminalFailure !== null)
|
|
93
|
+
result = { kind: "failed", diagnostic: state.terminalFailure };
|
|
94
|
+
else if (!events.assistantSeen)
|
|
95
|
+
result = { kind: "failed", diagnostic: "Pi completed without a native assistant answer" };
|
|
96
|
+
else {
|
|
97
|
+
const historyId = native.sessionManager.getLeafId();
|
|
98
|
+
result = {
|
|
99
|
+
kind: "answered",
|
|
100
|
+
answer: events.answer,
|
|
101
|
+
...(historyId === null ? {} : { historyId }),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
settle(result);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
settle(state.aborting
|
|
108
|
+
? { kind: "failed", diagnostic: "Pi session aborted" }
|
|
109
|
+
: { kind: "failed", diagnostic: diagnostic(error) });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
86
112
|
async function drivePi(sdk, execution, input, signal) {
|
|
87
113
|
const created = await createPiSession(sdk, execution, input, signal);
|
|
88
114
|
const native = created.session;
|
|
@@ -95,16 +121,17 @@ async function drivePi(sdk, execution, input, signal) {
|
|
|
95
121
|
throw new Error("Pi session admitted without sessionFile");
|
|
96
122
|
}
|
|
97
123
|
const events = new AgentEventChannel();
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
124
|
+
const eventState = { answer: "", assistantSeen: false, tools: new Map() };
|
|
125
|
+
const state = {
|
|
126
|
+
terminalFailure: null,
|
|
127
|
+
disposed: false,
|
|
128
|
+
aborting: false,
|
|
129
|
+
settled: false,
|
|
130
|
+
};
|
|
104
131
|
const dispose = () => {
|
|
105
|
-
if (disposed)
|
|
132
|
+
if (state.disposed)
|
|
106
133
|
return;
|
|
107
|
-
disposed = true;
|
|
134
|
+
state.disposed = true;
|
|
108
135
|
try {
|
|
109
136
|
unsubscribe();
|
|
110
137
|
}
|
|
@@ -119,8 +146,8 @@ async function drivePi(sdk, execution, input, signal) {
|
|
|
119
146
|
};
|
|
120
147
|
const unsubscribe = native.subscribe((event) => {
|
|
121
148
|
if (event.type === "agent_end" && !event.willRetry)
|
|
122
|
-
terminalFailure = piTerminalFailure(event.messages);
|
|
123
|
-
for (const translated of translatePiEvent(event,
|
|
149
|
+
state.terminalFailure = piTerminalFailure(event.messages);
|
|
150
|
+
for (const translated of translatePiEvent(event, eventState))
|
|
124
151
|
events.emit(translated);
|
|
125
152
|
});
|
|
126
153
|
events.emit({ type: "session", coordinate: { sessionFile: native.sessionFile, sessionId: native.sessionId } });
|
|
@@ -129,47 +156,22 @@ async function drivePi(sdk, execution, input, signal) {
|
|
|
129
156
|
settleCompletion = resolve;
|
|
130
157
|
});
|
|
131
158
|
const settle = (result) => {
|
|
132
|
-
if (settled)
|
|
159
|
+
if (state.settled)
|
|
133
160
|
return;
|
|
134
|
-
settled = true;
|
|
161
|
+
state.settled = true;
|
|
135
162
|
dispose();
|
|
136
163
|
settleCompletion(result);
|
|
137
164
|
};
|
|
138
|
-
void (
|
|
139
|
-
try {
|
|
140
|
-
await native.prompt([input.body, ...input.launchTells.map((tell) => tell.text)].join("\n\n"));
|
|
141
|
-
let result;
|
|
142
|
-
if (aborting)
|
|
143
|
-
result = { kind: "failed", diagnostic: "Pi session aborted" };
|
|
144
|
-
else if (terminalFailure !== null)
|
|
145
|
-
result = { kind: "failed", diagnostic: terminalFailure };
|
|
146
|
-
else if (!state.assistantSeen)
|
|
147
|
-
result = { kind: "failed", diagnostic: "Pi completed without a native assistant answer" };
|
|
148
|
-
else {
|
|
149
|
-
const historyId = native.sessionManager.getLeafId();
|
|
150
|
-
result = {
|
|
151
|
-
kind: "answered",
|
|
152
|
-
answer: state.answer,
|
|
153
|
-
...(historyId === null ? {} : { historyId }),
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
settle(result);
|
|
157
|
-
}
|
|
158
|
-
catch (error) {
|
|
159
|
-
settle(aborting
|
|
160
|
-
? { kind: "failed", diagnostic: "Pi session aborted" }
|
|
161
|
-
: { kind: "failed", diagnostic: diagnostic(error) });
|
|
162
|
-
}
|
|
163
|
-
})();
|
|
165
|
+
void runPiPrompt(native, input, eventState, state, settle);
|
|
164
166
|
return {
|
|
165
167
|
admission: { fence: native.sessionId },
|
|
166
168
|
events,
|
|
167
169
|
completion,
|
|
168
170
|
abort: () => {
|
|
169
|
-
abortRequest ??= (async () => {
|
|
170
|
-
if (settled)
|
|
171
|
+
state.abortRequest ??= (async () => {
|
|
172
|
+
if (state.settled)
|
|
171
173
|
return;
|
|
172
|
-
aborting = true;
|
|
174
|
+
state.aborting = true;
|
|
173
175
|
try {
|
|
174
176
|
await native.abort();
|
|
175
177
|
}
|
|
@@ -178,10 +180,10 @@ async function drivePi(sdk, execution, input, signal) {
|
|
|
178
180
|
}
|
|
179
181
|
settle({ kind: "failed", diagnostic: "Pi session aborted" });
|
|
180
182
|
})();
|
|
181
|
-
return abortRequest;
|
|
183
|
+
return state.abortRequest;
|
|
182
184
|
},
|
|
183
185
|
forceDispose: () => forceDisposePi(dispose, settle, () => {
|
|
184
|
-
aborting = true;
|
|
186
|
+
state.aborting = true;
|
|
185
187
|
}),
|
|
186
188
|
};
|
|
187
189
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { type AllocatedAkuma } from "./identity.js";
|
|
2
|
+
import type { OwnedProcess } from "../runtime/proc/run.js";
|
|
2
3
|
export declare const BIRTH_TIMEOUT_MS = 30000;
|
|
3
4
|
export declare function publishAkuma(input: Readonly<{
|
|
4
5
|
worldPath: string;
|
|
5
6
|
archetype: string;
|
|
6
7
|
awaitAsleep?: boolean;
|
|
7
8
|
reserve?(allocated: AllocatedAkuma): Promise<void>;
|
|
8
|
-
launch(allocated: AllocatedAkuma): Promise<void>;
|
|
9
|
+
launch(allocated: AllocatedAkuma): Promise<OwnedProcess | void>;
|
|
9
10
|
signal?: AbortSignal;
|
|
10
11
|
}>): Promise<AllocatedAkuma>;
|
|
@@ -40,7 +40,40 @@ async function observedBirthFailure(paths) {
|
|
|
40
40
|
leash.release();
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
function preAdmissionDiagnostic(exit) {
|
|
44
|
+
return exit.code === null ? `pre-admission signal ${exit.signal ?? "unknown"}` : `pre-admission exit ${exit.code}`;
|
|
45
|
+
}
|
|
46
|
+
async function sealObservedExit(paths, exit) {
|
|
47
|
+
const evidence = preAdmissionDiagnostic(exit);
|
|
48
|
+
try {
|
|
49
|
+
const leash = await HeldAkumaLeash.try(paths);
|
|
50
|
+
if (leash !== null) {
|
|
51
|
+
try {
|
|
52
|
+
await leash.sealIfUnborn(paths, { evidence, at: new Date().toISOString() });
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
leash.release();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* Parent evidence remains authoritative when best-effort sealing fails. */
|
|
61
|
+
}
|
|
62
|
+
throw new Error(evidence);
|
|
63
|
+
}
|
|
64
|
+
async function observeSettledExit(owned) {
|
|
65
|
+
if (owned === undefined)
|
|
66
|
+
return { kind: "pending" };
|
|
67
|
+
const pending = Symbol("pending");
|
|
68
|
+
try {
|
|
69
|
+
const exit = await Promise.race([owned.exited, Promise.resolve(pending)]);
|
|
70
|
+
return exit === pending ? { kind: "pending" } : { kind: "exited", exit };
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
return { kind: "exit-error", error };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function awaitBirth(paths, owned, signal) {
|
|
44
77
|
const deadline = performance.now() + BIRTH_TIMEOUT_MS;
|
|
45
78
|
for (;;) {
|
|
46
79
|
signal?.throwIfAborted();
|
|
@@ -48,14 +81,37 @@ async function awaitBirth(paths, signal) {
|
|
|
48
81
|
if (soul !== null)
|
|
49
82
|
return soul;
|
|
50
83
|
const failure = await observedBirthFailure(paths);
|
|
51
|
-
if (failure !== null)
|
|
84
|
+
if (failure !== null) {
|
|
85
|
+
const settled = await observeSettledExit(owned);
|
|
86
|
+
if (settled.kind === "exited")
|
|
87
|
+
await sealObservedExit(paths, settled.exit);
|
|
88
|
+
if (settled.kind === "exit-error")
|
|
89
|
+
throw new Error(diagnostic(settled.error));
|
|
52
90
|
throw new Error(failure);
|
|
91
|
+
}
|
|
53
92
|
if (performance.now() >= deadline) {
|
|
54
93
|
const settled = await settleTimedOutBirth(paths);
|
|
55
94
|
if (settled !== null)
|
|
56
95
|
return settled;
|
|
57
96
|
}
|
|
58
|
-
|
|
97
|
+
if (owned === undefined) {
|
|
98
|
+
await abortableDelay(Math.min(POLL_MS, Math.max(0, deadline - performance.now())), signal);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const outcome = await Promise.race([
|
|
102
|
+
abortableDelay(Math.min(POLL_MS, Math.max(0, deadline - performance.now())), signal).then(() => "poll"),
|
|
103
|
+
owned.exited.then((exit) => ({ kind: "exited", exit }), (error) => ({ kind: "exit-error", error })),
|
|
104
|
+
]);
|
|
105
|
+
if (outcome === "poll")
|
|
106
|
+
continue;
|
|
107
|
+
if (outcome.kind === "exited") {
|
|
108
|
+
const settledSoul = await readSoul(paths);
|
|
109
|
+
if (settledSoul !== null)
|
|
110
|
+
return settledSoul;
|
|
111
|
+
await sealObservedExit(paths, outcome.exit);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
throw new Error(diagnostic(outcome.error));
|
|
59
115
|
}
|
|
60
116
|
}
|
|
61
117
|
async function takeLeashUntil(paths, deadline) {
|
|
@@ -108,17 +164,22 @@ export async function publishAkuma(input) {
|
|
|
108
164
|
input.signal?.throwIfAborted();
|
|
109
165
|
await input.reserve?.(allocated);
|
|
110
166
|
input.signal?.throwIfAborted();
|
|
111
|
-
await input.launch(allocated);
|
|
112
|
-
|
|
167
|
+
const owned = await input.launch(allocated);
|
|
168
|
+
try {
|
|
169
|
+
input.signal?.throwIfAborted();
|
|
170
|
+
const soul = await awaitBirth(allocated.paths, owned ?? undefined, input.signal);
|
|
171
|
+
if (soul.id !== allocated.id)
|
|
172
|
+
throw new Error("Akuma birth returned a different identity");
|
|
173
|
+
if (input.awaitAsleep === true)
|
|
174
|
+
await awaitAsleepBirth(allocated.paths);
|
|
175
|
+
return allocated;
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
owned?.release();
|
|
179
|
+
}
|
|
113
180
|
}
|
|
114
181
|
catch (error) {
|
|
115
182
|
await sealLocalFailure(allocated, error);
|
|
116
183
|
throw error;
|
|
117
184
|
}
|
|
118
|
-
const soul = await awaitBirth(allocated.paths, input.signal);
|
|
119
|
-
if (soul.id !== allocated.id)
|
|
120
|
-
throw new Error("Akuma birth returned a different identity");
|
|
121
|
-
if (input.awaitAsleep === true)
|
|
122
|
-
await awaitAsleepBirth(allocated.paths);
|
|
123
|
-
return allocated;
|
|
124
185
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { UpstreamRequestService } from "./heart/index.js";
|
|
2
2
|
import type { ServeInput, UpstreamFact } from "./request-serve.js";
|
|
3
|
-
|
|
3
|
+
type ExecutionResult = Readonly<{
|
|
4
4
|
result: unknown;
|
|
5
5
|
service?: UpstreamRequestService;
|
|
6
|
-
}
|
|
6
|
+
}>;
|
|
7
|
+
export declare function executeRequest(input: ServeInput, request: UpstreamFact): Promise<ExecutionResult>;
|
|
8
|
+
export {};
|
|
@@ -1,78 +1,90 @@
|
|
|
1
|
+
async function executeWait(input, request) {
|
|
2
|
+
return {
|
|
3
|
+
result: await input.upstream.wait({
|
|
4
|
+
targets: request.targets,
|
|
5
|
+
completion: request.completion,
|
|
6
|
+
...(request.timeoutMs === undefined ? {} : { timeoutMs: request.timeoutMs }),
|
|
7
|
+
signal: input.signal,
|
|
8
|
+
}),
|
|
9
|
+
service: { action: request.action },
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
async function executeTell(input, request) {
|
|
13
|
+
return {
|
|
14
|
+
result: await input.upstream.tell({
|
|
15
|
+
target: request.target,
|
|
16
|
+
body: request.body,
|
|
17
|
+
tellId: request.id,
|
|
18
|
+
recordedAt: request.admittedAt,
|
|
19
|
+
signal: input.signal,
|
|
20
|
+
}),
|
|
21
|
+
service: { action: request.action, target: request.target, tellId: request.id },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
async function executeKill(input, request) {
|
|
25
|
+
const served = await input.upstream.kill({ targets: request.targets, signal: input.signal });
|
|
26
|
+
return { result: served.result, service: { action: request.action, results: served.service } };
|
|
27
|
+
}
|
|
28
|
+
async function executeDeliver(input, request) {
|
|
29
|
+
const served = await input.upstream.deliver({
|
|
30
|
+
repoRoot: request.repoRoot,
|
|
31
|
+
contractId: request.contractId,
|
|
32
|
+
...(request.message === undefined ? {} : { message: request.message }),
|
|
33
|
+
includeDirty: request.includeDirty,
|
|
34
|
+
materializeConflict: request.materializeConflict,
|
|
35
|
+
requester: request.requester,
|
|
36
|
+
signal: input.signal,
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
result: served.result,
|
|
40
|
+
...(served.deliveryFactId === undefined
|
|
41
|
+
? {}
|
|
42
|
+
: {
|
|
43
|
+
service: {
|
|
44
|
+
action: request.action,
|
|
45
|
+
repoRoot: request.repoRoot,
|
|
46
|
+
contractId: request.contractId,
|
|
47
|
+
deliveryFactId: served.deliveryFactId,
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async function executeReview(input, request) {
|
|
53
|
+
const served = await input.upstream.review({
|
|
54
|
+
repoRoot: request.repoRoot,
|
|
55
|
+
contractId: request.contractId,
|
|
56
|
+
verdict: request.verdict,
|
|
57
|
+
...(request.summary === undefined ? {} : { summary: request.summary }),
|
|
58
|
+
requester: request.requester,
|
|
59
|
+
signal: input.signal,
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
result: served.result,
|
|
63
|
+
...(served.reviewFactId === undefined
|
|
64
|
+
? {}
|
|
65
|
+
: {
|
|
66
|
+
service: {
|
|
67
|
+
action: request.action,
|
|
68
|
+
repoRoot: request.repoRoot,
|
|
69
|
+
contractId: request.contractId,
|
|
70
|
+
reviewFactId: served.reviewFactId,
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
1
75
|
export async function executeRequest(input, request) {
|
|
2
76
|
if (input.upstream === undefined)
|
|
3
77
|
throw new Error("upstream execution port is unavailable");
|
|
4
78
|
if (request.action === "akuma.wait")
|
|
5
|
-
return
|
|
6
|
-
result: await input.upstream.wait({
|
|
7
|
-
targets: request.targets,
|
|
8
|
-
completion: request.completion,
|
|
9
|
-
...(request.timeoutMs === undefined ? {} : { timeoutMs: request.timeoutMs }),
|
|
10
|
-
signal: input.signal,
|
|
11
|
-
}),
|
|
12
|
-
service: { action: request.action },
|
|
13
|
-
};
|
|
79
|
+
return executeWait(input, request);
|
|
14
80
|
if (request.action === "akuma.tell")
|
|
15
|
-
return
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}),
|
|
23
|
-
service: { action: request.action, target: request.target, tellId: request.id },
|
|
24
|
-
};
|
|
25
|
-
if (request.action === "akuma.kill") {
|
|
26
|
-
const served = await input.upstream.kill({ targets: request.targets, signal: input.signal });
|
|
27
|
-
return { result: served.result, service: { action: request.action, results: served.service } };
|
|
28
|
-
}
|
|
29
|
-
if (request.action === "contract.deliver") {
|
|
30
|
-
const served = await input.upstream.deliver({
|
|
31
|
-
repoRoot: request.repoRoot,
|
|
32
|
-
contractId: request.contractId,
|
|
33
|
-
...(request.message === undefined ? {} : { message: request.message }),
|
|
34
|
-
includeDirty: request.includeDirty,
|
|
35
|
-
materializeConflict: request.materializeConflict,
|
|
36
|
-
requester: request.requester,
|
|
37
|
-
signal: input.signal,
|
|
38
|
-
});
|
|
39
|
-
return {
|
|
40
|
-
result: served.result,
|
|
41
|
-
...(served.deliveryFactId === undefined
|
|
42
|
-
? {}
|
|
43
|
-
: {
|
|
44
|
-
service: {
|
|
45
|
-
action: request.action,
|
|
46
|
-
repoRoot: request.repoRoot,
|
|
47
|
-
contractId: request.contractId,
|
|
48
|
-
deliveryFactId: served.deliveryFactId,
|
|
49
|
-
},
|
|
50
|
-
}),
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
if (request.action === "contract.review") {
|
|
54
|
-
const served = await input.upstream.review({
|
|
55
|
-
repoRoot: request.repoRoot,
|
|
56
|
-
contractId: request.contractId,
|
|
57
|
-
verdict: request.verdict,
|
|
58
|
-
...(request.summary === undefined ? {} : { summary: request.summary }),
|
|
59
|
-
requester: request.requester,
|
|
60
|
-
signal: input.signal,
|
|
61
|
-
});
|
|
62
|
-
return {
|
|
63
|
-
result: served.result,
|
|
64
|
-
...(served.reviewFactId === undefined
|
|
65
|
-
? {}
|
|
66
|
-
: {
|
|
67
|
-
service: {
|
|
68
|
-
action: request.action,
|
|
69
|
-
repoRoot: request.repoRoot,
|
|
70
|
-
contractId: request.contractId,
|
|
71
|
-
reviewFactId: served.reviewFactId,
|
|
72
|
-
},
|
|
73
|
-
}),
|
|
74
|
-
};
|
|
75
|
-
}
|
|
81
|
+
return executeTell(input, request);
|
|
82
|
+
if (request.action === "akuma.kill")
|
|
83
|
+
return executeKill(input, request);
|
|
84
|
+
if (request.action === "contract.deliver")
|
|
85
|
+
return executeDeliver(input, request);
|
|
86
|
+
if (request.action === "contract.review")
|
|
87
|
+
return executeReview(input, request);
|
|
76
88
|
const task = request;
|
|
77
89
|
return {
|
|
78
90
|
result: await input.upstream.task({
|
|
@@ -2,6 +2,7 @@ import { type KillEvidence, type RequestFact, type Soul } from "./heart/index.js
|
|
|
2
2
|
import { type AkuId, type AkumaPaths } from "./identity.js";
|
|
3
3
|
import { type StructuralRequestClaim } from "./request-wire.js";
|
|
4
4
|
import { type TaskMutationRequest } from "../task/mutation.js";
|
|
5
|
+
import type { OwnedProcess } from "../runtime/proc/run.js";
|
|
5
6
|
export type RequestChildLaunch = Readonly<{
|
|
6
7
|
paths: AkumaPaths;
|
|
7
8
|
seed: Omit<Soul, "createdAt">;
|
|
@@ -64,7 +65,7 @@ type PumpInput = Readonly<{
|
|
|
64
65
|
parent: Soul;
|
|
65
66
|
bodySequence: number;
|
|
66
67
|
now(): string;
|
|
67
|
-
spawn(launch: RequestChildLaunch): Promise<void>;
|
|
68
|
+
spawn(launch: RequestChildLaunch): Promise<OwnedProcess | void>;
|
|
68
69
|
upstream?: UpstreamExecutionPort;
|
|
69
70
|
signal: AbortSignal;
|
|
70
71
|
}>;
|
|
@@ -119,7 +119,7 @@ async function serveCall(input) {
|
|
|
119
119
|
launch: async (allocated) => {
|
|
120
120
|
if (!input.admissionOpen())
|
|
121
121
|
throw new Error("body closed request admission");
|
|
122
|
-
await input.spawn({
|
|
122
|
+
return await input.spawn({
|
|
123
123
|
paths: allocated.paths,
|
|
124
124
|
seed: {
|
|
125
125
|
id: allocated.id,
|
|
@@ -3,6 +3,7 @@ import type { AkumaPaths } from "./identity.js";
|
|
|
3
3
|
import { type ProviderAdapter, type TurnResult } from "./provider.js";
|
|
4
4
|
import { type RequestChildLaunch, type UpstreamExecutionPort } from "./request-serve.js";
|
|
5
5
|
import { BodySupervisor } from "./body-supervisor.js";
|
|
6
|
+
import type { OwnedProcess } from "../runtime/proc/run.js";
|
|
6
7
|
export declare function turnRecipe(paths: AkumaPaths, soul: Soul): Promise<Readonly<{
|
|
7
8
|
cwd: string;
|
|
8
9
|
options: Soul["options"];
|
|
@@ -24,7 +25,7 @@ export type DriveTurnInput = Readonly<{
|
|
|
24
25
|
call?: string;
|
|
25
26
|
launchTells: readonly TellFact[];
|
|
26
27
|
supervisor: BodySupervisor;
|
|
27
|
-
runtimeSpawn(launch: RequestChildLaunch): Promise<void>;
|
|
28
|
+
runtimeSpawn(launch: RequestChildLaunch): Promise<OwnedProcess | void>;
|
|
28
29
|
upstream?: UpstreamExecutionPort;
|
|
29
30
|
now(): string;
|
|
30
31
|
}>;
|
|
@@ -126,7 +126,7 @@ async function startTurnDrive(input) {
|
|
|
126
126
|
fence: selected.admission.fence,
|
|
127
127
|
deliveredAt: input.now(),
|
|
128
128
|
})));
|
|
129
|
-
void selected.completion.then(() => requests.stopAdmission());
|
|
129
|
+
void selected.completion.then(() => requests.stopAdmission(), () => undefined);
|
|
130
130
|
return {
|
|
131
131
|
turnSequence: turn.sequence,
|
|
132
132
|
drive: selected,
|
|
@@ -194,6 +194,9 @@ function pumpReceipts(writers) {
|
|
|
194
194
|
function observeReceiptFailure(receiptPump) {
|
|
195
195
|
return receiptPump.then(() => new Promise(() => { }), (error) => Promise.reject(error));
|
|
196
196
|
}
|
|
197
|
+
function observeCompletionFailure(drive) {
|
|
198
|
+
return drive.completion.then(() => new Promise(() => { }), (error) => ({ kind: "completion-failed", error }));
|
|
199
|
+
}
|
|
197
200
|
async function submitPendingLiveTells(writers, pending, attempted, tellLive) {
|
|
198
201
|
const { input, turnSequence, drive, writeWitness, mayWrite } = writers;
|
|
199
202
|
for (const tell of pending) {
|
|
@@ -253,6 +256,7 @@ async function consumeTurnDrive(input, active) {
|
|
|
253
256
|
const mayWrite = () => writesOpen && !input.supervisor.signal.aborted;
|
|
254
257
|
const writers = { input, turnSequence, drive, writeWitness, mayWrite };
|
|
255
258
|
const receiptFailure = observeReceiptFailure(pumpReceipts(writers));
|
|
259
|
+
const completionFailure = observeCompletionFailure(drive);
|
|
256
260
|
const iterator = drive.events[Symbol.asyncIterator]();
|
|
257
261
|
let pending = iterator.next();
|
|
258
262
|
let liveTells = true;
|
|
@@ -280,7 +284,12 @@ async function consumeTurnDrive(input, active) {
|
|
|
280
284
|
...(tellObservation === null ? [] : [tellObservation]),
|
|
281
285
|
receiptFailure,
|
|
282
286
|
requests.failure,
|
|
287
|
+
completionFailure,
|
|
283
288
|
]);
|
|
289
|
+
if (next.kind === "completion-failed") {
|
|
290
|
+
requests.stopAdmission();
|
|
291
|
+
throw next.error;
|
|
292
|
+
}
|
|
284
293
|
if (next.kind === "heart") {
|
|
285
294
|
heart = next.observation;
|
|
286
295
|
continue;
|
|
@@ -36,12 +36,23 @@ function streamCursor(stream) {
|
|
|
36
36
|
};
|
|
37
37
|
return { line, exact };
|
|
38
38
|
}
|
|
39
|
-
function
|
|
40
|
-
|
|
39
|
+
function batchChild(repository) {
|
|
40
|
+
return spawn(repository.gitPath, ["cat-file", "--batch"], {
|
|
41
41
|
cwd: repository.effectiveCwd,
|
|
42
42
|
stdio: ["pipe", "pipe", "pipe"],
|
|
43
43
|
windowsHide: true,
|
|
44
44
|
});
|
|
45
|
+
}
|
|
46
|
+
function batchError(child, stderr, message, status) {
|
|
47
|
+
return new GitPlumbingError({
|
|
48
|
+
stderr: Buffer.concat(stderr),
|
|
49
|
+
status,
|
|
50
|
+
message: `git cat-file --batch: ${message}`,
|
|
51
|
+
pid: child.pid ?? null,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function batchObjectReader(repository) {
|
|
55
|
+
const child = batchChild(repository);
|
|
45
56
|
const cursor = streamCursor(child.stdout);
|
|
46
57
|
const cache = new Map();
|
|
47
58
|
const stderr = [];
|
|
@@ -58,12 +69,6 @@ function batchObjectReader(repository) {
|
|
|
58
69
|
child.stdin.on("error", (error) => {
|
|
59
70
|
spawnError ??= error;
|
|
60
71
|
});
|
|
61
|
-
const plumbingError = (message, status) => new GitPlumbingError({
|
|
62
|
-
stderr: Buffer.concat(stderr),
|
|
63
|
-
status,
|
|
64
|
-
message: `git cat-file --batch: ${message}`,
|
|
65
|
-
pid: child.pid ?? null,
|
|
66
|
-
});
|
|
67
72
|
const write = async (value) => {
|
|
68
73
|
if (spawnError !== null)
|
|
69
74
|
throw spawnError;
|
|
@@ -98,7 +103,7 @@ function batchObjectReader(repository) {
|
|
|
98
103
|
throw failure;
|
|
99
104
|
child.kill();
|
|
100
105
|
await closed;
|
|
101
|
-
failure =
|
|
106
|
+
failure = batchError(child, stderr, error instanceof Error ? error.message : String(error), child.exitCode);
|
|
102
107
|
throw failure;
|
|
103
108
|
}
|
|
104
109
|
};
|
|
@@ -126,7 +131,7 @@ function batchObjectReader(repository) {
|
|
|
126
131
|
child.stdin.end();
|
|
127
132
|
const outcome = await closed;
|
|
128
133
|
if (spawnError !== null || outcome.code !== 0) {
|
|
129
|
-
throw
|
|
134
|
+
throw batchError(child, stderr, "git cat-file --batch did not close cleanly", outcome.code);
|
|
130
135
|
}
|
|
131
136
|
};
|
|
132
137
|
return { objects, close };
|
package/build/src/kanshi/read.js
CHANGED
|
@@ -304,34 +304,31 @@ async function readDispatches(observation) {
|
|
|
304
304
|
return { kind: "failed", failure: { message: diagnostic(error) } };
|
|
305
305
|
}
|
|
306
306
|
}
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
const
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
};
|
|
331
|
-
}
|
|
307
|
+
async function observeWithoutRepo(world, region, observedAt, branch) {
|
|
308
|
+
const contracts = { kind: "absent" };
|
|
309
|
+
const holders = { kind: "absent" };
|
|
310
|
+
const observeContract = contractEndpointObserver(contracts);
|
|
311
|
+
const board = await readTaskWorld(world);
|
|
312
|
+
const tasks = world === null ? { kind: "absent" } : readTasks(world, board, holders, observeContract);
|
|
313
|
+
const aliases = world === null ? { kind: "absent" } : await readAliasBindings(world);
|
|
314
|
+
const akuma = world === null ? { kind: "absent" } : await joinAkuma(world, observeContract, [], aliases);
|
|
315
|
+
return {
|
|
316
|
+
report: {
|
|
317
|
+
root: world,
|
|
318
|
+
observedAt,
|
|
319
|
+
branch,
|
|
320
|
+
contracts,
|
|
321
|
+
tasks,
|
|
322
|
+
akuma,
|
|
323
|
+
...(region === undefined ? {} : { region: { kind: "absent" } }),
|
|
324
|
+
},
|
|
325
|
+
aliases,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
async function observeRepo(input) {
|
|
329
|
+
const { world, repo, region, contract, observedAt, branch } = input;
|
|
332
330
|
try {
|
|
333
|
-
|
|
334
|
-
return await withGitDecodeChannel(repository, (channel) => withGitReadObservation(repository, channel, async (observation) => {
|
|
331
|
+
return await withGitDecodeChannel(repo, (channel) => withGitReadObservation(repo, channel, async (observation) => {
|
|
335
332
|
const [contractSection, holders, dispatches, regionSection, board] = await Promise.all([
|
|
336
333
|
readContracts(observation, contract),
|
|
337
334
|
readHolders(observation),
|
|
@@ -385,6 +382,14 @@ export async function observeKanshi(input) {
|
|
|
385
382
|
};
|
|
386
383
|
}
|
|
387
384
|
}
|
|
385
|
+
export async function observeKanshi(input) {
|
|
386
|
+
const observedAt = new Date().toISOString();
|
|
387
|
+
const { world, repo, region, contract } = coordinate(input);
|
|
388
|
+
const branch = await readBranch(repo);
|
|
389
|
+
if (repo === undefined)
|
|
390
|
+
return observeWithoutRepo(world, region, observedAt, branch);
|
|
391
|
+
return observeRepo({ world, repo: scopeForRepo(repo), region, contract, observedAt, branch });
|
|
392
|
+
}
|
|
388
393
|
export async function kanshi(input) {
|
|
389
394
|
return (await observeKanshi(input)).report;
|
|
390
395
|
}
|
|
@@ -120,7 +120,8 @@ export async function executeWaitAkuma(input) {
|
|
|
120
120
|
for (;;) {
|
|
121
121
|
const round = await observeWaitRound(input.path, input.ids, input.signal);
|
|
122
122
|
const settled = round.statuses.map(defaultWaitComplete);
|
|
123
|
-
const completed = round.statuses.length > 0 &&
|
|
123
|
+
const completed = round.statuses.length > 0 &&
|
|
124
|
+
(input.completion === "any" ? settled.some(Boolean) : round.unobserved.length === 0 && settled.every(Boolean));
|
|
124
125
|
if (completed || (deadline !== undefined && performance.now() >= deadline)) {
|
|
125
126
|
return {
|
|
126
127
|
completion: input.completion,
|
|
@@ -128,16 +128,23 @@ async function retainDetachedExitEvidence(log, path, from, code, signal) {
|
|
|
128
128
|
export async function spawnDetachedProcess(input) {
|
|
129
129
|
const log = await open(input.log, "a");
|
|
130
130
|
let launched = false;
|
|
131
|
+
let logClosed = false;
|
|
132
|
+
const closeLog = async () => {
|
|
133
|
+
if (logClosed)
|
|
134
|
+
return;
|
|
135
|
+
logClosed = true;
|
|
136
|
+
await log.close();
|
|
137
|
+
};
|
|
131
138
|
try {
|
|
132
139
|
const from = (await log.stat()).size;
|
|
133
140
|
const child = spawn(input.argv[0], input.argv.slice(1), spawnOptionsFor("retained", input, ["ignore", log.fd, log.fd]));
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
let state = "active";
|
|
142
|
+
let termination;
|
|
143
|
+
const invalidate = () => {
|
|
144
|
+
state = "inert";
|
|
145
|
+
};
|
|
146
|
+
child.once("exit", invalidate);
|
|
147
|
+
child.once("close", invalidate);
|
|
141
148
|
const exited = new Promise((resolve, reject) => {
|
|
142
149
|
child.once("close", (code, signal) => {
|
|
143
150
|
void (async () => {
|
|
@@ -151,7 +158,7 @@ export async function spawnDetachedProcess(input) {
|
|
|
151
158
|
failure = error;
|
|
152
159
|
}
|
|
153
160
|
try {
|
|
154
|
-
await
|
|
161
|
+
await closeLog();
|
|
155
162
|
}
|
|
156
163
|
catch (error) {
|
|
157
164
|
failure ??= error;
|
|
@@ -164,13 +171,13 @@ export async function spawnDetachedProcess(input) {
|
|
|
164
171
|
});
|
|
165
172
|
});
|
|
166
173
|
void exited.catch(() => undefined);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
child.
|
|
174
|
+
await new Promise((resolve, reject) => {
|
|
175
|
+
child.once("spawn", resolve);
|
|
176
|
+
child.once("error", reject);
|
|
177
|
+
});
|
|
178
|
+
if (child.pid === undefined)
|
|
179
|
+
throw new Error("detached process spawned without a pid");
|
|
180
|
+
const pid = child.pid;
|
|
174
181
|
launched = true;
|
|
175
182
|
return {
|
|
176
183
|
pid,
|
|
@@ -194,7 +201,7 @@ export async function spawnDetachedProcess(input) {
|
|
|
194
201
|
}
|
|
195
202
|
finally {
|
|
196
203
|
if (!launched)
|
|
197
|
-
await
|
|
204
|
+
await closeLog();
|
|
198
205
|
}
|
|
199
206
|
}
|
|
200
207
|
function terminalOutcome(code, stdout, stderr, consuming) {
|
|
Binary file
|