@astrosheep/keiyaku 4.5.6 → 4.5.7

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 (36) hide show
  1. package/build/integrations/marketplace/plugins/keiyaku/opencode.js +1 -1
  2. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +1 -0
  3. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-babysit/SKILL.md +114 -0
  4. package/build/src/akuma/body.d.ts +1 -1
  5. package/build/src/akuma/body.js +20 -33
  6. package/build/src/akuma/heart/index.d.ts +2 -2
  7. package/build/src/akuma/heart/index.js +2 -2
  8. package/build/src/akuma/heart/request-authority.d.ts +1 -0
  9. package/build/src/akuma/heart/request-authority.js +11 -1
  10. package/build/src/akuma/heart/storage.d.ts +0 -1
  11. package/build/src/akuma/heart/storage.js +0 -54
  12. package/build/src/akuma/publication.js +75 -13
  13. package/build/src/akuma/request-lifecycle.js +9 -22
  14. package/build/src/akuma/request-serve.js +17 -2
  15. package/build/src/cli/commands/task-invoke.d.ts +4 -2
  16. package/build/src/cli/commands/task-invoke.js +48 -22
  17. package/build/src/cli/commands/task.d.ts +1 -1
  18. package/build/src/cli/commands/task.js +7 -3
  19. package/build/src/cli/coordinates.d.ts +2 -0
  20. package/build/src/cli/coordinates.js +18 -2
  21. package/build/src/cli/render/task.js +6 -5
  22. package/build/src/cli/square-edge.js +2 -2
  23. package/build/src/git/repository.d.ts +2 -0
  24. package/build/src/git/repository.js +34 -21
  25. package/build/src/git/target-placement.js +0 -6
  26. package/build/src/protocol/attempt.js +9 -4
  27. package/build/src/task/compose-language.d.ts +4 -0
  28. package/build/src/task/compose-language.js +7 -0
  29. package/build/src/task/compose-parser.d.ts +1 -0
  30. package/build/src/task/compose-parser.js +4 -2
  31. package/build/src/task/compose.d.ts +1 -0
  32. package/build/src/task/compose.js +2 -1
  33. package/build/src/task/context.d.ts +7 -1
  34. package/build/src/task/context.js +25 -6
  35. package/build/src/task/index.d.ts +6 -1
  36. package/package.json +2 -2
@@ -1,7 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { fileURLToPath } from "node:url";
3
3
 
4
- const skills = ["keiyaku", "keiyaku-task", "keiyaku-bind", "keiyaku-workflow", "keiyaku-akuma"]
4
+ const skills = ["keiyaku", "keiyaku-task", "keiyaku-bind", "keiyaku-workflow", "keiyaku-akuma", "keiyaku-babysit"]
5
5
  .map((name) => fileURLToPath(new URL(`./skills/${name}/SKILL.md`, import.meta.url)));
6
6
 
