@astrosheep/keiyaku 4.4.0 → 4.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/build/integrations/marketplace/plugins/keiyaku/.claude-plugin/plugin.json +4 -2
  2. package/build/integrations/marketplace/plugins/keiyaku/.codex-plugin/plugin.json +1 -1
  3. package/build/integrations/marketplace/plugins/keiyaku/package.json +9 -3
  4. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +16 -5
  5. package/build/integrations/pi/keiyaku.ts +159 -0
  6. package/build/src/akuma/abort.js +9 -9
  7. package/build/src/akuma/akuma-handle.d.ts +4 -2
  8. package/build/src/akuma/akuma-handle.js +14 -2
  9. package/build/src/akuma/akuma-product.d.ts +32 -3
  10. package/build/src/akuma/akuma-product.js +44 -20
  11. package/build/src/akuma/akuma.d.ts +0 -1
  12. package/build/src/akuma/body.d.ts +1 -7
  13. package/build/src/akuma/body.js +35 -13
  14. package/build/src/akuma/heart/index.js +7 -3
  15. package/build/src/akuma/heart/rows.d.ts +1 -1
  16. package/build/src/akuma/heart/rows.js +3 -3
  17. package/build/src/akuma/heart/storage.d.ts +1 -0
  18. package/build/src/akuma/heart/storage.js +28 -6
  19. package/build/src/akuma/identity.d.ts +1 -0
  20. package/build/src/akuma/identity.js +7 -0
  21. package/build/src/akuma/index.d.ts +1 -2
  22. package/build/src/akuma/projection.d.ts +18 -2
  23. package/build/src/akuma/projection.js +11 -1
  24. package/build/src/akuma/providers/claude/index.js +68 -37
  25. package/build/src/akuma/publication.d.ts +9 -3
  26. package/build/src/akuma/publication.js +20 -3
  27. package/build/src/akuma/request-lifecycle.d.ts +45 -0
  28. package/build/src/akuma/request-lifecycle.js +171 -0
  29. package/build/src/akuma/request-serve.d.ts +9 -32
  30. package/build/src/akuma/request-serve.js +48 -178
  31. package/build/src/akuma/request-wire.js +1 -1
  32. package/build/src/akuma/requests.js +3 -2
  33. package/build/src/cli/commands/akuma-invoke.d.ts +11 -2
  34. package/build/src/cli/commands/akuma-invoke.js +29 -7
  35. package/build/src/cli/commands/akuma.d.ts +1 -0
  36. package/build/src/cli/commands/akuma.js +16 -6
  37. package/build/src/cli/commands/contract.d.ts +3 -2
  38. package/build/src/cli/commands/contract.js +5 -2
  39. package/build/src/cli/commands/status-set.d.ts +4 -0
  40. package/build/src/cli/commands/status-set.js +61 -0
  41. package/build/src/cli/invoke.js +6 -3
  42. package/build/src/cli/parse.js +1 -1
  43. package/build/src/cli/render/akuma-activity.js +11 -0
  44. package/build/src/cli/render/akuma.js +7 -0
  45. package/build/src/cli/render/status-set.d.ts +5 -0
  46. package/build/src/cli/render/status-set.js +14 -0
  47. package/build/src/cli/render/text.js +3 -0
  48. package/build/src/cli/result.d.ts +15 -1
  49. package/build/src/cli/runtime.js +2 -0
  50. package/build/src/cli/square-edge.d.ts +6 -0
  51. package/build/src/cli/square-edge.js +74 -0
  52. package/build/src/contract-worktree.js +18 -5
  53. package/build/src/library/akuma-creation.d.ts +14 -3
  54. package/build/src/library/akuma-creation.js +31 -18
  55. package/build/src/library/fleet.d.ts +12 -1
  56. package/build/src/library/fleet.js +21 -5
  57. package/build/src/library/repo.js +3 -1
  58. package/build/src/runtime/proc/process-exit.js +6 -1
  59. package/build/src/runtime/proc/run.js +6 -10
  60. package/build/src/runtime/proc/termination.d.ts +1 -0
  61. package/build/src/runtime/proc/termination.js +22 -2
  62. package/build/src/runtime/proc/windows-launch.exe +0 -0
  63. package/build/src/runtime/proc/windows-run.js +10 -7
  64. package/build/src/world.d.ts +2 -0
  65. package/build/src/world.js +4 -0
  66. package/package.json +8 -3
  67. package/build/src/akuma/result-route.d.ts +0 -2
  68. package/build/src/akuma/result-route.js +0 -1
