@astrosheep/keiyaku 4.1.2 → 4.1.3

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.
@@ -220,7 +220,7 @@ export class AkumaHandle {
220
220
  archetype: source.archetype,
221
221
  awaitAsleep: true,
222
222
  launch: async (allocated) => {
223
- (await spawnAkumaBody({
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
- })).release();
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) => (await spawnAkumaBody({
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
- })).release(),
105
+ }),
106
106
  });
107
107
  return new AkumaHandle(published.id, this.path, {
108
108
  cwd,
@@ -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: source ? [process.execPath, "--import", "tsx", entry, encoded] : [process.execPath, entry, encoded],
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 {
@@ -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 (held.length !== entries.length) {
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 { query, input, events, receipts, accepted, state } = context;
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
- let checkpoint = 0, ended = false;
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 (async () => {
157
- let admitted = false;
158
- let terminal = null;
159
- let historyId;
160
- try {
161
- for await (const message of query) {
162
- if (!admitted && "session_id" in message && typeof message.session_id === "string") {
163
- admitted = true;
164
- events.emit({ type: "session", coordinate: { sessionId: message.session_id } });
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
- let submission = 0;
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(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
- async function startCodex(execution, input) {
128
- const signal = input.signal ?? new AbortController().signal;
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 abortable(loadOpencode({
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.client.session;
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 state = { answer: "", assistantSeen: false, tools: new Map() };
99
- let terminalFailure = null;
100
- let disposed = false;
101
- let abortRequest;
102
- let aborting = false;
103
- let settled = false;
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, state))
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 (async () => {
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
- async function awaitBirth(paths, signal) {
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
- await abortableDelay(Math.min(POLL_MS, Math.max(0, deadline - performance.now())), signal);
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
- input.signal?.throwIfAborted();
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
- export declare function executeRequest(input: ServeInput, request: UpstreamFact): Promise<Readonly<{
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
- result: await input.upstream.tell({
17
- target: request.target,
18
- body: request.body,
19
- tellId: request.id,
20
- recordedAt: request.admittedAt,
21
- signal: input.signal,
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 batchObjectReader(repository) {
40
- const child = spawn(repository.gitPath, ["cat-file", "--batch"], {
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 = plumbingError(error instanceof Error ? error.message : String(error), child.exitCode);
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 plumbingError("git cat-file --batch did not close cleanly", outcome.code);
134
+ throw batchError(child, stderr, "git cat-file --batch did not close cleanly", outcome.code);
130
135
  }
131
136
  };
132
137
  return { objects, close };
@@ -304,34 +304,31 @@ async function readDispatches(observation) {
304
304
  return { kind: "failed", failure: { message: diagnostic(error) } };
305
305
  }
306
306
  }
307
- export async function observeKanshi(input) {
308
- const observedAt = new Date().toISOString();
309
- const { world, repo, region, contract } = coordinate(input);
310
- const branch = await readBranch(repo);
311
- if (repo === undefined) {
312
- const contracts = { kind: "absent" };
313
- const holders = { kind: "absent" };
314
- const observeContract = contractEndpointObserver(contracts);
315
- const board = await readTaskWorld(world);
316
- const tasks = world === null ? { kind: "absent" } : readTasks(world, board, holders, observeContract);
317
- const aliases = world === null ? { kind: "absent" } : await readAliasBindings(world);
318
- const akuma = world === null ? { kind: "absent" } : await joinAkuma(world, observeContract, [], aliases);
319
- return {
320
- report: {
321
- root: world,
322
- observedAt,
323
- branch,
324
- contracts,
325
- tasks,
326
- akuma,
327
- ...(region === undefined ? {} : { region: { kind: "absent" } }),
328
- },
329
- aliases,
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
- const repository = scopeForRepo(repo);
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 && (input.completion === "any" ? settled.some(Boolean) : settled.every(Boolean));
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/keiyaku",
3
- "version": "4.1.2",
3
+ "version": "4.1.3",
4
4
  "files": [
5
5
  "build"
6
6
  ],