7
7
  export default {
@@ -14,6 +14,7 @@ read `<command> --help` before using flags.
14
14
  - A Contract is delivery authority; see `keiyaku-bind` to author and bind one,
15
15
  then `keiyaku-workflow` for the remaining lifecycle.
16
16
  - An Akuma is a callable worker; see `keiyaku-akuma`.
17
+ - A long-running multi-lane goal needs coordination; see `keiyaku-babysit`.
17
18
 
18
19
  Settings owns named gate bundles. Omitting `--gates` selects `gates.default`,
19
20
  or freezes `["reviewed"]` when that entry is absent. `--gates <name,...>`
@@ -0,0 +1,114 @@
1
+ ---
2
+ name: keiyaku-babysit
3
+ description: Babysit a long-running multi-lane goal — a room of lanes that advance on their own, call for you on their schedule, and hold each other up while you are busy being productive.
4
+ ---
5
+
6
+ # keiyaku-babysit
7
+
8
+ A goal big enough to babysit is a room full of wards: kei in delivery,
9
+ Akuma working, reviews pending, a caller owing a decision, an external
10
+ dependency due someday. Each lane advances on its own once set moving;
11
+ each calls for you when it chooses; a lane left waiting on you holds up
12
+ the lanes behind it. There is one of you. The hard part is where your
13
+ next ten minutes go.
14
+
15
+ ## You did not hand it off. You booked yourself.
16
+
17
+ Handing a lane to a worker felt lighter, didn't it. What you actually
18
+ did was schedule your own future: a question to answer, a return to
19
+ verify, a review to start, a delivery to make — all arriving when the
20
+ lane is ready, never when you are. While absorbed anywhere, every lane
21
+ reaching such a moment stops dead and waits, billing the goal by the
22
+ minute. Price attention by delay cost, not work importance: the
23
+ two-minute answer an Akuma is stalled on outranks an hour of brilliant
24
+ work in your hands, because your hour happens later and the stall does
25
+ not un-happen. Yes, even though your work is more interesting.
26
+
27
+ ## The sweep
28
+
29
+ Several lanes calling at once? How flattering. Take them in order of
30
+ what your delay costs, never the order you prefer:
31
+
32
+ 1. Unblock anyone stopped and waiting on you: a question, hand-off, or
33
+ review only you can start. It will take minutes; it has cost hours.
34
+ 2. Verify returns, starting with the one other lanes build on. An
35
+ unverified return cannot close its lane, and every lane treating it
36
+ as settled builds on your unchecked assumption. If two returns can
37
+ interact, read both before settling either.
38
+ 3. Tell lanes what changed. A settled result that invalidates a running
39
+ lane's premise gets relayed before your own work; silence manufactures
40
+ rework.
41
+ 4. Then, and only then, your own lane. It survived waiting.
42
+
43
+ Sweep at the seams: before a long stretch, when a lane returns something
44
+ material, and when you hand a new lane out. Not on a timer. A quiet lane
45
+ is not a content lane: quiet is working, stuck, or waiting on you, and
46
+ you cannot tell which from your chair. Go look; reading progress into
47
+ silence is astrology, not babysitting.
48
+
49
+ ## Productive absorption
50
+
51
+ Neglect does not have to look like thrashing on a dead end. Real neglect
52
+ looks like you, deeply satisfied, making excellent progress on one ward
53
+ while the room goes unswept: an Akuma holding a one-line question for
54
+ two hours, a finished return nobody dares build on because you never
55
+ checked it, a caller decision request composed in your head and sent
56
+ nowhere. The test is not "am I being productive" — you are. Is any
57
+ lane's due care older than the work in your hands?
58
+
59
+ ## The critical path decides ties
60
+
61
+ Two lanes call, both claims valid? Serve the critical path. Hand out
62
+ critical-path work first, too: the side lane is tempting because it is
63
+ easier to specify, and easy-to-specify is how goals die surrounded by
64
+ finished side quests. Re-derive the path after every settled result; a
65
+ delivered kei or fresh blocker can promote a background lane.
66
+
67
+ ## Only you can see the whole room
68
+
69
+ Task records dependencies between tasks; nothing records that an Akuma
70
+ waits on your answer, a review waits on a worktree, or the caller owes a
71
+ lane a decision. That wait-graph lives in exactly one place: your head.
72
+ It is what the sweep walks. When it outgrows your head — it will — park
73
+ the waits in task notes. The map may degrade to paper, never to memory
74
+ you merely trust. Wards answer at different speeds — Akuma in minutes,
75
+ reviewers in hours, callers and external dependencies in geological
76
+ time — so ask the slow ones when the question is knowable, not urgent.
77
+ Shape a caller request as a decision with evidence, not a pile of raw
78
+ results.
79
+
80
+ ## Release a blocked lane, keep the goal
81
+
82
+ Lane blocked beyond your reach? Write the blocker into its task where
83
+ the next sweep will see it, and release the lane. Hovering beside a
84
+ blocker you cannot move is decoration, not diligence. The note makes
85
+ releasing different from abandoning. Exception: a blocker gating the
86
+ critical path with nothing else runnable is the job — press on it, split
87
+ it into its own task or kei, or take it to the caller now.
88
+
89
+ ## Tired babysitters skip the checking
90
+
91
+ Under load you cut verification depth first: diffs skimmed, gates
92
+ assumed, "done" believed because the worker sounded confident. The
93
+ goal's quality becomes the shabbiest check you accepted while busy.
94
+ For a risky return, delegate an independent review: minutes spent, bar
95
+ kept. Accepting it unread is free today and costs a later lane its
96
+ premise.
97
+
98
+ ## Steer early, with the existing verbs
99
+
100
+ Evidence changed the delivery? Amend the kei. Evidence obsoleted it?
101
+ Abandon and bind a new one. Discovery deserves its own lane? Make it a
102
+ task. Now, not "after this stretch": every lane keeps spending against
103
+ the old contract until you speak.
104
+
105
+ ## Boundaries
106
+
107
+ Babysitting is a method, not a role: no new identity, permission, ledger,
108
+ status, id, lifecycle, fixed cadence, numeric threshold, or mandatory
109
+ Task+kei+Akuma bundle. Task is optional planning and dependency memory;
110
+ each kei stays one bounded delivery; Akuma is one ward among the
111
+ entities you watch, not the definition of the job. Commands live with
112
+ their owners — keiyaku-task, keiyaku-bind, keiyaku-workflow,
113
+ keiyaku-akuma — this skill says when to reach for them, never how they
114
+ work.
@@ -40,8 +40,8 @@ export type TellResult = Readonly<{
40
40
  wake: TellWake;
41
41
  }>;
42
42
  export type TellWakeRuntime = Readonly<{
43
- observeHeart(paths: AkumaPaths, signal: AbortSignal): Promise<AsyncGenerator<void>>;
44
43
  spawn(paths: AkumaPaths): Promise<OwnedProcess>;
44
+ schedule?(milliseconds: number, signal: AbortSignal): Promise<void>;
45
45
  }>;
46
46
  type BodyRuntime = Readonly<{
47
47
  now(): string;
@@ -3,13 +3,14 @@ import { fileURLToPath } from "node:url";
3
3
  import { abortableDelay } from "./abort.js";
4
4
  import { BodySupervisor } from "./body-supervisor.js";
5
5
  import { driveTurn, turnRecipe } from "./turn-drive.js";
6
- import { HeldAkumaLeash, breakBody, endTurn, finishBodyIfIdle, heartExists, isHeartAbsent, readHeart, readNonterminalRequests, watchHeart, } from "./heart/index.js";
6
+ import { HeldAkumaLeash, breakBody, endTurn, finishBodyIfIdle, heartExists, isHeartAbsent, readHeart, readNonterminalRequests, } from "./heart/index.js";
7
7
  import { worldRootForAkumaPaths } from "./identity.js";
8
8
  import { keiyakuSquarePath } from "../world.js";
9
9
  import { resolveProviderExecution } from "./providers/index.js";
10
10
  import { clearBodyRequestTransport, settleBodyRequests, } from "./request-serve.js";
11
11
  import { spawnDetachedProcess, } from "../runtime/proc/run.js";
12
12
  const LEASH_RETRY_MS = 100;
13
+ const WAKE_REREAD_MS = 100;
13
14
  export const LEASH_HELD_EXIT = 75;
14
15
  export { CONTROL_RESPONSE_MS } from "./body-supervisor.js";
15
16
  function missing(error) {
@@ -296,8 +297,8 @@ export async function runAkumaBody(launch, upstream) {
296
297
  return await driveAkumaBody(launch, undefined, { ...defaultRuntime(), upstream });
297
298
  }
298
299
  const DIRECT_TELL_WAKE = {
299
- observeHeart: watchHeart,
300
300
  spawn: async (paths) => await spawnAkumaBody({ paths, refuseIfHeld: true }),
301
+ schedule: abortableDelay,
301
302
  };
302
303
  function diagnostic(error) {
303
304
  return error instanceof Error ? error.message : String(error);
@@ -321,61 +322,47 @@ function failedChild(exit) {
321
322
  child: { code: exit.code, signal: exit.signal, log: exit.log },
322
323
  };
323
324
  }
324
- async function awaitWake(paths, tellId, after, child, changed) {
325
- let nextChange = changed.next();
325
+ async function awaitWake(paths, tellId, after, child, schedule) {
326
326
  for (;;) {
327
327
  const settled = await settledWake(paths, tellId, after);
328
328
  if (settled !== null) {
329
329
  child.release();
330
330
  return settled;
331
331
  }
332
- const winner = await Promise.race([
333
- nextChange.then(({ done }) => ({ kind: done ? "closed" : "changed" }), (error) => ({ kind: "observer-failed", error })),
334
- child.exited.then((exit) => ({ kind: "exited", exit })),
335
- ]);
336
- if (winner.kind === "changed") {
337
- nextChange = changed.next();
338
- continue;
332
+ const timerController = new AbortController();
333
+ const timer = schedule(WAKE_REREAD_MS, timerController.signal).then(() => ({ kind: "timer" }));
334
+ let winner;
335
+ try {
336
+ winner = await Promise.race([child.exited.then((exit) => ({ kind: "exited", exit })), timer]);
339
337
  }
338
+ catch (error) {
339
+ timerController.abort();
340
+ await timer.catch(() => undefined);
341
+ throw error;
342
+ }
343
+ if (winner.kind === "timer")
344
+ continue;
345
+ timerController.abort();
346
+ await timer.catch(() => undefined);
340
347
  const final = await settledWake(paths, tellId, after);
341
348
  if (final !== null) {
342
349
  child.release();
343
350
  return final;
344
351
  }
345
- if (winner.kind === "exited") {
346
- return winner.exit.code === LEASH_HELD_EXIT ? { kind: "held" } : failedChild(winner.exit);
347
- }
348
- try {
349
- await child.terminate();
350
- }
351
- catch (error) {
352
- /* Termination rejection leaves physical custody unresolved; release it deliberately. */
353
- child.release();
354
- return { kind: "failed", diagnostic: diagnostic(error) };
355
- }
356
- return winner.kind === "observer-failed"
357
- ? { kind: "failed", diagnostic: diagnostic(winner.error) }
358
- : { kind: "failed", diagnostic: "Heart observer closed" };
352
+ return winner.exit.code === LEASH_HELD_EXIT ? { kind: "held" } : failedChild(winner.exit);
359
353
  }
360
354
  }
361
355
  async function wakePendingTells(paths, tellId, runtime) {
362
- const watching = new AbortController();
363
- let changed;
364
356
  try {
365
- changed = await runtime.observeHeart(paths, watching.signal);
366
357
  const beforeHeart = await readHeart(paths);
367
358
  if (tellId === undefined && beforeHeart.pending.length === 0)
368
359
  return null;
369
360
  const before = beforeHeart.latestBody?.sequence ?? 0;
370
- return await awaitWake(paths, tellId, before, await runtime.spawn(paths), changed);
361
+ return await awaitWake(paths, tellId, before, await runtime.spawn(paths), runtime.schedule ?? abortableDelay);
371
362
  }
372
363
  catch (error) {
373
364
  return { kind: "failed", diagnostic: diagnostic(error) };
374
365
  }
375
- finally {
376
- watching.abort();
377
- await changed?.return(undefined);
378
- }
379
366
  }
380
367
  export async function wakeRecordedTell(paths, tellId, runtime = DIRECT_TELL_WAKE) {
381
368
  return {
@@ -3,8 +3,8 @@ import type { BodyEnd, BodyFact, ForkPoint, HeartSnapshot, KillFact, SealFact, S
3
3
  export type { CallFact } from "./facts.js";
4
4
  import type { ActivityFact } from "./rows.js";
5
5
  import { type ActivityFactSlice } from "./timeline.js";
6
- export { HeartAbsentError, HeldAkumaLeash, classifyHeartSchema, initializeHeart, isHeartAbsent, probeLeash, watchHeart, } from "./storage.js";
7
- export { admitRequest, readNonterminalRequests, readRequest, refuseRequest, reserveRequest, serveRequest, serveUpstreamRequest, voidRequest, } from "./request-authority.js";
6
+ export { HeartAbsentError, HeldAkumaLeash, classifyHeartSchema, initializeHeart, isHeartAbsent, probeLeash, } from "./storage.js";
7
+ export { admitRequest, isRequestInputConflict, readNonterminalRequests, readRequest, refuseRequest, reserveRequest, serveRequest, serveUpstreamRequest, voidRequest, } from "./request-authority.js";
8
8
  export { life, lifeAt } from "./facts.js";
9
9
  export type { AkumaLife, AkumaOrigin, BodyEnd, BodyFact, HeartSnapshot, ForkPoint, KillEvidence, KillFact, LeashProbe, PauseFact, RequestFact, RequestInput, RequestRecipe, UpstreamRequestService, SealFact, SessionFact, Soul, StopFact, TellFact, TellDelivery, TellDeliveryInput, TellReceiptInput, TurnEndFact, TurnFact, TurnOutcome, TurnStartFact, } from "./facts.js";
10
10
  export type { ResumeCoordinate } from "../coordinate.js";
@@ -4,8 +4,8 @@ import { insertTellDeliveryFact, insertTellFact, insertTellReceiptFact, pendingT
4
4
  import { activityFactSlice, lastActivityAt as readLastActivityAt, pruneActivityFacts, } from "./timeline.js";
5
5
  import { isHeartAbsent, readSealFromLeash, readTransaction, transaction, withHeart, withReadOnlyHeart, } from "./storage.js";
6
6
  import { soulFact } from "./soul.js";
7
- export { HeartAbsentError, HeldAkumaLeash, classifyHeartSchema, initializeHeart, isHeartAbsent, probeLeash, watchHeart, } from "./storage.js";
8
- export { admitRequest, readNonterminalRequests, readRequest, refuseRequest, reserveRequest, serveRequest, serveUpstreamRequest, voidRequest, } from "./request-authority.js";
7
+ export { HeartAbsentError, HeldAkumaLeash, classifyHeartSchema, initializeHeart, isHeartAbsent, probeLeash, } from "./storage.js";
8
+ export { admitRequest, isRequestInputConflict, readNonterminalRequests, readRequest, refuseRequest, reserveRequest, serveRequest, serveUpstreamRequest, voidRequest, } from "./request-authority.js";
9
9
  export { life, lifeAt } from "./facts.js";
10
10
  const ACTIVITY_LIMIT = 5_000;
11
11
  export async function readSoul(paths) {
@@ -1,5 +1,6 @@
1
1
  import type { AkuId, AkumaPaths } from "../identity.js";
2
2
  import type { RequestFact, RequestInput, UpstreamRequestService } from "./facts.js";
3
+ export declare function isRequestInputConflict(error: unknown): error is Error;
3
4
  export declare function admitRequest(paths: AkumaPaths, input: RequestInput & Readonly<{
4
5
  admittedAt: string;
5
6
  }>): Promise<RequestFact>;
@@ -3,6 +3,16 @@ import { transaction, withHeart } from "./storage.js";
3
3
  import { soulFact } from "./soul.js";
4
4
  import { clipAllowedActions } from "../allowed.js";
5
5
  import { requestPayloadJson } from "./request-rows.js";
6
+ // This stays out of the public Heart index: only transport service projects it as a refusal.
7
+ class RequestInputConflictError extends Error {
8
+ constructor(id) {
9
+ super(`Akuma request ${id} reused different input`);
10
+ this.name = "RequestInputConflictError";
11
+ }
12
+ }
13
+ export function isRequestInputConflict(error) {
14
+ return error instanceof RequestInputConflictError;
15
+ }
6
16
  export async function admitRequest(paths, input) {
7
17
  return await withHeart(paths, (heart) => transaction(heart, () => {
8
18
  const soul = soulFact(heart);
@@ -33,7 +43,7 @@ export async function admitRequest(paths, input) {
33
43
  fact.id !== normalized.id ||
34
44
  fact.action !== normalized.action ||
35
45
  requestPayloadJson(fact) !== requestPayloadJson(normalized)) {
36
- throw new Error(`Akuma request ${input.id} reused different input`);
46
+ throw new RequestInputConflictError(input.id);
37
47
  }
38
48
  return fact;
39
49
  }));
@@ -2,7 +2,6 @@ import { DatabaseSync } from "node:sqlite";
2
2
  import type { AkumaPaths } from "../identity.js";
3
3
  import type { BodyFact, LeashProbe, SessionFact, Soul, StopFact, TellFact } from "./facts.js";
4
4
  import { sealFact } from "./rows.js";
5
- export declare function watchHeart(paths: AkumaPaths, signal: AbortSignal): Promise<AsyncGenerator<void>>;
6
5
  export declare class HeartAbsentError extends Error {
7
6
  readonly path: string;
8
7
  constructor(path: string, options?: ErrorOptions);
@@ -1,7 +1,5 @@
1
1
  import { DatabaseSync } from "node:sqlite";
2
- import { watch as watchDirectory } from "node:fs";
3
2
  import { lstat } from "node:fs/promises";
4
- import { basename, dirname } from "node:path";
5
3
  import { pathToFileURL } from "node:url";
6
4
  import { HEART_SCHEMA, LEASH_SCHEMA, assertHeartSchemaVersion, assertLeashSchemaVersion, heartSchemaIsCurrent, } from "./schema.js";
7
5
  import { deletePauseControl, deleteStopControl, insertBodyFact, insertKillFact, insertSealFact, insertSessionFact, killFactForBody, latestBodyFact, latestKillFact, markBodyHung, sealExists, sealFact, stopFact, } from "./rows.js";
@@ -10,58 +8,6 @@ import { pruneActivityFacts } from "./timeline.js";
10
8
  import { insertSoulFact, soulFact } from "./soul.js";
11
9
  const ACTIVITY_LIMIT = 5_000;
12
10
  const SQLITE_CANTOPEN = 14;
13
- export async function watchHeart(paths, signal) {
14
- const heart = basename(paths.heart);
15
- let changed = false;
16
- let failure;
17
- let wake;
18
- // fs.watch opens the directory synchronously. Its throw is therefore the
19
- // observer's ready/failure boundary, before a waker can spawn a child.
20
- const watcher = watchDirectory(dirname(paths.heart), { signal }, (_event, filename) => {
21
- const name = filename?.toString();
22
- if (name === undefined || name === heart || name === `${heart}-wal`) {
23
- changed = true;
24
- wake?.();
25
- }
26
- });
27
- watcher.on("error", (error) => {
28
- failure = error;
29
- wake?.();
30
- });
31
- const abort = () => {
32
- wake?.();
33
- watcher.close();
34
- };
35
- signal.addEventListener("abort", abort, { once: true });
36
- return (async function* () {
37
- try {
38
- for (;;) {
39
- if (changed) {
40
- changed = false;
41
- yield;
42
- continue;
43
- }
44
- if (failure !== undefined)
45
- throw failure;
46
- if (signal.aborted)
47
- return;
48
- await new Promise((resolve) => {
49
- wake = resolve;
50
- });
51
- wake = undefined;
52
- }
53
- }
54
- catch (error) {
55
- if (signal.aborted && error.name === "AbortError")
56
- return;
57
- throw error;
58
- }
59
- finally {
60
- signal.removeEventListener("abort", abort);
61
- watcher.close();
62
- }
63
- })();
64
- }
65
11
  export class HeartAbsentError extends Error {
66
12
  path;
67
13
  constructor(path, options) {
@@ -127,6 +127,60 @@ async function takeLeashUntil(paths, deadline) {
127
127
  await abortableDelay(Math.min(POLL_MS, Math.max(0, deadline - performance.now())));
128
128
  }
129
129
  }
130
+ async function takeLeash(paths) {
131
+ for (;;) {
132
+ const leash = await HeldAkumaLeash.try(paths);
133
+ if (leash !== null)
134
+ return leash;
135
+ await abortableDelay(POLL_MS);
136
+ }
137
+ }
138
+ async function allocatedSoul(allocated) {
139
+ const soul = await readSoul(allocated.paths);
140
+ if (soul === null)
141
+ throw new Error("Akuma birth settled without a soul");
142
+ if (soul.id !== allocated.id)
143
+ throw new Error("Akuma birth returned a different identity");
144
+ return soul;
145
+ }
146
+ async function awaitConfirmation(result) {
147
+ try {
148
+ await result;
149
+ }
150
+ catch {
151
+ await new Promise(() => { });
152
+ }
153
+ }
154
+ async function settleCancelledLaunch(allocated, owned, cancellation) {
155
+ if ((await readSoul(allocated.paths)) !== null) {
156
+ await allocatedSoul(allocated);
157
+ return "born";
158
+ }
159
+ await awaitConfirmation(owned.terminate());
160
+ await awaitConfirmation(owned.exited);
161
+ if ((await readSoul(allocated.paths)) !== null) {
162
+ await allocatedSoul(allocated);
163
+ return "born";
164
+ }
165
+ const leash = await takeLeash(allocated.paths);
166
+ let outcome;
167
+ try {
168
+ outcome = await leash.sealIfUnborn(allocated.paths, {
169
+ evidence: diagnostic(cancellation),
170
+ at: new Date().toISOString(),
171
+ });
172
+ }
173
+ finally {
174
+ leash.release();
175
+ }
176
+ if (outcome === "born") {
177
+ await allocatedSoul(allocated);
178
+ return "born";
179
+ }
180
+ if ((await readSeal(allocated.paths)) === null)
181
+ throw new Error("Akuma birth settled without a seal");
182
+ return "sealed";
183
+ }
130
184
  async function awaitAsleepBirth(paths) {
131
185
  const leash = await takeLeashUntil(paths, performance.now() + BIRTH_TIMEOUT_MS);
132
186
  if (leash === null)
@@ -174,25 +228,33 @@ export async function birthAkuma(input) {
174
228
  }
175
229
  export async function launchAkuma(input) {
176
230
  const { allocated } = input;
177
- let owned;
231
+ let owned = undefined;
178
232
  try {
179
233
  input.signal?.throwIfAborted();
180
234
  owned = await input.launch(allocated);
181
- try {
182
- input.signal?.throwIfAborted();
183
- const soul = await awaitBirth(allocated.paths, owned ?? undefined, input.signal);
184
- if (soul.id !== allocated.id)
185
- throw new Error("Akuma birth returned a different identity");
186
- if (input.awaitAsleep === true)
187
- await awaitAsleepBirth(allocated.paths);
188
- return allocated;
189
- }
190
- finally {
191
- owned?.release();
192
- }
235
+ input.signal?.throwIfAborted();
236
+ const soul = await awaitBirth(allocated.paths, owned ?? undefined, input.signal);
237
+ if (soul.id !== allocated.id)
238
+ throw new Error("Akuma birth returned a different identity");
239
+ if (input.awaitAsleep === true)
240
+ await awaitAsleepBirth(allocated.paths);
241
+ owned?.release();
242
+ return allocated;
193
243
  }
194
244
  catch (error) {
245
+ if (owned !== undefined && input.signal?.aborted && error === input.signal.reason) {
246
+ const outcome = await settleCancelledLaunch(allocated, owned, error);
247
+ if (outcome === "born") {
248
+ if (input.awaitAsleep === true)
249
+ await awaitAsleepBirth(allocated.paths);
250
+ owned.release();
251
+ return allocated;
252
+ }
253
+ owned.release();
254
+ throw error;
255
+ }
195
256
  await sealLocalFailure(allocated, error);
257
+ owned?.release();
196
258
  throw error;
197
259
  }
198
260
  }
@@ -4,7 +4,7 @@ import { abortableDelay } from "./abort.js";
4
4
  import { HeldAkumaLeash, readNonterminalRequests, readSoul, serveRequest, voidRequest, } from "./heart/index.js";
5
5
  import { pathsForAkuId } from "./identity.js";
6
6
  import { BIRTH_TIMEOUT_MS } from "./publication.js";
7
- import { atomicJson, decodeClaim, receiptPath } from "./request-wire.js";
7
+ import { decodeClaim } from "./request-wire.js";
8
8
  const POLL_MS = 100;
9
9
  export class BodyRequestPump {
10
10
  input;
@@ -52,19 +52,14 @@ export class BodyRequestPump {
52
52
  continue;
53
53
  }
54
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
- }
55
+ await this.serveClaim({
56
+ directory: this.directory,
57
+ transportId,
58
+ claim,
59
+ ...this.input,
60
+ signal: this.executionSignal.signal,
61
+ admissionOpen: () => !this.admissionClosed,
62
+ });
68
63
  }
69
64
  this.handled.add(transportId);
70
65
  }
@@ -107,14 +102,6 @@ async function requestFiles(directory) {
107
102
  throw error;
108
103
  }
109
104
  }
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
105
  function matchingRequestOrigin(soul, parent, id) {
119
106
  return soul.origin.kind === "request" && soul.origin.parent === parent && soul.origin.requestId === id;
120
107
  }
@@ -1,4 +1,4 @@
1
- import { admitRequest, readRequest, refuseRequest, reserveRequest, serveRequest as serveChildRequest, serveUpstreamRequest, voidRequest, } from "./heart/index.js";
1
+ import { admitRequest, isRequestInputConflict, readRequest, refuseRequest, reserveRequest, serveRequest as serveChildRequest, serveUpstreamRequest, voidRequest, } from "./heart/index.js";
2
2
  import { worldRootForAkumaPaths } from "./identity.js";
3
3
  import { publishAkuma } from "./publication.js";
4
4
  import { atomicJson, canonicalAkuId, decodeRecipe, receiptPath, } from "./request-wire.js";
@@ -244,8 +244,23 @@ export async function serveRequest(input) {
244
244
  ? await serveCall({ ...input, claim: input.claim })
245
245
  : await serveUpstream({ ...input, claim: input.claim });
246
246
  }
247
+ async function serveTransportClaim(input) {
248
+ try {
249
+ await serveRequest(input);
250
+ }
251
+ catch (error) {
252
+ if (!isRequestInputConflict(error))
253
+ throw error;
254
+ await atomicJson(receiptPath(input.directory, input.transportId), {
255
+ id: input.claim.id,
256
+ action: input.claim.action,
257
+ state: "refused",
258
+ diagnostic: error.message,
259
+ });
260
+ }
261
+ }
247
262
  export class BodyRequestPump extends LifecycleBodyRequestPump {
248
263
  static async open(input) {
249
- return (await LifecycleBodyRequestPump.openWithService(input, async (claim) => await serveRequest(claim)));
264
+ return (await LifecycleBodyRequestPump.openWithService(input, async (claim) => await serveTransportClaim(claim)));
250
265
  }
251
266
  }
@@ -1,10 +1,10 @@
1
- import { type BlockedTaskList, type TaskBatchResult, type TaskCompositionResult, type TaskDecompositionTree, type TaskDetail, type TaskDoctorReport, type TaskList, type TaskMutationResult, type TaskNamespaceResult, type TaskQueryResult, type TaskUpdateResult } from "../../task/index.js";
1
+ import { type BlockedTaskList, type TaskBatchResult, type TaskCompositionResult, type TaskDecompositionTree, type TaskDetail, type TaskDoctorReport, type TaskList, type TaskMutationResult, type TaskContextResult, type TaskQueryResult, type TaskUpdateResult } from "../../task/index.js";
2
2
  import type { ParsedTaskCommand } from "./task.js";
3
3
  import type { WorldRoot } from "../../world.js";
4
4
  export type TaskShowResult = TaskDetail | readonly TaskDetail[] | Extract<TaskMutationResult, {
5
5
  kind: "refused";
6
6
  }>;
7
- export type TaskInvocationResult = TaskMutationResult | TaskUpdateResult | TaskBatchResult | TaskCompositionResult | TaskShowResult | TaskList | BlockedTaskList | TaskQueryResult | TaskDecompositionTree | TaskDoctorReport | TaskNamespaceResult | TaskWorldObservation;
7
+ export type TaskInvocationResult = TaskMutationResult | TaskUpdateResult | TaskBatchResult | TaskCompositionResult | TaskShowResult | TaskList | BlockedTaskList | TaskQueryResult | TaskDecompositionTree | TaskDoctorReport | TaskContextResult | TaskWorldObservation;
8
8
  type TaskWorldRead = TaskList | BlockedTaskList | TaskQueryResult | TaskDoctorReport;
9
9
  export type TaskWorldObservation = Readonly<{
10
10
  kind: "present";
@@ -23,6 +23,8 @@ type TaskInput = Readonly<{
23
23
  context: Readonly<{
24
24
  directory: string;
25
25
  boundary: string;
26
+ writeRoot?: string;
27
+ managed?: boolean;
26
28
  }>;
27
29
  establish(): Promise<WorldRoot>;
28
30
  readStdin(): Promise<string>;