@astrosheep/keiyaku 4.6.18 → 4.6.20

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 (103) hide show
  1. package/README.md +7 -3
  2. package/build/plugins/pi/README.md +111 -0
  3. package/build/plugins/pi/akuma-tools.ts +310 -0
  4. package/build/plugins/pi/keiyaku.ts +2 -0
  5. package/build/src/akuma/akuma-call.d.ts +4 -1
  6. package/build/src/akuma/akuma-call.js +24 -7
  7. package/build/src/akuma/akuma-instance.d.ts +28 -8
  8. package/build/src/akuma/akuma-instance.js +85 -42
  9. package/build/src/akuma/akuma-owner.d.ts +18 -9
  10. package/build/src/akuma/akuma-owner.js +31 -25
  11. package/build/src/akuma/akuma-selection-execution.d.ts +4 -1
  12. package/build/src/akuma/akuma-selection-execution.js +8 -4
  13. package/build/src/akuma/akuma.d.ts +17 -0
  14. package/build/src/akuma/body-launch.d.ts +107 -0
  15. package/build/src/akuma/body-launch.js +57 -0
  16. package/build/src/akuma/body-supervisor.d.ts +7 -6
  17. package/build/src/akuma/body-supervisor.js +17 -12
  18. package/build/src/akuma/body.d.ts +13 -94
  19. package/build/src/akuma/body.js +124 -296
  20. package/build/src/akuma/call-input.d.ts +2 -0
  21. package/build/src/akuma/call-input.js +21 -1
  22. package/build/src/akuma/call-request.d.ts +6 -2
  23. package/build/src/akuma/call-request.js +9 -1
  24. package/build/src/akuma/detached-body.d.ts +9 -0
  25. package/build/src/akuma/detached-body.js +94 -0
  26. package/build/src/akuma/execution-instructions.d.ts +8 -0
  27. package/build/src/akuma/execution-instructions.js +17 -0
  28. package/build/src/akuma/heart/facts.d.ts +21 -1
  29. package/build/src/akuma/heart/facts.js +10 -0
  30. package/build/src/akuma/heart/index.d.ts +16 -1
  31. package/build/src/akuma/heart/index.js +95 -30
  32. package/build/src/akuma/heart/rows.d.ts +14 -2
  33. package/build/src/akuma/heart/rows.js +48 -4
  34. package/build/src/akuma/heart/schema.d.ts +4 -1
  35. package/build/src/akuma/heart/schema.js +87 -30
  36. package/build/src/akuma/heart/soul.js +4 -0
  37. package/build/src/akuma/heart/storage.d.ts +1 -0
  38. package/build/src/akuma/heart/storage.js +59 -15
  39. package/build/src/akuma/heart/timeline.d.ts +6 -4
  40. package/build/src/akuma/heart/timeline.js +14 -4
  41. package/build/src/akuma/in-process-body.d.ts +5 -0
  42. package/build/src/akuma/in-process-body.js +57 -0
  43. package/build/src/akuma/index.d.ts +1 -1
  44. package/build/src/akuma/projection.d.ts +44 -1
  45. package/build/src/akuma/projection.js +13 -1
  46. package/build/src/akuma/provider.d.ts +4 -0
  47. package/build/src/akuma/providers/acp/core.js +1 -1
  48. package/build/src/akuma/providers/acp/index.js +3 -0
  49. package/build/src/akuma/providers/claude/index.js +8 -5
  50. package/build/src/akuma/providers/codex-app-server/configuration.d.ts +5 -0
  51. package/build/src/akuma/providers/codex-app-server/configuration.js +72 -0
  52. package/build/src/akuma/providers/codex-app-server/index.js +7 -10
  53. package/build/src/akuma/providers/execution-environment.d.ts +8 -2
  54. package/build/src/akuma/providers/execution-environment.js +10 -1
  55. package/build/src/akuma/providers/grok-build/index.js +23 -16
  56. package/build/src/akuma/providers/index.d.ts +1 -0
  57. package/build/src/akuma/providers/index.js +8 -0
  58. package/build/src/akuma/providers/opencode-sdk/index.js +9 -0
  59. package/build/src/akuma/providers/opencode-sdk/session.d.ts +6 -0
  60. package/build/src/akuma/providers/opencode-sdk/session.js +4 -3
  61. package/build/src/akuma/providers/paseo/index.js +8 -2
  62. package/build/src/akuma/providers/pi/index.js +27 -10
  63. package/build/src/akuma/publication.d.ts +4 -3
  64. package/build/src/akuma/publication.js +21 -18
  65. package/build/src/akuma/selection-observation.d.ts +51 -0
  66. package/build/src/akuma/selection-owner-port.d.ts +2 -1
  67. package/build/src/akuma/selection-owner-port.js +4 -3
  68. package/build/src/akuma/turn-drive.d.ts +4 -2
  69. package/build/src/akuma/turn-drive.js +58 -21
  70. package/build/src/akuma-body.d.ts +0 -10
  71. package/build/src/akuma-body.js +13 -118
  72. package/build/src/body-host.d.ts +28 -0
  73. package/build/src/body-host.js +86 -0
  74. package/build/src/body-requests.d.ts +13 -0
  75. package/build/src/body-requests.js +131 -0
  76. package/build/src/cli/commands/akuma-grammar.d.ts +8 -1
  77. package/build/src/cli/commands/akuma-grammar.js +36 -6
  78. package/build/src/cli/commands/akuma.js +11 -0
  79. package/build/src/cli/commands/report.d.ts +6 -0
  80. package/build/src/cli/commands/report.js +31 -0
  81. package/build/src/cli/parse.js +1 -0
  82. package/build/src/cli/render/akuma-activity.js +27 -17
  83. package/build/src/cli/render/terminal.d.ts +5 -0
  84. package/build/src/cli/render/terminal.js +7 -0
  85. package/build/src/cli/runtime.js +4 -1
  86. package/build/src/cli/square-identity.js +15 -15
  87. package/build/src/index.d.ts +3 -1
  88. package/build/src/index.js +1 -0
  89. package/build/src/library/akuma-creation.d.ts +7 -3
  90. package/build/src/library/akuma-creation.js +23 -11
  91. package/build/src/library/akumas.d.ts +4 -1
  92. package/build/src/library/akumas.js +13 -5
  93. package/build/src/library/selection.d.ts +2 -1
  94. package/build/src/library/selection.js +6 -3
  95. package/build/src/plugin/public.d.ts +11 -0
  96. package/build/src/plugin/runtime.js +1 -0
  97. package/build/src/runtime/proc/windows-launch.exe +0 -0
  98. package/package.json +3 -3
  99. package/plugins/paseo/plugin/server/keiyaku.ts +1 -0
  100. package/plugins/paseo/plugin/server/mirror.ts +2 -2
  101. package/plugins/pi/README.md +111 -0
  102. package/plugins/pi/akuma-tools.ts +310 -0
  103. package/plugins/pi/keiyaku.ts +2 -0
