@astrosheep/keiyaku 4.1.1 → 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.
Files changed (75) hide show
  1. package/build/src/akuma/akuma-errors.d.ts +6 -0
  2. package/build/src/akuma/akuma-errors.js +9 -0
  3. package/build/src/akuma/akuma-handle.d.ts +39 -0
  4. package/build/src/akuma/akuma-handle.js +263 -0
  5. package/build/src/akuma/akuma-observe.d.ts +19 -0
  6. package/build/src/akuma/akuma-observe.js +74 -0
  7. package/build/src/akuma/akuma-product-symbols.d.ts +1 -0
  8. package/build/src/akuma/akuma-product-symbols.js +1 -0
  9. package/build/src/akuma/akuma-product.d.ts +18 -0
  10. package/build/src/akuma/akuma-product.js +154 -0
  11. package/build/src/akuma/akuma.d.ts +23 -70
  12. package/build/src/akuma/akuma.js +17 -433
  13. package/build/src/akuma/body-supervisor.d.ts +26 -0
  14. package/build/src/akuma/body-supervisor.js +89 -0
  15. package/build/src/akuma/body.d.ts +1 -1
  16. package/build/src/akuma/body.js +7 -3
  17. package/build/src/akuma/heart/index.d.ts +1 -1
  18. package/build/src/akuma/heart/index.js +1 -1
  19. package/build/src/akuma/heart/schema.d.ts +1 -0
  20. package/build/src/akuma/heart/schema.js +9 -0
  21. package/build/src/akuma/heart/storage.d.ts +1 -0
  22. package/build/src/akuma/heart/storage.js +18 -1
  23. package/build/src/akuma/nuke.js +6 -2
  24. package/build/src/akuma/projection-read.d.ts +17 -0
  25. package/build/src/akuma/projection-read.js +135 -0
  26. package/build/src/akuma/projection.d.ts +1 -18
  27. package/build/src/akuma/projection.js +21 -155
  28. package/build/src/akuma/providers/claude/index.js +101 -89
  29. package/build/src/akuma/providers/codex-app-server/index.js +9 -6
  30. package/build/src/akuma/providers/opencode-sdk/index.js +22 -15
  31. package/build/src/akuma/providers/pi/index.js +45 -43
  32. package/build/src/akuma/publication.d.ts +2 -1
  33. package/build/src/akuma/publication.js +72 -11
  34. package/build/src/akuma/request-execution.d.ts +8 -0
  35. package/build/src/akuma/request-execution.js +98 -0
  36. package/build/src/akuma/request-serve.d.ts +14 -2
  37. package/build/src/akuma/request-serve.js +3 -103
  38. package/build/src/akuma/{body-turn.d.ts → turn-drive.d.ts} +4 -26
  39. package/build/src/akuma/{body-turn.js → turn-drive.js} +14 -100
  40. package/build/src/cli/commands/akuma.d.ts +0 -6
  41. package/build/src/cli/commands/akuma.js +1 -11
  42. package/build/src/cli/commands/contract-invoke.d.ts +25 -0
  43. package/build/src/cli/commands/contract-invoke.js +193 -0
  44. package/build/src/cli/commands/contract.d.ts +12 -0
  45. package/build/src/cli/commands/contract.js +229 -0
  46. package/build/src/cli/invoke.js +24 -184
  47. package/build/src/cli/parse.d.ts +1 -1
  48. package/build/src/cli/parse.js +8 -222
  49. package/build/src/cli/render/akuma-activity.d.ts +38 -0
  50. package/build/src/cli/render/akuma-activity.js +362 -0
  51. package/build/src/cli/render/akuma.d.ts +1 -1
  52. package/build/src/cli/render/akuma.js +14 -371
  53. package/build/src/cli/render/contract-history.d.ts +2 -0
  54. package/build/src/cli/render/contract-history.js +83 -0
  55. package/build/src/cli/render/contract.d.ts +1 -2
  56. package/build/src/cli/render/contract.js +1 -83
  57. package/build/src/git/read-observation.js +15 -10
  58. package/build/src/git/reconcile.d.ts +8 -1
  59. package/build/src/git/reconcile.js +13 -301
  60. package/build/src/git/scratch.d.ts +8 -5
  61. package/build/src/git/scratch.js +29 -2
  62. package/build/src/git/terminal-reconcile.d.ts +21 -0
  63. package/build/src/git/terminal-reconcile.js +278 -0
  64. package/build/src/kanshi/read.js +32 -27
  65. package/build/src/library/contract-bind.d.ts +26 -0
  66. package/build/src/library/contract-bind.js +106 -0
  67. package/build/src/library/contract-operations.d.ts +77 -0
  68. package/build/src/library/contract-operations.js +130 -0
  69. package/build/src/library/contract-types.d.ts +95 -0
  70. package/build/src/library/contract-types.js +1 -0
  71. package/build/src/library/contract.d.ts +11 -136
  72. package/build/src/library/contract.js +9 -222
  73. package/build/src/library/fleet.js +2 -1
  74. package/build/src/runtime/proc/windows-launch.exe +0 -0
  75. package/package.json +1 -1
@@ -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
  }
@@ -0,0 +1,8 @@
1
+ import type { UpstreamRequestService } from "./heart/index.js";
2
+ import type { ServeInput, UpstreamFact } from "./request-serve.js";
3
+ type ExecutionResult = Readonly<{
4
+ result: unknown;
5
+ service?: UpstreamRequestService;
6
+ }>;
7
+ export declare function executeRequest(input: ServeInput, request: UpstreamFact): Promise<ExecutionResult>;
8
+ export {};