@astrosheep/keiyaku 4.4.0 → 4.5.1

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.
Files changed (68) hide show
  1. package/build/integrations/marketplace/plugins/keiyaku/.claude-plugin/plugin.json +4 -2
  2. package/build/integrations/marketplace/plugins/keiyaku/.codex-plugin/plugin.json +1 -1
  3. package/build/integrations/marketplace/plugins/keiyaku/package.json +9 -3
  4. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +16 -5
  5. package/build/integrations/pi/keiyaku.ts +159 -0
  6. package/build/src/akuma/abort.js +9 -9
  7. package/build/src/akuma/akuma-handle.d.ts +4 -2
  8. package/build/src/akuma/akuma-handle.js +14 -2
  9. package/build/src/akuma/akuma-product.d.ts +32 -3
  10. package/build/src/akuma/akuma-product.js +44 -20
  11. package/build/src/akuma/akuma.d.ts +0 -1
  12. package/build/src/akuma/body.d.ts +1 -7
  13. package/build/src/akuma/body.js +35 -13
  14. package/build/src/akuma/heart/index.js +7 -3
  15. package/build/src/akuma/heart/rows.d.ts +1 -1
  16. package/build/src/akuma/heart/rows.js +3 -3
  17. package/build/src/akuma/heart/storage.d.ts +1 -0
  18. package/build/src/akuma/heart/storage.js +28 -6
  19. package/build/src/akuma/identity.d.ts +1 -0
  20. package/build/src/akuma/identity.js +7 -0
  21. package/build/src/akuma/index.d.ts +1 -2
  22. package/build/src/akuma/projection.d.ts +18 -2
  23. package/build/src/akuma/projection.js +11 -1
  24. package/build/src/akuma/providers/claude/index.js +68 -37
  25. package/build/src/akuma/publication.d.ts +9 -3
  26. package/build/src/akuma/publication.js +20 -3
  27. package/build/src/akuma/request-lifecycle.d.ts +45 -0
  28. package/build/src/akuma/request-lifecycle.js +171 -0
  29. package/build/src/akuma/request-serve.d.ts +9 -32
  30. package/build/src/akuma/request-serve.js +48 -178
  31. package/build/src/akuma/request-wire.js +1 -1
  32. package/build/src/akuma/requests.js +3 -2
  33. package/build/src/cli/commands/akuma-invoke.d.ts +11 -2
  34. package/build/src/cli/commands/akuma-invoke.js +29 -7
  35. package/build/src/cli/commands/akuma.d.ts +1 -0
  36. package/build/src/cli/commands/akuma.js +16 -6
  37. package/build/src/cli/commands/contract.d.ts +3 -2
  38. package/build/src/cli/commands/contract.js +5 -2
  39. package/build/src/cli/commands/status-set.d.ts +4 -0
  40. package/build/src/cli/commands/status-set.js +61 -0
  41. package/build/src/cli/invoke.js +6 -3
  42. package/build/src/cli/parse.js +1 -1
  43. package/build/src/cli/render/akuma-activity.js +11 -0
  44. package/build/src/cli/render/akuma.js +7 -0
  45. package/build/src/cli/render/status-set.d.ts +5 -0
  46. package/build/src/cli/render/status-set.js +14 -0
  47. package/build/src/cli/render/text.js +3 -0
  48. package/build/src/cli/result.d.ts +15 -1
  49. package/build/src/cli/runtime.js +2 -0
  50. package/build/src/cli/square-edge.d.ts +6 -0
  51. package/build/src/cli/square-edge.js +74 -0
  52. package/build/src/contract-worktree.js +18 -5
  53. package/build/src/library/akuma-creation.d.ts +14 -3
  54. package/build/src/library/akuma-creation.js +31 -18
  55. package/build/src/library/fleet.d.ts +12 -1
  56. package/build/src/library/fleet.js +21 -5
  57. package/build/src/library/repo.js +3 -1
  58. package/build/src/runtime/proc/process-exit.js +6 -1
  59. package/build/src/runtime/proc/run.js +6 -10
  60. package/build/src/runtime/proc/termination.d.ts +1 -0
  61. package/build/src/runtime/proc/termination.js +22 -2
  62. package/build/src/runtime/proc/windows-launch.exe +0 -0
  63. package/build/src/runtime/proc/windows-run.js +10 -7
  64. package/build/src/world.d.ts +2 -0
  65. package/build/src/world.js +4 -0
  66. package/package.json +8 -3
  67. package/build/src/akuma/result-route.d.ts +0 -2
  68. package/build/src/akuma/result-route.js +0 -1