@@ -1,48 +1,22 @@
1
1
  import { projectAkumaBirth } from "./caller.js";
2
- import { z } from "zod";
3
- import { decodeSoulSeed } from "./heart/soul.js";
4
- import { sessionAdmissionSchema } from "./heart/rows.js";
5
- import { appendFile, stat } from "node:fs/promises";
6
- import { fileURLToPath } from "node:url";
7
- import { abortable, abortableDelay } from "./abort.js";
2
+ import { appendFile } from "node:fs/promises";
3
+ import { abortableDelay } from "./abort.js";
8
4
  import { BodySupervisor } from "./body-supervisor.js";
9
- import { driveTurn, turnRecipe } from "./turn-drive.js";
10
- import { HeldAkumaLeash, BodyControlledError, breakBody, decidePendingTellDisposition, drainPendingTells, endTurn, failOpenBoundTurns, finishBodyIfIdle, heartExists, isHeartAbsent, probeLeash, projectTell, readHeart, readLatestTurnForBody, readOpenBoundTurns, readOpenPendingTellDisposition, readTell, readTurn, readTurnParticipants, readNonterminalRequests, resolvePendingTellDisposition, } from "./heart/index.js";
11
- import { akumaPathsSchema, pathsForAkuId, worldRootForAkumaPaths } from "./identity.js";
5
+ import { driveTurn } from "./turn-drive.js";
6
+ import { HeldAkumaLeash, BodyControlledError, breakBody, cancelBodyTurns, decidePendingTellDisposition, drainPendingTells, endTurn, failOpenBoundTurns, finishBodyIfIdle, heartExists, isHeartAbsent, probeLeash, projectTell, readHeart, readLatestTurnForBody, readOpenBoundTurns, readOpenPendingTellDisposition, readTell, readTurn, readTurnParticipants, readNonterminalRequests, resolvePendingTellDisposition, } from "./heart/index.js";
7
+ import { pathsForAkuId, worldRootForAkumaPaths } from "./identity.js";
12
8
  import { recordAkumaLineage } from "./lineage-cache.js";
13
9
  import { admitCallInitialTell, } from "./call-initial-tell.js";
14
- import { akumaExecutionEnvironment } from "./providers/execution-environment.js";
15
10
  import { emitPluginSignal } from "../plugin/akuma-signals.js";
16
11
  import { World } from "../world.js";
17
12
  import { resolveProviderExecution } from "./providers/index.js";
18
13
  import { clearBodyRequestTransport, settleBodyRequests } from "./request-serve.js";
19
- import { spawnDetachedProcess } from "../runtime/proc/run.js";
14
+ import { observeBodyLaunch, launchEndWake, } from "./body-launch.js";
20
15
  const LEASH_RETRY_MS = 100;
21
- const WAKE_REREAD_MS = 100;
22
- export const LEASH_HELD_EXIT = 75;
23
16
  export { CONTROL_RESPONSE_MS } from "./body-supervisor.js";
24
- const launchSeedSchema = z.unknown().transform((value, context) => {
25
- try {
26
- return decodeSoulSeed(value);
27
- }
28
- catch {
29
- context.addIssue({ code: "custom", message: "invalid Body birth seed" });
30
- return z.NEVER;
31
- }
32
- });
33
- export const bodyLaunchSchema = z
34
- .object({
35
- paths: akumaPathsSchema,
36
- seed: launchSeedSchema.optional(),
37
- birthSession: sessionAdmissionSchema.optional(),
38
- initialBody: z.string().optional(),
39
- initiator: z.string().optional(),
40
- birthInitiator: z.string().optional(),
41
- initialSchemaJson: z.string().optional(),
42
- refuseIfHeld: z.boolean().optional(),
43
- completion: z.object({ contractId: z.string().optional() }).strict().optional(),
44
- })
45
- .strict();
17
+ export function tellWakeRuntime(spawn) {
18
+ return { spawn: async (paths) => await spawn({ paths, refuseIfHeld: true }) };
19
+ }
46
20
  function missing(error) {
47
21
  return isHeartAbsent(error) || error.code === "ENOENT";
48
22
  }