@@ -1,7 +1,9 @@
1
1
  {
2
2
  "name": "keiyaku",
3
- "version": "0.1.0+codex.20260819155641",
3
+ "version": "0.1.0",
4
4
  "description": "Use the Keiyaku contract, task, and Akuma CLI.",
5
- "author": { "name": "Keiyaku" },
5
+ "author": {
6
+ "name": "Keiyaku"
7
+ },
6
8
  "skills": "./skills/"
7
9
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keiyaku",
3
- "version": "0.1.0+codex.20260819155641",
3
+ "version": "0.1.0+codex.20260826051747",
4
4
  "description": "Use the Keiyaku contract, task, and Akuma CLI.",
5
5
  "author": {
6
6
  "name": "Keiyaku"
@@ -1,8 +1,14 @@
1
1
  {
2
2
  "name": "keiyaku-harness",
3
- "version": "0.1.0+codex.20260819155641",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "main": "./opencode.js",
6
- "keywords": ["pi-package"],
7
- "pi": { "skills": ["./skills"] }
6
+ "keywords": [
7
+ "pi-package"
8
+ ],
9
+ "pi": {
10
+ "skills": [
11
+ "./skills"
12
+ ]
13
+ }
8
14
  }
@@ -84,6 +84,7 @@ choose `--interrupt` when the current attempt itself is the problem.
84
84
 
85
85
  ```bash
86
86
  keiyaku history <aku/...|@alias> --last
87
+ keiyaku history <aku/...|@alias> --id <historyId>
87
88
  keiyaku history <aku/...|@alias> [--before <N> | --since <N>] [--limit <N>]
88
89
  ```
89
90
 
@@ -91,9 +92,17 @@ keiyaku history <aku/...|@alias> [--before <N> | --since <N>] [--limit <N>]
91
92
  and says so plainly when no answer exists yet. Snapshot rows elsewhere may
92
93
  clip long text; the terminal answer from `call`/`wait` and the bytes from
93
94
  `--last` are never clipped — when you need the full result, take it from one
94
- of those. Cursor reads page the activity timeline; `--before` and `--since`
95
- are exclusive sequence cursors. `--limit` defaults to 50 and accepts at most
96
- 5000 semantic rows.
95
+ of those. Every completed answered or failed outcome carries one Heart-owned
96
+ public `historyId` shaped as `turn/<positive-safe-integer>` in `status`, `wait`,
97
+ and history output. Use `--id` with that same value to read exactly one retained
98
+ outcome: text writes the complete answer or diagnostic bytes without clipping
99
+ or framing. Provider-native history coordinates remain private. A malformed or
100
+ unknown ID is a typed nonzero refusal. `--id` is mutually exclusive with
101
+ `--last`, `--before`, `--since`, and `--limit`.
102
+
103
+ Cursor reads page the activity timeline; `--before` and `--since` are exclusive
104
+ sequence cursors. `--limit` defaults to 50 and accepts at most 5000 semantic
105
+ rows.
97
106
 
98
107
  ## Stop
99
108
 
@@ -116,5 +125,7 @@ keiyaku fork <aku/...|@alias> --at <historyId> [--alias @name]
116
125
  `fork` starts a child from one exact retained answered-turn coordinate and
117
126
  leaves the source untouched. It is a provider capability, not a guarantee:
118
127
  when the provider cannot fork from that turn, the command refuses rather than
119
- fabricating a fresh start. Take the `historyId` from `history` output; it must
120
- name an answered turn.
128
+ fabricating a fresh start. Pass the same public `historyId` exposed by status,
129
+ wait, or history; Heart privately resolves the provider coordinate. The ID must
130
+ name an answered outcome with a provider fork point. Failed outcomes are not
131
+ forkable.
@@ -0,0 +1,159 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
5
+
6
+ type Section = Readonly<{ kind?: string; value?: Readonly<{ rows?: readonly Record<string, unknown>[] }> }>;
7
+ type Report = Readonly<{
8
+ contracts?: Section;
9
+ tasks?: Section;
10
+ akuma?: Section;
11
+ }>;
12
+
13
+ type ExecResult = Readonly<{ stdout: string; stderr: string; code: number }>;
14
+
15
+ const REFRESH_TIMEOUT_MS = 10_000;
16
+
17
+ function rows(section: Section | undefined): readonly Record<string, unknown>[] {
18
+ return section?.kind === "present" && Array.isArray(section.value?.rows) ? section.value.rows : [];
19
+ }
20
+
21
+ function failed(section: Section | undefined): boolean {
22
+ return section?.kind === "failed";
23
+ }
24
+
25
+ function contractNeedsAttention(row: Record<string, unknown>): boolean {
26
+ const gates = row.gates;
27
+ if (typeof gates === "object" && gates !== null && "reports" in gates && Array.isArray(gates.reports)) {
28
+ for (const report of gates.reports) {
29
+ if (
30
+ typeof report === "object" &&
31
+ report !== null &&
32
+ "current" in report &&
33
+ typeof report.current === "object" &&
34
+ report.current !== null &&
35
+ "kind" in report.current &&
36
+ report.current.kind === "attested" &&
37
+ "verdict" in report.current &&
38
+ report.current.verdict === "unsatisfied"
39
+ )
40
+ return true;
41
+ }
42
+ }
43
+ return row.targetLag !== undefined &&
44
+ typeof row.targetLag === "object" &&
45
+ row.targetLag !== null &&
46
+ "kind" in row.targetLag
47
+ ? row.targetLag.kind === "unknown"
48
+ : false;
49
+ }
50
+
51
+ function taskNeedsAttention(row: Record<string, unknown>): boolean {
52
+ return row.disposition === "blocked";
53
+ }
54
+
55
+ function akumaNeedsAttention(row: Record<string, unknown>): boolean {
56
+ return row.life === "stillborn" || row.life === "hung" || row.life === "untidy";
57
+ }
58
+
59
+ function summary(report: Report): string {
60
+ const contractRows = rows(report.contracts);
61
+ const taskRows = rows(report.tasks);
62
+ const akumaRows = rows(report.akuma);
63
+ const attention =
64
+ Number(failed(report.contracts)) +
65
+ Number(failed(report.tasks)) +
66
+ Number(failed(report.akuma)) +
67
+ contractRows.filter(contractNeedsAttention).length +
68
+ taskRows.filter(taskNeedsAttention).length +
69
+ akumaRows.filter(akumaNeedsAttention).length;
70
+ const parts = [`Keiyaku · ${contractRows.length} contracts`, `${akumaRows.length} fleet`, `${taskRows.length} tasks`];
71
+ if (attention > 0) parts.push(`! ${attention}`);
72
+ return parts.join(" · ");
73
+ }
74
+
75
+ function parseReport(result: ExecResult): Report | undefined {
76
+ if (result.code !== 0 || result.stdout.trim().length === 0) return undefined;
77
+ try {
78
+ const value: unknown = JSON.parse(result.stdout);
79
+ return typeof value === "object" && value !== null ? (value as Report) : undefined;
80
+ } catch {
81
+ return undefined;
82
+ }
83
+ }
84
+
85
+ async function run(pi: ExtensionAPI, args: readonly string[], timeout = REFRESH_TIMEOUT_MS): Promise<ExecResult> {
86
+ const bundledCli = resolve(dirname(fileURLToPath(import.meta.url)), "../../src/cli/index.js");
87
+ if (existsSync(bundledCli)) return await pi.exec(process.execPath, [bundledCli, ...args], { timeout });
88
+ return await pi.exec("keiyaku", [...args], { timeout });
89
+ }
90
+
91
+ function lineForWidth(value: string, width: number): string {
92
+ const clean = value.replaceAll(/[\u0000-\u001f\u007f]/gu, " ");
93
+ return clean.length <= width ? clean : `${clean.slice(0, Math.max(0, width - 1))}…`;
94
+ }
95
+
96
+ function overlay(lines: readonly string[], theme: Theme, done: () => void) {
97
+ return {
98
+ invalidate(): void {},
99
+ handleInput(data: string): void {
100
+ if (data === "\u001b" || data === "q" || data === "Q" || data === "\r") done();
101
+ },
102
+ render(width: number): string[] {
103
+ const inner = Math.max(20, Math.min(100, width - 4));
104
+ const border = theme.fg("border", "+");
105
+ const rule = theme.fg("border", "-".repeat(inner));
106
+ return [
107
+ `${border}${rule}${border}`,
108
+ `${theme.fg("border", "|")}${lineForWidth("Keiyaku", inner).padEnd(inner)}${theme.fg("border", "|")}`,
109
+ `${theme.fg("border", "|")}${" ".repeat(inner)}${theme.fg("border", "|")}`,
110
+ ...lines.map(
111
+ (line) => `${theme.fg("border", "|")}${lineForWidth(line, inner).padEnd(inner)}${theme.fg("border", "|")}`,
112
+ ),
113
+ `${theme.fg("border", "|")}${" ".repeat(inner)}${theme.fg("border", "|")}`,
114
+ `${theme.fg("border", "|")}${lineForWidth("esc/q/enter close", inner).padEnd(inner)}${theme.fg("border", "|")}`,
115
+ `${border}${rule}${border}`,
116
+ ];
117
+ },
118
+ };
119
+ }
120
+
121
+ export default function keiyakuExtension(pi: ExtensionAPI): void {
122
+ let refreshInFlight: Promise<void> | undefined;
123
+
124
+ const refresh = async (ctx: Pick<ExtensionContext, "hasUI" | "ui">): Promise<void> => {
125
+ if (!ctx.hasUI) return;
126
+ refreshInFlight ??= (async () => {
127
+ const result = await run(pi, ["status", "--json"]);
128
+ const report = parseReport(result);
129
+ if (report === undefined) {
130
+ const diagnostic = result.stderr.trim() || "status unavailable";
131
+ ctx.ui.setWidget("keiyaku", [`Keiyaku · ${lineForWidth(diagnostic, 100)}`]);
132
+ } else ctx.ui.setWidget("keiyaku", [summary(report)]);
133
+ })().finally(() => {
134
+ refreshInFlight = undefined;
135
+ });
136
+ await refreshInFlight;
137
+ };
138
+
139
+ pi.on("session_start", async (_event, ctx) => {
140
+ await refresh(ctx);
141
+ });
142
+ pi.on("turn_end", async (_event, ctx) => {
143
+ await refresh(ctx);
144
+ });
145
+ pi.registerCommand("keiyaku", {
146
+ description: "Open the Kanshi world status",
147
+ handler: async (_args, ctx) => {
148
+ if (!ctx.hasUI) return;
149
+ const result = await run(pi, ["status"]);
150
+ const body =
151
+ result.code === 0 ? result.stdout.trimEnd().split("\n") : [result.stderr.trim() || "status unavailable"];
152
+ await ctx.ui.custom<void>((_tui, theme, _keybindings, done) => overlay(body, theme, done), {
153
+ overlay: true,
154
+ overlayOptions: { anchor: "center", width: 100, maxHeight: 40 },
155
+ });
156
+ await refresh(ctx);
157
+ },
158
+ });
159
+ }
@@ -2,10 +2,13 @@ export function abortable(operation, signal, disposeLate) {
2
2
  signal.throwIfAborted();
3
3
  return new Promise((resolve, reject) => {
4
4
  let aborted = false;
5
+ let settled = false;
5
6
  const abort = () => {
6
7
  aborted = true;
7
- if (disposeLate === undefined)
8
+ if (!settled) {
9
+ settled = true;
8
10
  reject(signal.reason);
11
+ }
9
12
  };
10
13
  signal.addEventListener("abort", abort, { once: true });
11
14
  if (signal.aborted)
@@ -13,19 +16,16 @@ export function abortable(operation, signal, disposeLate) {
13
16
  void operation.then(async (value) => {
14
17
  signal.removeEventListener("abort", abort);
15
18
  if (!aborted) {
19
+ settled = true;
16
20
  resolve(value);
17
21
  return;
18
22
  }
19
- try {
20
- await disposeLate?.(value);
21
- }
22
- catch (error) {
23
- reject(error);
24
- return;
25
- }
26
- reject(signal.reason);
23
+ void Promise.resolve(disposeLate?.(value)).catch(() => undefined);
27
24
  }, (error) => {
28
25
  signal.removeEventListener("abort", abort);
26
+ if (settled)
27
+ return;
28
+ settled = true;
29
29
  reject(aborted ? signal.reason : error);
30
30
  });
31
31
  });
@@ -1,7 +1,7 @@
1
1
  import { type TellResult } from "./body.js";
2
2
  import { type KillEvidence } from "./heart/index.js";
3
3
  import { type AkuId } from "./identity.js";
4
- import { type ActivityHistory } from "./projection.js";
4
+ import { type ActivityHistory, type ExactHistory } from "./projection.js";
5
5
  import type { AkumaCallExecution, AkumaStatus, ForkReceipt, InterruptReceipt } from "./akuma.js";
6
6
  import type { WorldRoot } from "../world.js";
7
7
  declare const CALL_EXECUTION: unique symbol;
@@ -11,12 +11,14 @@ export declare class AkumaHandle {
11
11
  readonly [CALL_EXECUTION]?: AkumaCallExecution;
12
12
  constructor(id: AkuId, worldPath: WorldRoot, execution?: AkumaCallExecution);
13
13
  private get paths();
14
+ private exactHistory;
14
15
  status(): Promise<AkumaStatus>;
15
16
  history(input?: Readonly<{
17
+ id?: string;
16
18
  before?: number;
17
19
  since?: number;
18
20
  limit?: number;
19
- }>): Promise<ActivityHistory>;
21
+ }>): Promise<ActivityHistory | ExactHistory>;
20
22
  wait(predicate?: (status: AkumaStatus) => boolean, options?: Readonly<{
21
23
  timeoutMs?: number;
22
24
  }>): Promise<AkumaStatus>;
@@ -1,8 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { CONTROL_RESPONSE_MS, wakeRecordedTell } from "./body.js";
3
3
  import { HeldAkumaLeash, activitySlice, readForkPoint, readHeart, readKill, readLastAnsweredTurn, readSoul, recordTell, requestPause, requestStop, } from "./heart/index.js";
4
- import { pathsForAkuId } from "./identity.js";
5
- import { projectTurns, selectHistory } from "./projection.js";
4
+ import { parsePublicHistoryId, pathsForAkuId } from "./identity.js";
5
+ import { projectTurns, selectHistory, selectExactHistory, } from "./projection.js";
6
6
  import { resolveProviderExecution } from "./providers/index.js";
7
7
  import { publishAkuma } from "./publication.js";
8
8
  import { spawnAkumaBody } from "./body.js";
@@ -83,10 +83,22 @@ export class AkumaHandle {
83
83
  get paths() {
84
84
  return pathsForAkuId(this.worldPath, this.id);
85
85
  }
86
+ async exactHistory(input) {
87
+ if (typeof input.id !== "string" || input.id.trim() === "")
88
+ throw new TypeError("Akuma history id must be a nonblank string");
89
+ if (input.before !== undefined || input.since !== undefined || input.limit !== undefined)
90
+ throw new TypeError("Akuma history id cannot be combined with before, since, or limit");
91
+ if (parsePublicHistoryId(input.id) === null)
92
+ throw new TypeError("Akuma history id must match turn/<positive safe integer>");
93
+ const slice = await activitySlice(this.paths);
94
+ return selectExactHistory(projectTurns(slice.rows, { lowestRetained: slice.lowestRetained, highest: slice.highest }).rows, input.id);
95
+ }
86
96
  async status() {
87
97
  return (await bornStatus(this.paths, this.id, { aperture: "monitoring" })).status;
88
98
  }
89
99
  async history(input = {}) {
100
+ if (input.id !== undefined)
101
+ return this.exactHistory(input);
90
102
  if (input.before !== undefined && input.since !== undefined) {
91
103
  throw new TypeError("Akuma history before and since are mutually exclusive");
92
104
  }
@@ -1,11 +1,36 @@
1
1
  import { AkumaHandle } from "./akuma-handle.js";
2
2
  import type { AkumaCallContext, AkumaCallInput, AkumaConfiguration, AkumaList, AkumaListInput } from "./akuma.js";
3
3
  import { CALL_WITH_CONTEXT } from "./akuma-product-symbols.js";
4
- import type { ResultRoute } from "./result-route.js";
5
4
  import type { WorldRoot } from "../world.js";
6
- type AkumaCallLaunchInput = AkumaCallInput & Readonly<{
7
- resultRoute?: ResultRoute;
5
+ import type { BodyLaunch } from "./body.js";
6
+ import type { AllocatedAkuma } from "./identity.js";
7
+ type AkumaCallRecipe = Omit<NonNullable<BodyLaunch["seed"]>, "id" | "archetype" | "cwd" | "origin">;
8
+ type BornExecution = Readonly<{
9
+ cwd: string;
10
+ source: "input" | "caller" | "process" | "world";
8
11
  }>;
12
+ export type BornAkumaCall = Readonly<{
13
+ kind: "born";
14
+ allocated: AllocatedAkuma;
15
+ seed: AkumaCallRecipe & Readonly<{
16
+ id: AllocatedAkuma["id"];
17
+ archetype: string;
18
+ cwd: string;
19
+ origin: {
20
+ kind: "direct";
21
+ };
22
+ }>;
23
+ initialBody: string;
24
+ execution: BornExecution;
25
+ }>;
26
+ export type RequestedAkumaCall = Readonly<{
27
+ kind: "requested";
28
+ id: AllocatedAkuma["id"];
29
+ cwd: string;
30
+ execution: BornExecution;
31
+ }>;
32
+ export type AkumaBornCall = BornAkumaCall | RequestedAkumaCall;
33
+ type AkumaCallLaunchInput = AkumaCallInput;
9
34
  export declare class Akuma {
10
35
  private readonly path;
11
36
  private readonly configuration;
@@ -16,8 +41,12 @@ export declare class Akuma {
16
41
  }>): AkumaHandle;
17
42
  listArchetypes(): Promise<readonly string[]>;
18
43
  call(input: AkumaCallLaunchInput): Promise<AkumaHandle>;
44
+ beginCall(input: AkumaCallLaunchInput, context: AkumaCallContext): Promise<AkumaBornCall>;
45
+ finishCall(born: AkumaBornCall): Promise<AkumaHandle>;
19
46
  [CALL_WITH_CONTEXT](input: AkumaCallLaunchInput, context: AkumaCallContext): Promise<AkumaHandle>;
20
47
  list(input?: AkumaListInput): Promise<AkumaList>;
21
48
  }
22
49
  export declare function callAkumaWithContext(akuma: Akuma, input: AkumaCallLaunchInput, context: AkumaCallContext): Promise<AkumaHandle>;
50
+ export declare function beginAkumaCall(akuma: Akuma, input: AkumaCallLaunchInput, context: AkumaCallContext): Promise<AkumaBornCall>;
51
+ export declare function finishAkumaCall(akuma: Akuma, born: AkumaBornCall): Promise<AkumaHandle>;
23
52
  export {};
@@ -6,7 +6,7 @@ import { CALL_WITH_CONTEXT } from "./akuma-product-symbols.js";
6
6
  import { fleetListRow, readAkumaBirthCwd } from "./akuma-observe.js";
7
7
  import { akuIdFromDirectoryName, akumaPaths, akumaRunRoot, archetypeName, parseAkuId } from "./identity.js";
8
8
  import { loadArchetype, listArchetypes as readArchetypes } from "./archetype.js";
9
- import { publishAkuma } from "./publication.js";
9
+ import { birthAkuma, launchAkuma } from "./publication.js";
10
10
  import { spawnAkumaBody } from "./body.js";
11
11
  import { injectedBodyRequests, requestBodyCall } from "./requests.js";
12
12
  import { decodeAllowedActions, unionAllowedActions } from "./allowed.js";
@@ -51,7 +51,7 @@ export class Akuma {
51
51
  async call(input) {
52
52
  return await this[CALL_WITH_CONTEXT](input, { initiatorCwd: process.cwd() });
53
53
  }
54
- async [CALL_WITH_CONTEXT](input, context) {
54
+ async beginCall(input, context) {
55
55
  const readonly = callReadonly(input.readonly);
56
56
  const name = archetypeName(input.archetype);
57
57
  const home = this.configuration.home === undefined ? {} : { home: this.configuration.home };
@@ -84,32 +84,50 @@ export class Akuma {
84
84
  recipe: requestRecipe,
85
85
  });
86
86
  const bornCwd = await readAkumaBirthCwd(this.path, child);
87
- return new AkumaHandle(child, this.path, { cwd: bornCwd, source: cwd === undefined ? "caller" : "input" });
87
+ return {
88
+ kind: "requested",
89
+ id: child,
90
+ cwd: bornCwd,
91
+ execution: { cwd: bornCwd, source: cwd === undefined ? "caller" : "input" },
92
+ };
88
93
  }
89
94
  const initiatorCwd = context.initiatorCwd;
90
95
  const selectedCwd = input.cwd ?? initiatorCwd ?? this.path;
91
96
  const cwd = input.cwd !== undefined && context?.cwdCanonical === true ? input.cwd : await canonicalBirthCwd(selectedCwd);
92
- const published = await publishAkuma({
93
- worldPath: this.path,
94
- archetype: archetype.name,
95
- launch: async (allocated) => await spawnAkumaBody({
96
- paths: allocated.paths,
97
- seed: {
98
- id: allocated.id,
99
- archetype: allocated.archetype,
100
- ...requestRecipe,
101
- cwd,
102
- origin: { kind: "direct" },
103
- },
104
- initialBody: input.body,
105
- ...(input.resultRoute === undefined ? {} : { resultRoute: input.resultRoute }),
106
- }),
97
+ const allocated = await birthAkuma({ worldPath: this.path, archetype: archetype.name });
98
+ return {
99
+ kind: "born",
100
+ allocated,
101
+ seed: {
102
+ id: allocated.id,
103
+ archetype: allocated.archetype,
104
+ ...requestRecipe,
105
+ cwd,
106
+ origin: { kind: "direct" },
107
+ },
108
+ initialBody: input.body,
109
+ execution: {
110
+ cwd,
111
+ source: input.cwd !== undefined ? "input" : initiatorCwd === undefined ? "world" : "process",
112
+ },
113
+ };
114
+ }
115
+ async finishCall(born) {
116
+ if (born.kind === "requested") {
117
+ return new AkumaHandle(born.id, this.path, { cwd: born.cwd, source: born.execution.source });
118
+ }
119
+ const published = await launchAkuma({
120
+ allocated: born.allocated,
121
+ launch: async (allocated) => await spawnAkumaBody({ paths: allocated.paths, seed: born.seed, initialBody: born.initialBody }),
107
122
  });
108
123
  return new AkumaHandle(published.id, this.path, {
109
- cwd,
110
- source: input.cwd !== undefined ? "input" : initiatorCwd === undefined ? "world" : "process",
124
+ cwd: born.execution.cwd,
125
+ source: born.execution.source,
111
126
  });
112
127
  }
128
+ async [CALL_WITH_CONTEXT](input, context) {
129
+ return await this.finishCall(await this.beginCall(input, context));
130
+ }
113
131
  async list(input = {}) {
114
132
  if (typeof input !== "object" || input === null || Array.isArray(input))
115
133
  throw new TypeError("Akuma list input must be an object");
@@ -153,3 +171,9 @@ export class Akuma {
153
171
  export async function callAkumaWithContext(akuma, input, context) {
154
172
  return await akuma[CALL_WITH_CONTEXT](input, context);
155
173
  }
174
+ export async function beginAkumaCall(akuma, input, context) {
175
+ return await akuma.beginCall(input, context);
176
+ }
177
+ export async function finishAkumaCall(akuma, born) {
178
+ return await akuma.finishCall(born);
179
+ }
@@ -62,7 +62,6 @@ export type AkumaCallContext = Readonly<{
62
62
  cwdCanonical?: true;
63
63
  }>;
64
64
  export type { TellResult, TellWake } from "./body.js";
65
- export type { ResultRoute } from "./result-route.js";
66
65
  export type InterruptReceipt = Readonly<{
67
66
  kind: "unavailable";
68
67
  evidence: "hung" | "untidy" | "unavailable";
@@ -1,7 +1,6 @@
1
1
  import { type SessionFact, type Soul } from "./heart/index.js";
2
- import type { AkumaPaths } from "./identity.js";
2
+ import { type AkumaPaths } from "./identity.js";
3
3
  import type { ProviderAdapter } from "./provider.js";
4
- import type { ResultRoute } from "./result-route.js";
5
4
  import { type UpstreamExecutionPort, type RequestChildLaunch } from "./request-serve.js";
6
5
  import { type OwnedProcess, type RunLogReference } from "../runtime/proc/run.js";
7
6
  export declare const LEASH_HELD_EXIT = 75;
@@ -12,7 +11,6 @@ export type BodyLaunch = Readonly<{
12
11
  birthSession?: Omit<SessionFact, "sequence">;
13
12
  initialBody?: string;
14
13
  refuseIfHeld?: boolean;
15
- resultRoute?: ResultRoute;
16
14
  }>;
17
15
  export type TellWake = Readonly<{
18
16
  kind: "told";
@@ -45,10 +43,6 @@ type BodyRuntime = Readonly<{
45
43
  now(): string;
46
44
  spawnChild?(launch: RequestChildLaunch): Promise<OwnedProcess>;
47
45
  spawnBody?(launch: BodyLaunch): Promise<OwnedProcess>;
48
- express?(route: ResultRoute, options: Readonly<{
49
- as: string;
50
- body: string;
51
- }>): Promise<unknown>;
52
46
  upstream?: UpstreamExecutionPort;
53
47
  }>;
54
48
  export declare function bodyProcessInput(launch: BodyLaunch, bodyModuleUrl?: string): Promise<{
@@ -4,6 +4,8 @@ import { abortableDelay } from "./abort.js";
4
4
  import { BodySupervisor } from "./body-supervisor.js";
5
5
  import { driveTurn, turnRecipe } from "./turn-drive.js";
6
6
  import { HeldAkumaLeash, breakBody, endTurn, finishBodyIfIdle, heartExists, isHeartAbsent, readHeart, readNonterminalRequests, watchHeart, } from "./heart/index.js";
7
+ import { worldRootForAkumaPaths } from "./identity.js";
8
+ import { keiyakuSquarePath } from "../world.js";
7
9
  import { resolveProviderExecution } from "./providers/index.js";
8
10
  import { clearBodyRequestTransport, settleBodyRequests, } from "./request-serve.js";
9
11
  import { spawnDetachedProcess, } from "../runtime/proc/run.js";
@@ -76,23 +78,37 @@ async function persistTurn(paths, turnSequence, result, completedAt) {
76
78
  await endTurn(paths, { turnSequence, outcome: { kind: "failed", diagnostic }, completedAt });
77
79
  return { outcome: "failed", diagnostic };
78
80
  }
81
+ const OUTCOME_PREVIEW_LIMIT = 1_000;
82
+ function outcomePreview(identity, outcome) {
83
+ const text = outcome.outcome === "answered" ? outcome.answer : outcome.diagnostic;
84
+ if (text.length <= OUTCOME_PREVIEW_LIMIT)
85
+ return text;
86
+ return `${text.slice(0, OUTCOME_PREVIEW_LIMIT)}\n\nkeiyaku history ${identity} --last`;
87
+ }
79
88
  function boundedDiagnostic(error) {
80
89
  const text = diagnostic(error);
81
90
  return text.length <= 500 ? text : `${text.slice(0, 500)}...`;
82
91
  }
83
- async function expressInitialOutcome(launch, soul, outcome, runtime) {
84
- if (launch.resultRoute === undefined)
85
- return;
92
+ async function expressInitialOutcome(launch, outcome) {
86
93
  try {
87
- const express = runtime.express ?? (await import("@astrosheep/square")).express;
88
- await express(launch.resultRoute, {
89
- as: "keiyaku",
90
- body: JSON.stringify({ akuma: soul.id, ...outcome }),
91
- });
94
+ const { Square } = await import("@astrosheep/square");
95
+ const identity = launch.seed?.id ?? (await readHeart(launch.paths)).soul?.id;
96
+ if (identity === undefined)
97
+ return;
98
+ const square = await Square.at({ path: keiyakuSquarePath(worldRootForAkumaPaths(launch.paths)) });
99
+ try {
100
+ const joined = await square.implicitJoin(identity);
101
+ if (joined.state === "done" || joined.participant === undefined)
102
+ return;
103
+ await joined.participant.express(outcomePreview(identity, outcome));
104
+ }
105
+ finally {
106
+ await square.close();
107
+ }
92
108
  }
93
109
  catch (error) {
94
110
  try {
95
- await appendFile(launch.paths.log, `result-route express failed: ${boundedDiagnostic(error)}\n`);
111
+ await appendFile(launch.paths.log, `square outcome express failed: ${boundedDiagnostic(error)}\n`);
96
112
  }
97
113
  catch {
98
114
  /* express loss never changes Heart truth */
@@ -201,7 +217,7 @@ async function runBodyTurns(input) {
201
217
  }
202
218
  const outcome = await persistTurn(launch.paths, result.turnSequence, result, runtime.now());
203
219
  if (initial !== undefined)
204
- await expressInitialOutcome(launch, soul, outcome, runtime);
220
+ await expressInitialOutcome(launch, outcome);
205
221
  if (outcome.outcome === "failed") {
206
222
  await breakBody(launch.paths, { sequence: bodySequence, end: "broke-off", at: runtime.now() });
207
223
  return;
@@ -328,8 +344,14 @@ async function awaitWake(paths, tellId, after, child, changed) {
328
344
  if (winner.kind === "exited") {
329
345
  return winner.exit.code === LEASH_HELD_EXIT ? { kind: "held" } : failedChild(winner.exit);
330
346
  }
331
- await child.terminate();
332
- await child.exited;
347
+ try {
348
+ await child.terminate();
349
+ }
350
+ catch (error) {
351
+ /* Termination rejection leaves physical custody unresolved; release it deliberately. */
352
+ child.release();
353
+ return { kind: "failed", diagnostic: diagnostic(error) };
354
+ }
333
355
  return winner.kind === "observer-failed"
334
356
  ? { kind: "failed", diagnostic: diagnostic(winner.error) }
335
357
  : { kind: "failed", diagnostic: "Heart observer closed" };
@@ -339,11 +361,11 @@ async function wakePendingTells(paths, tellId, runtime) {
339
361
  const watching = new AbortController();
340
362
  let changed;
341
363
  try {
364
+ changed = await runtime.observeHeart(paths, watching.signal);
342
365
  const beforeHeart = await readHeart(paths);
343
366
  if (tellId === undefined && beforeHeart.pending.length === 0)
344
367
  return null;
345
368
  const before = beforeHeart.latestBody?.sequence ?? 0;
346
- changed = await runtime.observeHeart(paths, watching.signal);
347
369
  return await awaitWake(paths, tellId, before, await runtime.spawn(paths), changed);
348
370
  }
349
371
  catch (error) {
@@ -1,7 +1,8 @@
1
+ import { parsePublicHistoryId } from "../identity.js";
1
2
  import { answeredTurnFact, endBodyFact, finishBodyFact, insertActivityFact, insertTurnEndFact, insertTurnStartFact, insertPauseControl, insertSessionFact, insertStopControl, killFactForBody, latestBodyFact, latestKillFact, latestSessionFact, lastAnsweredTurnFact, pauseFact, sessionFactForCoordinate, stopFact, } from "./rows.js";
2
3
  import { insertTellDeliveryFact, insertTellFact, insertTellReceiptFact, pendingTellFacts, tellFact, tellIdsForFence, } from "./tells.js";
3
4
  import { activityFactSlice, lastActivityAt as readLastActivityAt, pruneActivityFacts, } from "./timeline.js";
4
- import { isHeartAbsent, readSealFromLeash, readTransaction, transaction, withHeart } from "./storage.js";
5
+ import { isHeartAbsent, readSealFromLeash, readTransaction, transaction, withHeart, withReadOnlyHeart, } from "./storage.js";
5
6
  import { soulFact } from "./soul.js";
6
7
  export { HeartAbsentError, HeldAkumaLeash, classifyHeartSchema, initializeHeart, isHeartAbsent, probeLeash, watchHeart, } from "./storage.js";
7
8
  export { admitRequest, readNonterminalRequests, readRequest, refuseRequest, reserveRequest, serveRequest, serveUpstreamRequest, voidRequest, } from "./request-authority.js";
@@ -162,7 +163,7 @@ export async function finishBodyIfIdle(paths, input) {
162
163
  }
163
164
  export async function readHeart(paths) {
164
165
  try {
165
- return await withHeart(paths, (heart) => readTransaction(heart, () => ({
166
+ return await withReadOnlyHeart(paths, (heart) => readTransaction(heart, () => ({
166
167
  soul: soulFact(heart),
167
168
  latestBody: latestBodyFact(heart),
168
169
  latestSession: latestSessionFact(heart),
@@ -193,9 +194,12 @@ export async function readLastAnsweredTurn(paths) {
193
194
  return await withHeart(paths, lastAnsweredTurnFact);
194
195
  }
195
196
  export async function readForkPoint(paths, historyId) {
197
+ const turnSequence = parsePublicHistoryId(historyId);
198
+ if (turnSequence === null)
199
+ return null;
196
200
  try {
197
201
  return await withHeart(paths, (heart) => {
198
- const turn = answeredTurnFact(heart, historyId);
202
+ const turn = answeredTurnFact(heart, turnSequence);
199
203
  const outcome = turn?.end?.outcome;
200
204
  if (outcome?.kind !== "answered" || outcome.historyId === undefined)
201
205
  return null;