@@ -116,4 +116,4 @@ export declare function sessionFactForCoordinate(database: DatabaseSync, coordin
116
116
  export declare function latestKillFact(database: DatabaseSync): KillFact | null;
117
117
  export declare function killFactForBody(database: DatabaseSync, bodySequence: number): KillFact | null;
118
118
  export declare function lastAnsweredTurnFact(database: DatabaseSync): TurnFact | null;
119
- export declare function answeredTurnFact(database: DatabaseSync, historyId: string): TurnFact | null;
119
+ export declare function answeredTurnFact(database: DatabaseSync, turnSequence: number): TurnFact | null;
@@ -263,10 +263,10 @@ export function lastAnsweredTurnFact(database) {
263
263
  .get();
264
264
  return row === undefined ? null : decodeTurnRow(row);
265
265
  }
266
- export function answeredTurnFact(database, historyId) {
266
+ export function answeredTurnFact(database, turnSequence) {
267
267
  const row = database
268
268
  .prepare(`SELECT sequence, body_sequence, started_at, end_sequence, outcome, history_id,
269
- session_json, answer, diagnostic, completed_at FROM turns WHERE outcome = 'answered' AND history_id = ?`)
270
- .get(historyId);
269
+ session_json, answer, diagnostic, completed_at FROM turns WHERE outcome = 'answered' AND sequence = ?`)
270
+ .get(turnSequence);
271
271
  return row === undefined ? null : decodeTurnRow(row);
272
272
  }
@@ -10,6 +10,7 @@ export declare class HeartAbsentError extends Error {
10
10
  export declare function isHeartAbsent(error: unknown): error is HeartAbsentError;
11
11
  export declare function classifyHeartSchema(paths: AkumaPaths): Promise<"current" | "unsupported">;
12
12
  export declare function withHeart<T>(paths: AkumaPaths, body: (database: DatabaseSync) => T): Promise<T>;
13
+ export declare function withReadOnlyHeart<T>(paths: AkumaPaths, body: (database: DatabaseSync) => T): Promise<T>;
13
14
  export declare function transaction<T>(database: DatabaseSync, body: () => T): T;
14
15
  export declare function readTransaction<T>(database: DatabaseSync, body: () => T): T;
15
16
  export declare function initializeHeart(paths: AkumaPaths): Promise<void>;
@@ -12,7 +12,7 @@ const ACTIVITY_LIMIT = 5_000;
12
12
  const SQLITE_CANTOPEN = 14;
13
13
  export async function watchHeart(paths, signal) {
14
14
  const heart = basename(paths.heart);
15
- const changes = [];
15
+ let changed = false;
16
16
  let failure;
17
17
  let wake;
18
18
  // fs.watch opens the directory synchronously. Its throw is therefore the
@@ -20,7 +20,7 @@ export async function watchHeart(paths, signal) {
20
20
  const watcher = watchDirectory(dirname(paths.heart), { signal }, (_event, filename) => {
21
21
  const name = filename?.toString();
22
22
  if (name === undefined || name === heart || name === `${heart}-wal`) {
23
- changes.push(undefined);
23
+ changed = true;
24
24
  wake?.();
25
25
  }
26
26
  });
@@ -36,8 +36,8 @@ export async function watchHeart(paths, signal) {
36
36
  return (async function* () {
37
37
  try {
38
38
  for (;;) {
39
- if (changes.length > 0) {
40
- changes.shift();
39
+ if (changed) {
40
+ changed = false;
41
41
  yield;
42
42
  continue;
43
43
  }
@@ -82,9 +82,9 @@ function isBusy(error) {
82
82
  message.includes("database is locked") ||
83
83
  message.includes("database is busy"));
84
84
  }
85
- async function openExistingDatabase(path, timeout) {
85
+ async function openExistingDatabase(path, timeout, mode = "rw") {
86
86
  const uri = pathToFileURL(path);
87
- uri.searchParams.set("mode", "rw");
87
+ uri.searchParams.set("mode", mode);
88
88
  try {
89
89
  return timeout === undefined ? new DatabaseSync(uri.href) : new DatabaseSync(uri.href, { timeout });
90
90
  }
@@ -115,6 +115,19 @@ async function openHeart(path, verify = true) {
115
115
  throw error;
116
116
  }
117
117
  }
118
+ async function openHeartReadOnly(path, verify = true) {
119
+ const database = await openExistingDatabase(path, undefined, "ro");
120
+ try {
121
+ database.exec("PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000");
122
+ if (verify)
123
+ assertHeartSchemaVersion(database);
124
+ return database;
125
+ }
126
+ catch (error) {
127
+ database.close();
128
+ throw error;
129
+ }
130
+ }
118
131
  export async function classifyHeartSchema(paths) {
119
132
  let database;
120
133
  try {
@@ -141,6 +154,15 @@ export async function withHeart(paths, body) {
141
154
  database.close();
142
155
  }
143
156
  }
157
+ export async function withReadOnlyHeart(paths, body) {
158
+ const database = await openHeartReadOnly(paths.heart);
159
+ try {
160
+ return body(database);
161
+ }
162
+ finally {
163
+ database.close();
164
+ }
165
+ }
144
166
  export function transaction(database, body) {
145
167
  database.exec("BEGIN IMMEDIATE");
146
168
  try {
@@ -2,6 +2,7 @@ declare const AKU_ID: unique symbol;
2
2
  export type AkuId = string & {
3
3
  readonly [AKU_ID]: true;
4
4
  };
5
+ export declare function parsePublicHistoryId(value: string): number | null;
5
6
  export type AkumaPaths = Readonly<{
6
7
  directory: string;
7
8
  heart: string;
@@ -4,6 +4,13 @@ import { dirname, join } from "node:path";
4
4
  import { identityCoordinate, identitySegments } from "../identity/coordinates.js";
5
5
  import { normalizeIdentityStem } from "../identity/normalize.js";
6
6
  const HEX8 = /^[0-9a-f]{8}$/u;
7
+ export function parsePublicHistoryId(value) {
8
+ const match = /^turn\/([1-9][0-9]*)$/u.exec(value);
9
+ if (match === null)
10
+ return null;
11
+ const sequence = Number(match[1]);
12
+ return Number.isSafeInteger(sequence) ? sequence : null;
13
+ }
7
14
  export function archetypeName(value) {
8
15
  if (value.length === 0 || normalizeIdentityStem({ source: value }) !== value) {
9
16
  throw new TypeError("Akuma name must be one normalized human identity segment");
@@ -2,7 +2,6 @@ import { type AkuId } from "./identity.js";
2
2
  import type { WorldRoot } from "../world.js";
3
3
  export { Akuma, AkumaHandle, AkumaNotBornError, defaultWaitComplete } from "./akuma.js";
4
4
  export type { AkumaCallInput } from "./akuma.js";
5
- export type { ResultRoute } from "./result-route.js";
6
5
  export { ALLOWED_ACTIONS } from "./allowed.js";
7
6
  export type { AllowedAction, AllowedActions } from "./allowed.js";
8
7
  export type { WorldRoot } from "../world.js";
@@ -10,7 +9,7 @@ export declare function probeBornAkuma(worldPath: WorldRoot, id: AkuId): Promise
10
9
  export { AkumaArchetypeError, listArchetypeDefinitions, listArchetypes } from "./archetype.js";
11
10
  export type { ArchetypeCatalogRow } from "./archetype.js";
12
11
  export { AkumaBodyRequestError } from "./requests.js";
13
- export type { AkumaList, AkumaListInput, AkumaListRow, AkumaStatus, ActivityHistory, ActivityRow, ActivitySnapshot, ActivitySnapshotEntry, ForkReceipt, InterruptReceipt, TellResult, TellWake, UnbornAkumaListRow, } from "./akuma.js";
12
+ export type { AkumaList, AkumaListInput, AkumaListRow, AkumaStatus, ActivityHistory, ExactHistory, ActivityRow, ActivitySnapshot, ActivitySnapshotEntry, ForkReceipt, InterruptReceipt, TellResult, TellWake, UnbornAkumaListRow, } from "./akuma.js";
14
13
  export type { ActiveToolRow, ClosedTurn, ClosedTurnRow, CompletedToolRow, HistoryCursor, HistoryPage, IdleSnapshotRow, OpenSnapshotRow, OpenTurn, OpenTurnRow, OutcomeRow, ReportedFileChange, RetainedWindow, Snapshot, SnapshotRow, TellRow, TurnLedger, TurnOutcome, TurnStartRow, UnsettledToolRow, } from "./akuma.js";
15
14
  export type { AgentEvent, ToolCall, ToolEvent, ToolResult } from "./provider.js";
16
15
  export type { AkuId } from "./identity.js";
@@ -1,6 +1,14 @@
1
1
  import { type ToolCall, type ToolResult } from "./provider.js";
2
- import type { TellDelivery, TellFact, TimelineFact, TurnEndFact } from "./heart/index.js";
3
- export type TurnOutcome = TurnEndFact["outcome"];
2
+ import type { TellDelivery, TellFact, TimelineFact } from "./heart/index.js";
3
+ export type TurnOutcome = Readonly<{
4
+ kind: "answered";
5
+ historyId: string;
6
+ answer: string;
7
+ }> | Readonly<{
8
+ kind: "failed";
9
+ historyId: string;
10
+ diagnostic: string;
11
+ }>;
4
12
  export type TurnStartRow = Readonly<{
5
13
  kind: "turn";
6
14
  sequence: number;
@@ -153,7 +161,15 @@ export type ActivityHistory = Readonly<{
153
161
  lowestRetained: number | null;
154
162
  highest: number | null;
155
163
  }>;
164
+ export type ExactHistory = Readonly<{
165
+ kind: "exact";
166
+ outcome: OutcomeRow;
167
+ }> | Readonly<{
168
+ kind: "unknown-history";
169
+ id: string;
170
+ }>;
156
171
  export type HistoryPage = ActivityHistory;
172
+ export declare function selectExactHistory(rows: readonly ActivityRow[], id: string): ExactHistory;
157
173
  export type HistoryCursor = Readonly<{
158
174
  before?: number;
159
175
  since?: number;
@@ -1,4 +1,14 @@
1
1
  import { decodeAgentEvent } from "./provider.js";
2
+ function publicOutcome(turnSequence, outcome) {
3
+ const historyId = `turn/${turnSequence}`;
4
+ return outcome.kind === "answered"
5
+ ? { kind: "answered", historyId, answer: outcome.answer }
6
+ : { kind: "failed", historyId, diagnostic: outcome.diagnostic };
7
+ }
8
+ export function selectExactHistory(rows, id) {
9
+ const outcome = rows.find((row) => row.kind === "outcome" && row.outcome.historyId === id);
10
+ return outcome === undefined ? { kind: "unknown-history", id } : { kind: "exact", outcome };
11
+ }
2
12
  function duration(startedAt, completedAt) {
3
13
  const start = Date.parse(startedAt);
4
14
  const end = Date.parse(completedAt);
@@ -171,7 +181,7 @@ export function projectTurns(facts, retained = {
171
181
  sequence: fact.sequence,
172
182
  turnSequence: fact.turnSequence,
173
183
  at: fact.completedAt,
174
- outcome: fact.outcome,
184
+ outcome: publicOutcome(fact.turnSequence, fact.outcome),
175
185
  };
176
186
  const turn = state.turnsBySequence.get(fact.turnSequence);
177
187
  if (turn === undefined)
@@ -118,53 +118,64 @@ function finishClaudeInput(input, terminal) {
118
118
  else
119
119
  input.close();
120
120
  }
121
+ function consumeClaudeMessage(context, state, message) {
122
+ const { input, events, observation, progress, admit, rejectAdmission, settle, controls } = context;
123
+ if (state.terminal !== null)
124
+ return;
125
+ if (!state.admitted && "session_id" in message && typeof message.session_id === "string" && admit()) {
126
+ state.admitted = true;
127
+ events.emit({ type: "session", coordinate: { sessionId: message.session_id } });
128
+ }
129
+ emitClaudeMessage(message, events, observation);
130
+ if (message.type === "assistant" &&
131
+ message.parent_tool_use_id === null &&
132
+ typeof message.uuid === "string" &&
133
+ message.uuid.length > 0)
134
+ state.historyId = message.uuid;
135
+ if (message.type !== "result")
136
+ return;
137
+ if (message.subtype === "success") {
138
+ state.terminal = successfulResult(message, state.historyId);
139
+ progress.checkpoint += 1;
140
+ controls.flushReceipts();
141
+ queueMicrotask(controls.closeWhenIdle);
142
+ }
143
+ else {
144
+ state.terminal = { kind: "failed", diagnostic: message.errors.join("; ") || message.subtype };
145
+ input.close();
146
+ }
147
+ progress.ended = true;
148
+ if (!state.admitted)
149
+ rejectAdmission(new Error(state.terminal.kind === "failed" ? state.terminal.diagnostic : "Claude query ended before session admission"));
150
+ finishClaudeInput(input, state.terminal);
151
+ events.end();
152
+ settle(state.terminal);
153
+ controls.settleReceipts();
154
+ }
121
155
  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;
156
+ const { query, input, events, progress, rejectAdmission, settle, controls } = context;
157
+ const state = { admitted: false, terminal: null };
126
158
  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
- }
159
+ for await (const message of query)
160
+ consumeClaudeMessage(context, state, message);
152
161
  progress.ended = true;
153
- const result = terminal ?? { kind: "failed", diagnostic: "Claude query ended without a result" };
154
- if (!admitted)
162
+ const result = state.terminal ?? { kind: "failed", diagnostic: "Claude query ended without a result" };
163
+ if (!state.admitted)
155
164
  rejectAdmission(new Error(result.kind === "failed" ? result.diagnostic : "Claude query ended before session admission"));
156
165
  settle(result);
157
166
  }
158
167
  catch (error) {
159
168
  progress.ended = true;
169
+ if (state.terminal !== null)
170
+ return;
160
171
  input.fail(error);
161
172
  const failure = error instanceof Error ? error : new Error(String(error));
162
- if (!admitted)
173
+ if (!state.admitted)
163
174
  rejectAdmission(failure);
164
175
  settle({ kind: "failed", diagnostic: failure.message });
165
176
  }
166
177
  finally {
167
- finishClaudeInput(input, terminal);
178
+ finishClaudeInput(input, state.terminal);
168
179
  events.end();
169
180
  controls.settleReceipts();
170
181
  }
@@ -206,9 +217,21 @@ function observeClaudeQuery(context) {
206
217
  const observation = { tools: new Map() };
207
218
  let admit;
208
219
  let rejectAdmission;
220
+ let admissionOpen = true;
209
221
  const admission = new Promise((resolve, reject) => {
210
- admit = resolve;
211
- rejectAdmission = reject;
222
+ admit = () => {
223
+ if (!admissionOpen)
224
+ return false;
225
+ admissionOpen = false;
226
+ resolve();
227
+ return true;
228
+ };
229
+ rejectAdmission = (error) => {
230
+ if (!admissionOpen)
231
+ return;
232
+ admissionOpen = false;
233
+ reject(error);
234
+ };
212
235
  });
213
236
  let settle;
214
237
  const completion = new Promise((resolve) => {
@@ -247,6 +270,7 @@ function observeClaudeQuery(context) {
247
270
  });
248
271
  return {
249
272
  admission,
273
+ rejectAdmission,
250
274
  completion,
251
275
  get ended() {
252
276
  return progress.ended;
@@ -282,9 +306,13 @@ async function driveClaude(load, execution, drive) {
282
306
  input.fail(error);
283
307
  query.close();
284
308
  };
285
- const abortSetup = () => shutDown(signal.reason);
309
+ let observed;
310
+ const abortSetup = () => {
311
+ shutDown(signal.reason);
312
+ observed?.rejectAdmission(signal.reason);
313
+ };
286
314
  signal.addEventListener("abort", abortSetup, { once: true });
287
- const observed = observeClaudeQuery({
315
+ observed = observeClaudeQuery({
288
316
  query,
289
317
  input,
290
318
  events,
@@ -296,8 +324,11 @@ async function driveClaude(load, execution, drive) {
296
324
  },
297
325
  },
298
326
  });
327
+ if (signal.aborted)
328
+ abortSetup();
299
329
  try {
300
330
  await Promise.all([launchAcknowledged, observed.admission]);
331
+ signal.throwIfAborted();
301
332
  }
302
333
  finally {
303
334
  signal.removeEventListener("abort", abortSetup);
@@ -1,11 +1,17 @@
1
1
  import { type AllocatedAkuma } from "./identity.js";
2
2
  import type { OwnedProcess } from "../runtime/proc/run.js";
3
3
  export declare const BIRTH_TIMEOUT_MS = 30000;
4
- export declare function publishAkuma(input: Readonly<{
4
+ export type BirthInput = Readonly<{
5
5
  worldPath: string;
6
6
  archetype: string;
7
+ signal?: AbortSignal;
8
+ }>;
9
+ export type LaunchInput = Readonly<{
10
+ allocated: AllocatedAkuma;
7
11
  awaitAsleep?: boolean;
8
- reserve?(allocated: AllocatedAkuma): Promise<void>;
9
12
  launch(allocated: AllocatedAkuma): Promise<OwnedProcess | void>;
10
13
  signal?: AbortSignal;
11
- }>): Promise<AllocatedAkuma>;
14
+ }>;
15
+ export declare function birthAkuma(input: BirthInput): Promise<AllocatedAkuma>;
16
+ export declare function launchAkuma(input: LaunchInput): Promise<AllocatedAkuma>;
17
+ export declare function publishAkuma(input: BirthInput & Omit<LaunchInput, "allocated">): Promise<AllocatedAkuma>;
@@ -93,6 +93,9 @@ async function awaitBirth(paths, owned, signal) {
93
93
  const settled = await settleTimedOutBirth(paths);
94
94
  if (settled !== null)
95
95
  return settled;
96
+ if (owned !== undefined)
97
+ await owned.terminate(true);
98
+ throw new Error("Akuma birth timed out before Soul admission");
96
99
  }
97
100
  if (owned === undefined) {
98
101
  await abortableDelay(Math.min(POLL_MS, Math.max(0, deadline - performance.now())), signal);
@@ -156,15 +159,25 @@ async function sealLocalFailure(allocated, error) {
156
159
  /* the original local publication failure remains authoritative */
157
160
  }
158
161
  }
159
- export async function publishAkuma(input) {
162
+ export async function birthAkuma(input) {
160
163
  input.signal?.throwIfAborted();
161
164
  const allocated = await allocateAkumaDirectory({ worldRoot: input.worldPath, archetype: input.archetype });
162
165
  try {
163
166
  await initializeHeart(allocated.paths);
164
167
  input.signal?.throwIfAborted();
165
- await input.reserve?.(allocated);
168
+ return allocated;
169
+ }
170
+ catch (error) {
171
+ await sealLocalFailure(allocated, error);
172
+ throw error;
173
+ }
174
+ }
175
+ export async function launchAkuma(input) {
176
+ const { allocated } = input;
177
+ let owned;
178
+ try {
166
179
  input.signal?.throwIfAborted();
167
- const owned = await input.launch(allocated);
180
+ owned = await input.launch(allocated);
168
181
  try {
169
182
  input.signal?.throwIfAborted();
170
183
  const soul = await awaitBirth(allocated.paths, owned ?? undefined, input.signal);
@@ -183,3 +196,7 @@ export async function publishAkuma(input) {
183
196
  throw error;
184
197
  }
185
198
  }
199
+ export async function publishAkuma(input) {
200
+ const allocated = await birthAkuma(input);
201
+ return await launchAkuma({ ...input, allocated });
202
+ }
@@ -0,0 +1,45 @@
1
+ import { type Soul } from "./heart/index.js";
2
+ import { type AkumaPaths } from "./identity.js";
3
+ import { type StructuralRequestClaim } from "./request-wire.js";
4
+ import type { OwnedProcess } from "../runtime/proc/run.js";
5
+ export type RequestChildLaunch = Readonly<{
6
+ paths: AkumaPaths;
7
+ seed: Omit<Soul, "createdAt">;
8
+ initialBody: string;
9
+ }>;
10
+ export type PumpInput = Readonly<{
11
+ paths: AkumaPaths;
12
+ parent: Soul;
13
+ bodySequence: number;
14
+ now(): string;
15
+ spawn(launch: RequestChildLaunch): Promise<OwnedProcess | void>;
16
+ upstream?: unknown;
17
+ signal: AbortSignal;
18
+ }>;
19
+ type ServeClaim = (input: PumpInput & Readonly<{
20
+ directory: string;
21
+ transportId: string;
22
+ claim: StructuralRequestClaim;
23
+ signal: AbortSignal;
24
+ admissionOpen(): boolean;
25
+ }>) => Promise<void>;
26
+ export declare class BodyRequestPump {
27
+ private readonly input;
28
+ private readonly serveClaim;
29
+ readonly directory: string;
30
+ readonly failure: Promise<never>;
31
+ private readonly closeSignal;
32
+ private readonly executionSignal;
33
+ private readonly handled;
34
+ private readonly running;
35
+ private readonly cancelBody;
36
+ private admissionClosed;
37
+ protected constructor(input: PumpInput, serveClaim: ServeClaim);
38
+ static openWithService(input: PumpInput, serveClaim: ServeClaim): Promise<BodyRequestPump>;
39
+ private run;
40
+ close(): Promise<void>;
41
+ stopAdmission(): void;
42
+ }
43
+ export declare function clearBodyRequestTransport(paths: AkumaPaths): Promise<void>;
44
+ export declare function settleBodyRequests(paths: AkumaPaths, parent: Soul, now: () => string, signal?: AbortSignal): Promise<"settled" | "pending">;
45
+ export {};
@@ -0,0 +1,171 @@
1
+ import { access, mkdir, readFile, readdir, rm } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { abortableDelay } from "./abort.js";
4
+ import { HeldAkumaLeash, readNonterminalRequests, readSoul, serveRequest, voidRequest, } from "./heart/index.js";
5
+ import { pathsForAkuId } from "./identity.js";
6
+ import { BIRTH_TIMEOUT_MS } from "./publication.js";
7
+ import { atomicJson, decodeClaim, receiptPath } from "./request-wire.js";
8
+ const POLL_MS = 100;
9
+ export class BodyRequestPump {
10
+ input;
11
+ serveClaim;
12
+ directory;
13
+ failure;
14
+ closeSignal = new AbortController();
15
+ executionSignal = new AbortController();
16
+ handled = new Set();
17
+ running;
18
+ cancelBody;
19
+ admissionClosed = false;
20
+ constructor(input, serveClaim) {
21
+ this.input = input;
22
+ this.serveClaim = serveClaim;
23
+ this.directory = join(input.paths.directory, "requests", String(input.bodySequence));
24
+ this.cancelBody = () => {
25
+ this.stopAdmission();
26
+ if (!this.executionSignal.signal.aborted)
27
+ this.executionSignal.abort(input.signal.reason);
28
+ };
29
+ input.signal.addEventListener("abort", this.cancelBody, { once: true });
30
+ if (input.signal.aborted)
31
+ this.cancelBody();
32
+ this.running = this.run();
33
+ this.failure = this.running.then(() => new Promise(() => { }), (error) => Promise.reject(error));
34
+ }
35
+ static async openWithService(input, serveClaim) {
36
+ await mkdir(join(input.paths.directory, "requests", String(input.bodySequence)), { recursive: true });
37
+ return new BodyRequestPump(input, serveClaim);
38
+ }
39
+ async run() {
40
+ while (!this.closeSignal.signal.aborted) {
41
+ for (const name of await requestFiles(this.directory)) {
42
+ if (this.closeSignal.signal.aborted)
43
+ return;
44
+ const transportId = name.slice(0, -".request.json".length);
45
+ if (this.handled.has(transportId))
46
+ continue;
47
+ const path = join(this.directory, name);
48
+ const claim = decodeClaim(await readFile(path, "utf8"), transportId);
49
+ if (claim === null) {
50
+ await rm(path, { force: true });
51
+ this.handled.add(transportId);
52
+ continue;
53
+ }
54
+ if (!this.admissionClosed) {
55
+ try {
56
+ await this.serveClaim({
57
+ directory: this.directory,
58
+ transportId,
59
+ claim,
60
+ ...this.input,
61
+ signal: this.executionSignal.signal,
62
+ admissionOpen: () => !this.admissionClosed,
63
+ });
64
+ }
65
+ catch (error) {
66
+ await projectTransportFailure(this.directory, transportId, claim, error);
67
+ }
68
+ }
69
+ this.handled.add(transportId);
70
+ }
71
+ try {
72
+ await abortableDelay(POLL_MS, this.closeSignal.signal);
73
+ }
74
+ catch (error) {
75
+ if (this.closeSignal.signal.aborted)
76
+ return;
77
+ throw error;
78
+ }
79
+ }
80
+ }
81
+ async close() {
82
+ this.stopAdmission();
83
+ try {
84
+ await this.running;
85
+ }
86
+ finally {
87
+ this.input.signal.removeEventListener("abort", this.cancelBody);
88
+ await rm(this.directory, { recursive: true, force: true });
89
+ }
90
+ }
91
+ stopAdmission() {
92
+ this.admissionClosed = true;
93
+ if (!this.closeSignal.signal.aborted)
94
+ this.closeSignal.abort(new Error("Body request pump closed"));
95
+ }
96
+ }
97
+ export async function clearBodyRequestTransport(paths) {
98
+ await rm(join(paths.directory, "requests"), { recursive: true, force: true });
99
+ }
100
+ async function requestFiles(directory) {
101
+ try {
102
+ return (await readdir(directory)).filter((name) => name.endsWith(".request.json")).sort();
103
+ }
104
+ catch (error) {
105
+ if (error.code === "ENOENT")
106
+ return [];
107
+ throw error;
108
+ }
109
+ }
110
+ async function projectTransportFailure(directory, transportId, claim, error) {
111
+ await atomicJson(receiptPath(directory, transportId), {
112
+ id: claim.id,
113
+ action: claim.action,
114
+ state: "refused",
115
+ diagnostic: error instanceof Error ? error.message : String(error),
116
+ });
117
+ }
118
+ function matchingRequestOrigin(soul, parent, id) {
119
+ return soul.origin.kind === "request" && soul.origin.parent === parent && soul.origin.requestId === id;
120
+ }
121
+ async function settleReservedSoul(paths, parent, request, soul) {
122
+ await (matchingRequestOrigin(soul, parent.id, request.id)
123
+ ? serveRequest(paths, request.id, request.child)
124
+ : voidRequest(paths, request.id, "reserved child origin does not match the request"));
125
+ }
126
+ async function settleReserved(paths, parent, request, now, signal) {
127
+ const childPaths = pathsForAkuId(request.world, request.child);
128
+ const deadline = performance.now() + BIRTH_TIMEOUT_MS;
129
+ for (;;) {
130
+ signal?.throwIfAborted();
131
+ if (!(await access(childPaths.directory).then(() => true, () => false))) {
132
+ await voidRequest(paths, request.id, "reserved child directory is absent");
133
+ return true;
134
+ }
135
+ const soul = await readSoul(childPaths);
136
+ if (soul !== null) {
137
+ await settleReservedSoul(paths, parent, request, soul);
138
+ return true;
139
+ }
140
+ const leash = await HeldAkumaLeash.try(childPaths);
141
+ if (leash !== null) {
142
+ try {
143
+ const settled = await readSoul(childPaths);
144
+ if (settled !== null)
145
+ await settleReservedSoul(paths, parent, request, settled);
146
+ else {
147
+ await leash.sealIfUnborn(childPaths, { evidence: "request settlement", at: now() });
148
+ await voidRequest(paths, request.id, "reserved child was sealed unborn");
149
+ }
150
+ }
151
+ finally {
152
+ leash.release();
153
+ }
154
+ return true;
155
+ }
156
+ if (performance.now() >= deadline)
157
+ return false;
158
+ await abortableDelay(Math.min(POLL_MS, Math.max(0, deadline - performance.now())), signal);
159
+ }
160
+ }
161
+ export async function settleBodyRequests(paths, parent, now, signal) {
162
+ let pending = false;
163
+ for (const request of await readNonterminalRequests(paths)) {
164
+ signal?.throwIfAborted();
165
+ if (request.state === "admitted")
166
+ await voidRequest(paths, request.id, "body died before serving the request");
167
+ else if (request.state === "reserved" && !(await settleReserved(paths, parent, request, now, signal)))
168
+ pending = true;
169
+ }
170
+ return pending ? "pending" : "settled";
171
+ }