@@ -51,14 +25,19 @@ async function failOpenBoundTurnsIfPresent(paths, bodySequence, diagnostic, at)
51
25
  return;
52
26
  await failOpenBoundTurns(paths, { bodySequence, diagnostic, completedAt: at });
53
27
  }
54
- async function putDownIfControlled(paths, supervisor, bodySequence, now) {
55
- if (supervisor.reason === "control" && (await heartExists(paths))) {
56
- await failOpenBoundTurnsIfPresent(paths, bodySequence, "Body put-down with open bound Turns", now());
57
- await breakBody(paths, { sequence: bodySequence, end: "put-down", at: now() });
58
- }
28
+ async function putDownBody(paths, bodySequence, now, turnOutcome, handoff = false) {
29
+ const settled = await cancelBodyTurns(paths, { bodySequence, at: now(), handoff });
30
+ for (const sequence of settled.canceled)
31
+ await turnOutcome?.emit(sequence);
32
+ return settled.disposition ?? undefined;
59
33
  }
60
- async function takeLeash(paths, refuseIfHeld = false) {
34
+ async function putDownIfControlled(paths, supervisor, bodySequence, now, turnOutcome) {
35
+ if ((supervisor.reason === "control" || supervisor.reason === "launch") && (await heartExists(paths)))
36
+ await putDownBody(paths, bodySequence, now, turnOutcome);
37
+ }
38
+ async function takeLeash(paths, refuseIfHeld = false, signal) {
61
39
  for (;;) {
40
+ signal?.throwIfAborted();
62
41
  try {
63
42
  const leash = await HeldAkumaLeash.try(paths);
64
43
  if (leash !== null)
@@ -71,7 +50,7 @@ async function takeLeash(paths, refuseIfHeld = false) {
71
50
  return "absent";
72
51
  throw error;
73
52
  }
74
- await abortableDelay(LEASH_RETRY_MS);
53
+ await abortableDelay(LEASH_RETRY_MS, signal);
75
54
  }
76
55
  }
77
56
  async function bornSoul(launch, leash, now) {
@@ -90,14 +69,6 @@ async function bornSoul(launch, leash, now) {
90
69
  }
91
70
  return { soul: born, created: result === "born" };
92
71
  }
93
- async function launchCwd(launch) {
94
- if (launch.seed !== undefined)
95
- return launch.seed.cwd;
96
- const soul = (await readHeart(launch.paths)).soul;
97
- if (soul === null)
98
- throw new Error("Akuma wake has no born soul");
99
- return (await turnRecipe(launch.paths, soul)).cwd;
100
- }
101
72
  function parseSchemaAnswerJson(raw) {
102
73
  try {
103
74
  JSON.parse(raw);
@@ -114,6 +85,7 @@ async function persistTurn(paths, turnSequence, result, completedAt) {
114
85
  const turn = await readTurn(paths, turnSequence);
115
86
  const schemaJson = turn?.schemaJson;
116
87
  if (result.kind === "answered" && result.session !== undefined) {
88
+ let answerJson;
117
89
  if (schemaJson !== undefined) {
118
90
  const parsed = parseSchemaAnswerJson(result.answer);
119
91
  if (parsed.kind === "invalid") {
@@ -127,18 +99,7 @@ async function persistTurn(paths, turnSequence, result, completedAt) {
127
99
  await endTurn(paths, { turnSequence, outcome, completedAt });
128
100
  return { outcome: "failed", diagnostic: parsed.diagnostic };
129
101
  }
130
- await endTurn(paths, {
131
- turnSequence,
132
- outcome: {
133
- kind: "answered",
134
- session: result.session,
135
- answer: result.answer,
136
- answerJson: parsed.json,
137
- ...(result.historyId === undefined ? {} : { historyId: result.historyId }),
138
- },
139
- completedAt,
140
- });
141
- return { outcome: "answered", answer: result.answer };
102
+ answerJson = parsed.json;
142
103
  }
143
104
  await endTurn(paths, {
144
105
  turnSequence,
@@ -146,11 +107,12 @@ async function persistTurn(paths, turnSequence, result, completedAt) {
146
107
  kind: "answered",
147
108
  session: result.session,
148
109
  answer: result.answer,
110
+ ...(answerJson === undefined ? {} : { answerJson }),
149
111
  ...(result.historyId === undefined ? {} : { historyId: result.historyId }),
150
112
  },
151
113
  completedAt,
152
114
  });
153
- return { outcome: "answered", answer: result.answer };
115
+ return { outcome: "answered" };
154
116
  }
155
117
  const diagnostic = result.kind === "answered" ? "Provider answered without a resumable session" : result.diagnostic;
156
118
  await endTurn(paths, { turnSequence, outcome: { kind: "failed", diagnostic }, completedAt });
@@ -168,110 +130,30 @@ export async function recordPluginDiagnostic(paths, occurrence, error) {
168
130
  /* plugin diagnostic loss never changes Heart truth */
169
131
  }
170
132
  }
171
- const DEFAULT_RUNTIME = {
172
- now: () => new Date().toISOString(),
173
- spawnChild: spawnAkumaBody,
174
- externalCommands: {},
175
- spawnBody: spawnAkumaBody,
176
- };
177
- /**
178
- * The Body spawn executable is the waking process's own executable, resolved
179
- * at use time. There is no recorded path and no re-resolution chain: when the
180
- * file is genuinely gone the spawn fails honestly and the wake diagnostic
181
- * names the missing piece.
182
- */
183
- async function executableMissing(path) {
184
- try {
185
- return !(await stat(path)).isFile();
186
- }
187
- catch {
188
- return true;
189
- }
190
- }
191
- async function frozenCwdMissing(path) {
192
- try {
193
- await stat(path);
194
- return false;
195
- }
196
- catch {
197
- return true;
198
- }
199
- }
200
- /**
201
- * Diagnose an ENOENT Body spawn failure by checking the attempted executable
202
- * and the frozen execution cwd separately. The message names the genuinely
203
- * missing piece(s) with the kill-and-call-fresh remedy; a missing cwd likely
204
- * means its Contract was accepted and the worktree cleaned up. When both
205
- * pieces still exist the original failure passes through unchanged, as does
206
- * any non-ENOENT spawn failure.
207
- */
208
- async function diagnoseSpawnEnoent(input, cause) {
209
- const executable = input.argv[0] ?? process.execPath;
210
- const cwd = input.cwd;
211
- const [noExecutable, noCwd] = await Promise.all([executableMissing(executable), frozenCwdMissing(cwd)]);
212
- if (!noExecutable && !noCwd)
213
- return cause instanceof Error ? cause : new Error(String(cause));
214
- const missing = [];
215
- if (noCwd)
216
- missing.push(`frozen execution directory ${cwd} no longer exists`);
217
- if (noExecutable)
218
- missing.push(`waking process executable ${executable} no longer exists (the host likely removed the running process binary)`);
219
- const remedies = [];
220
- if (noCwd)
221
- remedies.push("a missing execution directory likely means its Contract was accepted and the worktree cleaned up, with deliverables already in Git");
222
- if (noExecutable)
223
- remedies.push("a missing executable likely means the host removed the running process binary: re-enter with a fresh CLI process and retry");
224
- const remedy = `kill this Akuma and call a fresh one; ${remedies.join("; ")}`;
225
- const error = new Error(`Body spawn failed: ${missing.join(" and ")}; ${remedy}`, { cause });
226
- error.code = "ENOENT";
227
- return error;
228
- }
229
- export async function bodyProcessInput(launch, bodyModuleUrl = import.meta.url) {
230
- const encoded = Buffer.from(JSON.stringify(launch), "utf8").toString("base64url");
231
- const actorId = launch.seed?.id ?? (await readHeart(launch.paths)).soul?.id;
232
- if (actorId === undefined)
233
- throw new Error("Akuma wake has no born soul");
234
- const executable = process.execPath;
235
- const source = bodyModuleUrl.endsWith(".ts");
236
- const entry = fileURLToPath(new URL(source ? "../akuma-body.ts" : "../akuma-body.js", bodyModuleUrl));
237
- return {
238
- argv: source ? [executable, "--import", import.meta.resolve("tsx"), entry, encoded] : [executable, entry, encoded],
239
- cwd: await launchCwd(launch),
240
- env: akumaExecutionEnvironment(process.env, {
241
- KEIYAKU_ACTOR_ID: actorId,
242
- KEIYAKU_ACTOR_WORLD: worldRootForAkumaPaths(launch.paths),
243
- }),
244
- log: launch.paths.log,
245
- };
246
- }
247
133
  async function prepareBodyExecution(launch, leash, runtime, adapter) {
134
+ runtime.signal?.throwIfAborted();
248
135
  const birth = await bornSoul(launch, leash, runtime.now());
136
+ runtime.signal?.throwIfAborted();
249
137
  if (birth === null)
250
138
  return null;
251
139
  // The immutable caller edge refreshes after Soul durability, best-effort.
252
140
  if (birth.created)
253
141
  void recordAkumaLineage(runtime.world, birth.soul);
254
142
  const { soul } = birth;
255
- if (!(await prepareBodyStart(launch.paths, leash)))
143
+ if (launch.handoffFromBodySequence === undefined && !(await prepareBodyStart(launch.paths, leash)))
256
144
  return null;
257
145
  if (launch.seed === undefined && launch.initialBody === undefined) {
258
146
  const [heart, requests] = await Promise.all([readHeart(launch.paths), readNonterminalRequests(launch.paths)]);
259
147
  if (heart.pending.length === 0 && requests.length === 0)
260
148
  return null;
261
149
  }
150
+ runtime.signal?.throwIfAborted();
262
151
  const selected = adapter ?? (await resolveProviderExecution(soul.provider)).adapter;
152
+ runtime.signal?.throwIfAborted();
263
153
  return { ...birth, adapter: selected };
264
154
  }
265
155
  async function runPreparedBody(input) {
266
- let pendingTellDisposition;
267
- try {
268
- if (await recoverBodyRequests(input))
269
- pendingTellDisposition = await runBodyTurns(input);
270
- }
271
- finally {
272
- await input.supervisor.close();
273
- }
274
- return pendingTellDisposition;
156
+ return (await recoverBodyRequests(input)) ? await runBodyTurns(input) : undefined;
275
157
  }
276
158
  async function preserveBodyFailure(paths, leash, error, at) {
277
159
  if (!(await heartExists(paths)))
@@ -314,10 +196,25 @@ async function recoverBodyRequests(input) {
314
196
  if (!supervisor.signal.aborted)
315
197
  throw error;
316
198
  await clearBodyRequestTransport(launch.paths);
317
- await putDownIfControlled(launch.paths, supervisor, bodySequence, runtime.now);
199
+ await putDownIfControlled(launch.paths, supervisor, bodySequence, runtime.now, input.turnOutcome);
318
200
  }
319
201
  return false;
320
202
  }
203
+ async function emitBodyBirth(launch, soul, world, bodySequence) {
204
+ await emitPluginSignal({
205
+ world,
206
+ reportDiagnostic: (message) => {
207
+ void recordPluginDiagnostic(launch.paths, "birth", message);
208
+ },
209
+ }, {
210
+ kind: "akuma.born",
211
+ akumaId: soul.id,
212
+ bodySequence,
213
+ ...projectAkumaBirth(soul),
214
+ ...(launch.birthInitiator === undefined ? {} : { initiator: launch.birthInitiator }),
215
+ ...(launch.completion?.contractId === undefined ? {} : { contractId: launch.completion.contractId }),
216
+ });
217
+ }
321
218
  function turnStartedEmitter(launch, soul, world) {
322
219
  const reportDiagnostic = (message) => {
323
220
  void recordPluginDiagnostic(launch.paths, "turn started", message);
@@ -346,11 +243,12 @@ function turnOutcomeEmitter(launch, soul, world) {
346
243
  void recordPluginDiagnostic(launch.paths, "turn outcome", message);
347
244
  };
348
245
  return {
349
- async emit(turnSequence, outcome) {
246
+ async emit(turnSequence) {
350
247
  try {
351
248
  const turn = await readTurn(launch.paths, turnSequence);
352
- if (turn === null)
353
- throw new Error(`Committed Turn ${turnSequence} is missing from Heart`);
249
+ if (turn?.end === undefined)
250
+ throw new Error(`Committed Turn ${turnSequence} outcome is missing from Heart`);
251
+ const outcome = turn.end.outcome;
354
252
  const participants = await readTurnParticipants(launch.paths, turnSequence);
355
253
  await emitPluginSignal({ world, reportDiagnostic }, {
356
254
  kind: "akuma.turn-outcome",
@@ -359,9 +257,11 @@ function turnOutcomeEmitter(launch, soul, world) {
359
257
  turnSequence,
360
258
  ...(turn.initiator === undefined ? {} : { initiator: turn.initiator }),
361
259
  ...(participants.length === 0 ? {} : { participants }),
362
- outcome: outcome.outcome === "answered"
260
+ outcome: outcome.kind === "answered"
363
261
  ? { kind: "answered", text: outcome.answer }
364
- : { kind: "failed", reason: outcome.diagnostic },
262
+ : outcome.kind === "canceled"
263
+ ? { kind: "canceled" }
264
+ : { kind: "failed", reason: outcome.diagnostic },
365
265
  ...(launch.completion?.contractId === undefined ? {} : { contractId: launch.completion.contractId }),
366
266
  });
367
267
  }
@@ -405,7 +305,11 @@ async function admitInitialCallTell(runtime, input) {
405
305
  signal: input.signal,
406
306
  now: runtime.now,
407
307
  wake: async (tell) => await wakeRecordedTell(paths, tell.id, {
408
- spawn: async (tellPaths) => await (runtime.spawnBody ?? spawnAkumaBody)({ paths: tellPaths, refuseIfHeld: true }),
308
+ spawn: async (tellPaths) => await runtime.requestSpawn({
309
+ paths: tellPaths,
310
+ refuseIfHeld: true,
311
+ ...(input.contractId === undefined ? {} : { completion: { contractId: input.contractId } }),
312
+ }),
409
313
  schedule: abortableDelay,
410
314
  }, input.signal),
411
315
  });
@@ -417,7 +321,7 @@ async function runBodyTurns(input) {
417
321
  const control = supervisor.current();
418
322
  const launchTells = drainPendingTells(control.pending, await readOpenBoundTurns(launch.paths, bodySequence), control.pause, bodySequence);
419
323
  if (supervisor.signal.aborted) {
420
- await putDownIfControlled(launch.paths, supervisor, bodySequence, runtime.now);
324
+ await putDownIfControlled(launch.paths, supervisor, bodySequence, runtime.now, turnOutcome);
421
325
  return;
422
326
  }
423
327
  if (initial === undefined && launchTells.length === 0) {
@@ -426,6 +330,7 @@ async function runBodyTurns(input) {
426
330
  return;
427
331
  if (finished.kind === "controlled") {
428
332
  supervisor.cancel("control");
333
+ await putDownIfControlled(launch.paths, supervisor, bodySequence, runtime.now, turnOutcome);
429
334
  return;
430
335
  }
431
336
  continue;
@@ -437,7 +342,7 @@ async function runBodyTurns(input) {
437
342
  adapter,
438
343
  bodySequence,
439
344
  supervisor,
440
- runtimeSpawn: runtime.spawnChild ?? spawnAkumaBody,
345
+ runtimeSpawn: runtime.requestSpawn,
441
346
  admitInitialTell: async (admission) => await admitInitialCallTell(runtime, admission),
442
347
  body: initial ?? "",
443
348
  ...(initial === undefined ? {} : { call: initial }),
@@ -446,6 +351,7 @@ async function runBodyTurns(input) {
446
351
  ...(schema === undefined ? {} : { schemaJson: schema }),
447
352
  world: runtime.world,
448
353
  externalCommands: runtime.externalCommands,
354
+ ...(runtime.executionAssociation === undefined ? {} : { executionAssociation: runtime.executionAssociation }),
449
355
  now: runtime.now,
450
356
  turnStarted,
451
357
  });
@@ -454,18 +360,18 @@ async function runBodyTurns(input) {
454
360
  return;
455
361
  }
456
362
  if (result.kind === "stopped") {
457
- await putDownIfControlled(launch.paths, supervisor, bodySequence, runtime.now);
363
+ await putDownIfControlled(launch.paths, supervisor, bodySequence, runtime.now, turnOutcome);
458
364
  return;
459
365
  }
460
366
  if (result.kind === "handoff")
461
- return "handoff";
367
+ return await putDownBody(launch.paths, bodySequence, runtime.now, turnOutcome, true);
462
368
  if (result.kind === "resume-unsupported") {
463
369
  await failOpenBoundTurnsIfPresent(launch.paths, bodySequence, "resume-unsupported with open bound Turns", runtime.now());
464
370
  await breakBody(launch.paths, { sequence: bodySequence, end: "broke-off", at: runtime.now() });
465
371
  return;
466
372
  }
467
373
  const outcome = await persistTurn(launch.paths, result.turnSequence, result, runtime.now());
468
- await turnOutcome.emit(result.turnSequence, outcome);
374
+ await turnOutcome.emit(result.turnSequence);
469
375
  if (outcome.outcome === "failed") {
470
376
  await failOpenBoundTurnsIfPresent(launch.paths, bodySequence, outcome.diagnostic, runtime.now());
471
377
  await breakBody(launch.paths, { sequence: bodySequence, end: "broke-off", at: runtime.now() });
@@ -493,56 +399,27 @@ async function settleUndeliveredDisposition(paths, disposition, error) {
493
399
  async function consumeProvenDisposition(paths, disposition) {
494
400
  return await resolvePendingTellDisposition(paths, disposition.bodySequence, new Date().toISOString());
495
401
  }
496
- /**
497
- * Wait for Heart custody proof of the frozen Tell-id snapshot, or for the
498
- * spawned child to exit without that proof. Sequence growth, spawn resolution,
499
- * and an unqualified held leash never consume the disposition.
500
- */
501
- async function awaitDispositionCustody(paths, disposition, child, schedule) {
502
- for (;;) {
503
- if (await consumeProvenDisposition(paths, disposition)) {
504
- child.release();
505
- return { kind: "proven" };
506
- }
507
- const timerController = new AbortController();
508
- const timer = schedule(WAKE_REREAD_MS, timerController.signal).then(() => ({ kind: "timer" }));
509
- let winner;
510
- try {
511
- winner = await Promise.race([child.exited.then((exit) => ({ kind: "exited", exit })), timer]);
512
- }
513
- catch (error) {
514
- timerController.abort();
515
- await timer.catch(() => undefined);
516
- throw error;
517
- }
518
- if (winner.kind === "timer")
519
- continue;
520
- timerController.abort();
521
- await timer.catch(() => undefined);
522
- if (await consumeProvenDisposition(paths, disposition)) {
523
- child.release();
524
- return { kind: "proven" };
525
- }
526
- return winner.exit.code === LEASH_HELD_EXIT ? { kind: "held" } : failedChild(winner.exit);
527
- }
528
- }
529
402
  async function resolveDecidedPendingTellDisposition(paths, disposition, spawn) {
530
403
  try {
531
404
  if (await consumeProvenDisposition(paths, disposition))
532
405
  return;
533
- const wake = await awaitDispositionCustody(paths, disposition, await spawn({ paths, refuseIfHeld: true }), abortableDelay);
534
- if (wake.kind === "proven")
406
+ const child = await spawn({ paths, refuseIfHeld: true, handoffFromBodySequence: disposition.bodySequence });
407
+ const result = await observeBodyLaunch(child, async () => (await consumeProvenDisposition(paths, disposition)) ? true : null);
408
+ if (result.kind === "observed") {
409
+ child.release();
535
410
  return;
411
+ }
412
+ const wake = launchEndWake(result.end);
536
413
  // Predecessor still holds the leash: leave the Heart disposition open.
537
414
  if (wake.kind === "held")
538
415
  return;
539
- await settleUndeliveredDisposition(paths, disposition, wake.kind === "failed" ? wake.diagnostic : "successor custody unproven");
416
+ await settleUndeliveredDisposition(paths, disposition, wake.diagnostic);
540
417
  }
541
418
  catch (error) {
542
419
  await settleUndeliveredDisposition(paths, disposition, error);
543
420
  }
544
421
  }
545
- export async function handoffPendingTells(paths, spawn = spawnAkumaBody) {
422
+ export async function handoffPendingTells(paths, spawn) {
546
423
  const open = await readOpenPendingTellDisposition(paths);
547
424
  if (open !== null) {
548
425
  await resolveDecidedPendingTellDisposition(paths, open, spawn);
@@ -565,29 +442,30 @@ export async function handoffPendingTells(paths, spawn = spawnAkumaBody) {
565
442
  return;
566
443
  await resolveDecidedPendingTellDisposition(paths, decided, spawn);
567
444
  }
568
- async function finishBody(paths, bodySequence, handoff, emitBodyEnd, now) {
445
+ async function finishBody(paths, bodySequence, emitBodyEnd, now) {
569
446
  if (bodySequence === undefined || !(await heartExists(paths)))
570
- return { decided: null };
447
+ return null;
571
448
  const body = (await readHeart(paths)).latestBody;
572
- if (body?.sequence === bodySequence && (body.end !== undefined || body.hung !== undefined))
449
+ if (body?.sequence === bodySequence && (body.hung !== undefined || body.end === "broke-off"))
573
450
  await failOpenBoundTurns(paths, {
574
451
  bodySequence,
575
452
  diagnostic: body.hung?.diagnostic ?? `Body ${body.end ?? "ended"} with open bound Turns`,
576
453
  completedAt: now(),
577
454
  });
578
- // A controlled put-down preserves pending Tells for a later ordinary wake.
579
- // Only provider handoff creates this Body's frozen disposition snapshot.
580
- const decided = handoff ? await decidePendingTellDisposition(paths, { bodySequence, at: now(), handoff }) : null;
581
455
  const finalBody = (await readHeart(paths)).latestBody;
582
- let notification;
583
- if (emitBodyEnd && finalBody?.sequence === bodySequence && (finalBody.end || finalBody.hung))
584
- notification = emitBodyEnd(bodySequence, finalBody.hung === undefined ? finalBody.end : "hung", finalBody.hung?.diagnostic);
585
- return { decided, ...(notification === undefined ? {} : { notification }) };
586
- }
587
- export async function driveAkumaBody(launch, adapter, runtimeInput = {}) {
456
+ if (finalBody?.sequence !== bodySequence)
457
+ return {};
458
+ const hung = finalBody.hung !== undefined;
459
+ const notification = emitBodyEnd && (finalBody.end || hung)
460
+ ? emitBodyEnd(bodySequence, hung ? "hung" : finalBody.end, finalBody.hung?.diagnostic)
461
+ : undefined;
462
+ return { ...(notification === undefined ? {} : { notification }), ...(hung ? { hung: true } : {}) };
463
+ }
464
+ export async function driveAkumaBody(launch, adapter, runtimeInput) {
465
+ runtimeInput.signal?.throwIfAborted();
588
466
  const world = runtimeInput.world ?? (await World.at(worldRootForAkumaPaths(launch.paths)));
589
- const runtime = { ...DEFAULT_RUNTIME, ...runtimeInput, world };
590
- const acquired = await takeLeash(launch.paths, launch.refuseIfHeld);
467
+ const runtime = { now: () => new Date().toISOString(), externalCommands: {}, ...runtimeInput, world };
468
+ const acquired = await takeLeash(launch.paths, launch.refuseIfHeld, runtime.signal);
591
469
  if (acquired === "held")
592
470
  return "held";
593
471
  if (acquired === "absent")
@@ -596,8 +474,9 @@ export async function driveAkumaBody(launch, adapter, runtimeInput = {}) {
596
474
  let pendingTellDisposition = undefined;
597
475
  let bodySequence;
598
476
  let emitBodyEnd;
599
- let emitTurnStarted;
600
477
  let emitTurnOutcome;
478
+ let supervisor;
479
+ let hung = false;
601
480
  try {
602
481
  const prepared = await prepareBodyExecution(launch, leash, runtime, adapter);
603
482
  if (prepared === null) {
@@ -606,26 +485,19 @@ export async function driveAkumaBody(launch, adapter, runtimeInput = {}) {
606
485
  return;
607
486
  }
608
487
  emitBodyEnd = bodyEndEmitter(launch, prepared.soul, world);
609
- emitTurnStarted = turnStartedEmitter(launch, prepared.soul, world);
488
+ const emitTurnStarted = turnStartedEmitter(launch, prepared.soul, world);
610
489
  emitTurnOutcome = turnOutcomeEmitter(launch, prepared.soul, world);
611
- const body = await leash.recordBody(launch.paths, { leashTakenAt: runtime.now() });
490
+ runtime.signal?.throwIfAborted();
491
+ const body = await leash.recordBody(launch.paths, {
492
+ leashTakenAt: runtime.now(),
493
+ ...(launch.handoffFromBodySequence === undefined
494
+ ? {}
495
+ : { handoffFromBodySequence: launch.handoffFromBodySequence }),
496
+ });
612
497
  bodySequence = body.sequence;
613
- if (prepared.created) {
614
- await emitPluginSignal({
615
- world,
616
- reportDiagnostic: (message) => {
617
- void recordPluginDiagnostic(launch.paths, "birth", message);
618
- },
619
- }, {
620
- kind: "akuma.born",
621
- akumaId: prepared.soul.id,
622
- bodySequence: body.sequence,
623
- ...projectAkumaBirth(prepared.soul),
624
- ...(launch.birthInitiator === undefined ? {} : { initiator: launch.birthInitiator }),
625
- ...(launch.completion?.contractId === undefined ? {} : { contractId: launch.completion.contractId }),
626
- });
627
- }
628
- const supervisor = await BodySupervisor.open(launch.paths, body.sequence, leash);
498
+ supervisor = await BodySupervisor.open(launch.paths, body.sequence, leash, runtime.signal, runtime.forceSignal);
499
+ if (prepared.created)
500
+ await emitBodyBirth(launch, prepared.soul, world, body.sequence);
629
501
  pendingTellDisposition = await runPreparedBody({
630
502
  launch,
631
503
  soul: prepared.soul,
@@ -640,45 +512,33 @@ export async function driveAkumaBody(launch, adapter, runtimeInput = {}) {
640
512
  catch (error) {
641
513
  if (error instanceof BodyControlledError) {
642
514
  if (bodySequence !== undefined)
643
- await breakBody(launch.paths, { sequence: bodySequence, end: "put-down", at: runtime.now() });
515
+ await putDownBody(launch.paths, bodySequence, runtime.now, emitTurnOutcome);
644
516
  return "held";
645
517
  }
646
518
  await preserveBodyFailure(launch.paths, leash, error, runtime.now());
647
519
  throw error;
648
520
  }
649
521
  finally {
650
- const finished = await finishBody(launch.paths, bodySequence, pendingTellDisposition === "handoff", emitBodyEnd, runtime.now);
651
- leash.release();
652
- if (finished.decided !== null)
653
- await resolveDecidedPendingTellDisposition(launch.paths, finished.decided, runtime.spawnBody ?? spawnAkumaBody);
654
- if (finished.notification !== undefined)
522
+ let finished;
523
+ try {
524
+ await supervisor?.close();
525
+ finished = await finishBody(launch.paths, bodySequence, emitBodyEnd, runtime.now);
526
+ }
527
+ finally {
528
+ leash.release();
529
+ }
530
+ if (finished !== null && pendingTellDisposition !== undefined)
531
+ await resolveDecidedPendingTellDisposition(launch.paths, pendingTellDisposition, runtime.spawn);
532
+ if (finished?.notification !== undefined)
655
533
  await finished.notification;
534
+ hung = finished?.hung === true;
656
535
  }
536
+ if (hung)
537
+ return "hung";
657
538
  }
658
- export async function spawnAkumaBody(launch) {
659
- const input = await bodyProcessInput(launch, import.meta.url);
660
- try {
661
- return await spawnDetachedProcess(input);
662
- }
663
- catch (error) {
664
- if (error?.code !== "ENOENT")
665
- throw error;
666
- throw await diagnoseSpawnEnoent(input, error);
667
- }
668
- }
669
- export async function runAkumaBody(launch, world, externalCommands) {
670
- return await driveAkumaBody(launch, undefined, { world, externalCommands });
671
- }
672
- const DIRECT_TELL_WAKE = {
673
- spawn: async (paths) => await spawnAkumaBody({ paths, refuseIfHeld: true }),
674
- schedule: abortableDelay,
675
- };
676
539
  function diagnostic(error) {
677
540
  return error instanceof Error ? error.message : String(error);
678
541
  }
679
- function preAdmissionDiagnostic(exit) {
680
- return exit.code === null ? `pre-admission signal ${exit.signal ?? "unknown"}` : `pre-admission exit ${exit.code}`;
681
- }
682
542
  async function settledWake(paths, tellId, after) {
683
543
  const heart = await readHeart(paths);
684
544
  if (tellId !== undefined && !heart.pending.some((tell) => tell.id === tellId))
@@ -688,46 +548,14 @@ async function settledWake(paths, tellId, after) {
688
548
  }
689
549
  return null;
690
550
  }
691
- function failedChild(exit) {
692
- return {
693
- kind: "failed",
694
- diagnostic: preAdmissionDiagnostic(exit),
695
- child: { code: exit.code, signal: exit.signal, log: exit.log },
696
- };
697
- }
698
551
  async function awaitWake(paths, input) {
699
552
  const { tellId, after, child, schedule, signal } = input;
700
553
  try {
701
- for (;;) {
702
- signal?.throwIfAborted();
703
- const settled = await settledWake(paths, tellId, after);
704
- if (settled !== null) {
705
- child.release();
706
- return settled;
707
- }
708
- const timerController = new AbortController();
709
- const timer = schedule(WAKE_REREAD_MS, timerController.signal).then(() => ({ kind: "timer" }));
710
- let winner;
711
- try {
712
- const waited = Promise.race([child.exited.then((exit) => ({ kind: "exited", exit })), timer]);
713
- winner = signal === undefined ? await waited : await abortable(waited, signal);
714
- }
715
- catch (error) {
716
- timerController.abort();
717
- await timer.catch(() => undefined);
718
- throw error;
719
- }
720
- if (winner.kind === "timer")
721
- continue;
722
- timerController.abort();
723
- await timer.catch(() => undefined);
724
- const final = await settledWake(paths, tellId, after);
725
- if (final !== null) {
726
- child.release();
727
- return final;
728
- }
729
- return winner.exit.code === LEASH_HELD_EXIT ? { kind: "held" } : failedChild(winner.exit);
730
- }
554
+ const result = await observeBodyLaunch(child, async () => await settledWake(paths, tellId, after), schedule, signal);
555
+ if (result.kind === "completed")
556
+ return launchEndWake(result.end);
557
+ child.release();
558
+ return result.value;
731
559
  }
732
560
  catch (error) {
733
561
  child.release();
@@ -755,7 +583,7 @@ async function wakePendingTells(paths, tellId, runtime, signal) {
755
583
  return { kind: "failed", diagnostic: diagnostic(error) };
756
584
  }
757
585
  }
758
- export async function wakeRecordedTell(paths, tellId, runtime = DIRECT_TELL_WAKE, signal) {
586
+ export async function wakeRecordedTell(paths, tellId, runtime, signal) {
759
587
  const wake = (await wakePendingTells(paths, tellId, runtime, signal));
760
588
  const tell = await readTell(paths, tellId);
761
589
  if (tell === null)
@@ -1 +1,3 @@
1
+ export declare function bareOption(value: unknown): boolean | undefined;
2
+ export declare function writableDirectoriesOption(value: unknown): Promise<readonly string[]>;
1
3
  export declare function canonicalBirthCwd(input: string, diagnostic?: string): Promise<string>;