@astrosheep/keiyaku 4.6.19 → 4.6.21

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 (102) 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/index.d.ts +3 -1
  87. package/build/src/index.js +1 -0
  88. package/build/src/library/akuma-creation.d.ts +7 -3
  89. package/build/src/library/akuma-creation.js +23 -11
  90. package/build/src/library/akumas.d.ts +4 -1
  91. package/build/src/library/akumas.js +13 -5
  92. package/build/src/library/selection.d.ts +2 -1
  93. package/build/src/library/selection.js +6 -3
  94. package/build/src/plugin/public.d.ts +11 -0
  95. package/build/src/plugin/runtime.js +1 -0
  96. package/build/src/runtime/proc/windows-launch.exe +0 -0
  97. package/package.json +3 -3
  98. package/plugins/paseo/plugin/server/keiyaku.ts +1 -0
  99. package/plugins/paseo/plugin/server/mirror.ts +2 -2
  100. package/plugins/pi/README.md +111 -0
  101. package/plugins/pi/akuma-tools.ts +310 -0
  102. package/plugins/pi/keiyaku.ts +2 -0
@@ -1,5 +1,25 @@
1
1
  import { realpath, stat } from "node:fs/promises";
2
- import { resolve } from "node:path";
2
+ import { isAbsolute, resolve } from "node:path";
3
+ export function bareOption(value) {
4
+ if (value !== undefined && typeof value !== "boolean")
5
+ throw new TypeError("bare must be a boolean");
6
+ return value;
7
+ }
8
+ export async function writableDirectoriesOption(value) {
9
+ if (value === undefined)
10
+ return [];
11
+ if (!Array.isArray(value))
12
+ throw new TypeError("writableDirectories must be an array of canonical directories");
13
+ const directories = await Promise.all(value.map(async (path) => {
14
+ if (typeof path !== "string" || path.trim() === "" || !isAbsolute(path))
15
+ throw new TypeError("writableDirectories must contain nonblank canonical absolute directories");
16
+ const canonical = await canonicalBirthCwd(path);
17
+ if (canonical !== path)
18
+ throw new TypeError(`writableDirectories path is not canonical: ${path}`);
19
+ return canonical;
20
+ }));
21
+ return Object.freeze([...new Set(directories)]);
22
+ }
3
23
  export async function canonicalBirthCwd(input, diagnostic = `cwd is not an existing directory: ${input}`) {
4
24
  const selected = resolve(input);
5
25
  try {
@@ -2,7 +2,7 @@ import { type Soul } from "./heart/index.js";
2
2
  import { type AkuId, type AkumaPaths } from "./identity.js";
3
3
  import { type CallInitialTell, type CallInitialTellAdmission } from "./call-initial-tell.js";
4
4
  import { type ChildRequestCommand, type ErasedRequestCommand, type RequestProtocol } from "./request-wire.js";
5
- import type { OwnedProcess } from "../runtime/proc/run.js";
5
+ import type { LiveBodyLaunch } from "./body-launch.js";
6
6
  import { z } from "zod";
7
7
  import type { WorldRoot } from "../world.js";
8
8
  declare const akumaCallRecipeSchema: z.ZodObject<{
@@ -71,6 +71,7 @@ declare const akumaCallRecipeSchema: z.ZodObject<{
71
71
  systemPrompt?: string | undefined;
72
72
  systemPromptMode?: "replace" | "append" | undefined;
73
73
  }>>;
74
+ bare: z.ZodOptional<z.ZodBoolean>;
74
75
  }, z.core.$strict>;
75
76
  declare const akumaCallPayloadSchema: z.ZodObject<{
76
77
  world: z.ZodString;
@@ -84,6 +85,7 @@ declare const akumaCallPayloadSchema: z.ZodObject<{
84
85
  birthInitiator: z.ZodOptional<z.ZodString>;
85
86
  contractId: z.ZodOptional<z.ZodString>;
86
87
  cwd: z.ZodOptional<z.ZodString>;
88
+ writableDirectories: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodString>>>;
87
89
  recipe: z.ZodObject<{
88
90
  description: z.ZodOptional<z.ZodString>;
89
91
  allowed: z.ZodPipe<z.ZodArray<z.ZodEnum<{
@@ -150,6 +152,7 @@ declare const akumaCallPayloadSchema: z.ZodObject<{
150
152
  systemPrompt?: string | undefined;
151
153
  systemPromptMode?: "replace" | "append" | undefined;
152
154
  }>>;
155
+ bare: z.ZodOptional<z.ZodBoolean>;
153
156
  }, z.core.$strict>;
154
157
  }, z.core.$strict>;
155
158
  export type AkumaCallRecipe = z.infer<typeof akumaCallRecipeSchema>;
@@ -167,13 +170,14 @@ export type AkumaCallRequestChildLaunch = Readonly<{
167
170
  export type InitialTellAdmissionRequest = Readonly<{
168
171
  id: AkuId;
169
172
  initialTell: CallInitialTell;
173
+ contractId?: string;
170
174
  signal: AbortSignal;
171
175
  }>;
172
176
  type AkumaCallRequestCapabilities = Readonly<{
173
177
  world: WorldRoot;
174
178
  paths: AkumaPaths;
175
179
  parent: Soul;
176
- spawn(launch: AkumaCallRequestChildLaunch): Promise<OwnedProcess | void>;
180
+ spawn(launch: AkumaCallRequestChildLaunch): Promise<LiveBodyLaunch | void>;
177
181
  admitInitialTell(input: InitialTellAdmissionRequest): Promise<CallInitialTellAdmission>;
178
182
  }>;
179
183
  declare const liveResultSchema: z.ZodObject<{
@@ -25,6 +25,7 @@ const akumaCallRecipeSchema = z
25
25
  allowed: allowedActionsSchema,
26
26
  provider: providerRecipeSchema,
27
27
  options: providerOptionsSchema,
28
+ bare: z.boolean().optional(),
28
29
  })
29
30
  .strict();
30
31
  const initialTellSchema = z
@@ -43,6 +44,7 @@ const akumaCallPayloadSchema = z
43
44
  birthInitiator: z.string().optional(),
44
45
  contractId: z.string().optional(),
45
46
  cwd: absolutePathSchema.optional(),
47
+ writableDirectories: z.array(absolutePathSchema).readonly().optional(),
46
48
  recipe: akumaCallRecipeSchema,
47
49
  })
48
50
  .strict();
@@ -68,6 +70,7 @@ async function executeAkumaCall(request, facts, capabilities) {
68
70
  const outcome = await executePreparedCall({
69
71
  archetype: request.archetype,
70
72
  cwd: request.cwd ?? parent.cwd,
73
+ ...(request.writableDirectories === undefined ? {} : { writableDirectories: request.writableDirectories }),
71
74
  ...(request.initialTell === undefined
72
75
  ? {}
73
76
  : {
@@ -88,6 +91,7 @@ async function executeAkumaCall(request, facts, capabilities) {
88
91
  provider: request.recipe.provider,
89
92
  options: request.recipe.options,
90
93
  allowed: request.recipe.allowed,
94
+ ...(request.recipe.bare === undefined ? {} : { bare: request.recipe.bare }),
91
95
  },
92
96
  parent,
93
97
  requestId: facts.id,
@@ -103,7 +107,11 @@ async function executeAkumaCall(request, facts, capabilities) {
103
107
  ...(request.birthInitiator === undefined ? {} : { birthInitiator: request.birthInitiator }),
104
108
  ...(request.contractId === undefined ? {} : { completion: { contractId: request.contractId } }),
105
109
  }),
106
- admitInitialTell,
110
+ admitInitialTell: async (input) => await admitInitialTell({
111
+ ...input,
112
+ signal: facts.signal,
113
+ ...(request.contractId === undefined ? {} : { contractId: request.contractId }),
114
+ }),
107
115
  },
108
116
  });
109
117
  return {
@@ -0,0 +1,9 @@
1
+ import type { BodyLaunch, LiveBodyLaunch } from "./body-launch.js";
2
+ export declare const LEASH_HELD_EXIT = 75;
3
+ export declare function bodyProcessInput(launch: BodyLaunch, bodyModuleUrl?: string): Promise<{
4
+ argv: string[];
5
+ cwd: string;
6
+ env: Record<string, string>;
7
+ log: string;
8
+ }>;
9
+ export declare function spawnAkumaBody(launch: BodyLaunch): Promise<LiveBodyLaunch>;
@@ -0,0 +1,94 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+ import { spawnDetachedProcess } from "../runtime/proc/run.js";
4
+ import { readHeart } from "./heart/index.js";
5
+ import { worldRootForAkumaPaths } from "./identity.js";
6
+ import { akumaExecutionEnvironment } from "./providers/execution-environment.js";
7
+ export const LEASH_HELD_EXIT = 75;
8
+ async function launchCwd(launch) {
9
+ if (launch.seed !== undefined)
10
+ return launch.seed.cwd;
11
+ const soul = (await readHeart(launch.paths)).soul;
12
+ if (soul === null)
13
+ throw new Error("Akuma wake has no born soul");
14
+ return soul.cwd;
15
+ }
16
+ async function executableMissing(path) {
17
+ try {
18
+ return !(await stat(path)).isFile();
19
+ }
20
+ catch {
21
+ return true;
22
+ }
23
+ }
24
+ async function frozenCwdMissing(path) {
25
+ try {
26
+ await stat(path);
27
+ return false;
28
+ }
29
+ catch {
30
+ return true;
31
+ }
32
+ }
33
+ /** Diagnose the attempted executable and frozen cwd independently; never substitute either. */
34
+ async function diagnoseSpawnEnoent(input, cause) {
35
+ const executable = input.argv[0] ?? process.execPath;
36
+ const cwd = input.cwd;
37
+ const [noExecutable, noCwd] = await Promise.all([executableMissing(executable), frozenCwdMissing(cwd)]);
38
+ if (!noExecutable && !noCwd)
39
+ return cause instanceof Error ? cause : new Error(String(cause));
40
+ const missing = [];
41
+ if (noCwd)
42
+ missing.push(`frozen execution directory ${cwd} no longer exists`);
43
+ if (noExecutable)
44
+ missing.push(`waking process executable ${executable} no longer exists (the host likely removed the running process binary)`);
45
+ const remedies = [];
46
+ if (noCwd)
47
+ remedies.push("a missing execution directory likely means its Contract was accepted and the worktree cleaned up, with deliverables already in Git");
48
+ if (noExecutable)
49
+ remedies.push("a missing executable likely means the host removed the running process binary: re-enter with a fresh CLI process and retry");
50
+ const remedy = `kill this Akuma and call a fresh one; ${remedies.join("; ")}`;
51
+ const error = new Error(`Body spawn failed: ${missing.join(" and ")}; ${remedy}`, { cause });
52
+ error.code = "ENOENT";
53
+ return error;
54
+ }
55
+ export async function bodyProcessInput(launch, bodyModuleUrl = import.meta.url) {
56
+ const encoded = Buffer.from(JSON.stringify(launch), "utf8").toString("base64url");
57
+ const actorId = launch.seed?.id ?? (await readHeart(launch.paths)).soul?.id;
58
+ if (actorId === undefined)
59
+ throw new Error("Akuma wake has no born soul");
60
+ const executable = process.execPath;
61
+ const source = bodyModuleUrl.endsWith(".ts");
62
+ const entry = fileURLToPath(new URL(source ? "../akuma-body.ts" : "../akuma-body.js", bodyModuleUrl));
63
+ return {
64
+ argv: source ? [executable, "--import", import.meta.resolve("tsx"), entry, encoded] : [executable, entry, encoded],
65
+ cwd: await launchCwd(launch),
66
+ env: akumaExecutionEnvironment(process.env, {
67
+ KEIYAKU_ACTOR_ID: actorId,
68
+ KEIYAKU_ACTOR_WORLD: worldRootForAkumaPaths(launch.paths),
69
+ }),
70
+ log: launch.paths.log,
71
+ };
72
+ }
73
+ export async function spawnAkumaBody(launch) {
74
+ const input = await bodyProcessInput(launch);
75
+ try {
76
+ const owned = await spawnDetachedProcess(input);
77
+ const completed = owned.exited.then((exit) => ({
78
+ diagnostic: exit.code === null ? `pre-admission signal ${exit.signal ?? "unknown"}` : `pre-admission exit ${exit.code}`,
79
+ held: exit.code === LEASH_HELD_EXIT,
80
+ child: { code: exit.code, signal: exit.signal, log: exit.log },
81
+ }));
82
+ void completed.catch(() => undefined);
83
+ return {
84
+ completed,
85
+ cancel: (force) => owned.terminate(force),
86
+ release: () => owned.release(),
87
+ };
88
+ }
89
+ catch (error) {
90
+ if (error?.code !== "ENOENT")
91
+ throw error;
92
+ throw await diagnoseSpawnEnoent(input, error);
93
+ }
94
+ }
@@ -0,0 +1,8 @@
1
+ import type { Soul } from "./heart/index.js";
2
+ /** Association is supplied by composition, never read or persisted by Akuma. */
3
+ export type ExecutionAssociation = Readonly<{
4
+ contractId: string;
5
+ }>;
6
+ export type ExecutionAssociationReader = () => Promise<ExecutionAssociation | undefined>;
7
+ /** One current execution section; provider adapters only choose its native channel. */
8
+ export declare function keiyakuExecutionInstructions(soul: Soul, association?: ExecutionAssociation): string | undefined;
@@ -0,0 +1,17 @@
1
+ /** One current execution section; provider adapters only choose its native channel. */
2
+ export function keiyakuExecutionInstructions(soul, association) {
3
+ if (soul.bare !== false) {
4
+ return soul.bare === true && soul.origin.kind === "fork"
5
+ ? "Any inherited Keiyaku execution context is inactive for this execution."
6
+ : undefined;
7
+ }
8
+ return [
9
+ "<keiyaku>",
10
+ "This context supersedes any earlier Keiyaku execution context.",
11
+ `Akuma: ${soul.id}`,
12
+ `Archetype: ${soul.archetype}`,
13
+ ...(association === undefined ? [] : [`Contract context (not a Dispatch receipt): ${association.contractId}`]),
14
+ 'Use `keiyaku report "..."` to share updates without stopping your work.',
15
+ "</keiyaku>",
16
+ ].join("\n");
17
+ }
@@ -30,9 +30,10 @@ export type Soul = Readonly<{
30
30
  origin: AkumaOrigin;
31
31
  caller?: AkumaCaller;
32
32
  allowed: AllowedActions;
33
+ bare?: boolean;
33
34
  createdAt: string;
34
35
  }>;
35
- export type RequestRecipe = Pick<Soul, "description" | "provider" | "options" | "allowed">;
36
+ export type RequestRecipe = Pick<Soul, "description" | "provider" | "options" | "allowed" | "bare">;
36
37
  export type BodyEnd = "exited" | "broke-off" | "put-down";
37
38
  export type BodyFact = Readonly<{
38
39
  sequence: number;
@@ -60,6 +61,8 @@ export type TurnOutcome = Readonly<{
60
61
  session: ResumeCoordinate;
61
62
  answer: string;
62
63
  answerJson?: string;
64
+ }> | Readonly<{
65
+ kind: "canceled";
63
66
  }> | Readonly<{
64
67
  kind: "failed";
65
68
  diagnostic: string;
@@ -88,6 +91,23 @@ export type TurnEndFact = Readonly<{
88
91
  export type TurnFact = TurnStartFact & Readonly<{
89
92
  end?: TurnEndFact;
90
93
  }>;
94
+ export declare const reportFactSchema: z.ZodObject<{
95
+ kind: z.ZodLiteral<"report">;
96
+ sequence: z.ZodNumber;
97
+ bodySequence: z.ZodNumber;
98
+ turnSequence: z.ZodNumber;
99
+ text: z.ZodString;
100
+ at: z.ZodString;
101
+ }, z.core.$strict>;
102
+ export type ReportFact = z.infer<typeof reportFactSchema>;
103
+ export type ReportAdmission = Readonly<{
104
+ kind: "recorded";
105
+ report: ReportFact;
106
+ initiator?: string;
107
+ }> | Readonly<{
108
+ kind: "refused";
109
+ reason: "not-born" | "not-current-execution";
110
+ }>;
91
111
  export type CallFact = Readonly<{
92
112
  kind: "call";
93
113
  sequence: number;
@@ -12,6 +12,16 @@ export const akumaOriginSchema = z.discriminatedUnion("kind", [
12
12
  z.object({ kind: z.literal("request"), parent: akumaIdSchema, requestId: originText }),
13
13
  z.object({ kind: z.literal("fork"), parent: akumaIdSchema, at: originText }),
14
14
  ]);
15
+ export const reportFactSchema = z
16
+ .object({
17
+ kind: z.literal("report"),
18
+ sequence: z.number().int().positive().safe(),
19
+ bodySequence: z.number().int().positive().safe(),
20
+ turnSequence: z.number().int().positive().safe(),
21
+ text: z.string().refine((value) => value.trim() !== "", "expected nonblank report text"),
22
+ at: z.string().refine((value) => Number.isFinite(Date.parse(value)), "expected report timestamp"),
23
+ })
24
+ .strict();
15
25
  export function projectTell(fact) {
16
26
  return {
17
27
  kind: "tell",
@@ -1,5 +1,5 @@
1
1
  import type { AkumaPaths } from "../identity.js";
2
- import type { BodyEnd, BodyFact, ForkPoint, HeartSnapshot, InterruptCompletion, KillFact, PauseFact, SealFact, SessionFact, Soul, TellDeliveryInput, TellFact, TellReceiptInput, TurnEndFact, TurnFact, TurnOutcome, TurnStartFact } from "./facts.js";
2
+ import type { BodyEnd, BodyFact, ForkPoint, HeartSnapshot, InterruptCompletion, KillFact, PauseFact, SealFact, SessionFact, Soul, TellDeliveryInput, TellFact, TellReceiptInput, TurnEndFact, TurnFact, TurnOutcome, TurnStartFact, ReportAdmission } from "./facts.js";
3
3
  import type { AgentEvent } from "./activity-schema.js";
4
4
  import { type StatusFactInput, type ActivityFactSlice } from "./timeline.js";
5
5
  import { type HeartRead } from "./storage.js";
@@ -112,6 +112,12 @@ export declare function recordTell(paths: AkumaPaths, tell: TellInput, options?:
112
112
  interrupt?: false;
113
113
  signal?: AbortSignal;
114
114
  }>): Promise<TellAdmission>;
115
+ export declare function recordReport(paths: AkumaPaths, input: Readonly<{
116
+ bodySequence: number;
117
+ turnSequence: number;
118
+ text: string;
119
+ at: string;
120
+ }>, signal?: AbortSignal): Promise<ReportAdmission>;
115
121
  export declare function readTell(paths: AkumaPaths, id: string): Promise<TellFact | null>;
116
122
  export declare function recordTellDeliveries(paths: AkumaPaths, inputs: readonly TellDeliveryInput[]): Promise<void>;
117
123
  export declare function recordTellReceipt(paths: AkumaPaths, input: TellReceiptInput): Promise<void>;
@@ -143,6 +149,15 @@ export declare function breakBody(paths: AkumaPaths, input: Readonly<{
143
149
  end: Exclude<BodyEnd, "exited">;
144
150
  at: string;
145
151
  }>): Promise<void>;
152
+ /** The owning Body calls this only after proving its provider custody retired. */
153
+ export declare function cancelBodyTurns(paths: AkumaPaths, input: Readonly<{
154
+ bodySequence: number;
155
+ at: string;
156
+ handoff?: boolean;
157
+ }>): Promise<Readonly<{
158
+ canceled: readonly number[];
159
+ disposition: PendingTellDisposition | null;
160
+ }>>;
146
161
  export declare function beginTurn(paths: AkumaPaths, input: Readonly<{
147
162
  bodySequence: number;
148
163
  startedAt: string;
@@ -1,7 +1,7 @@
1
1
  import { parsePublicHistoryId } from "../identity.js";
2
2
  import { decodeRuntimeModel, withoutModel } from "../provider-recipe.js";
3
3
  import { BodyControlledError } from "./facts.js";
4
- import { answeredTurnFact, completeInterruptBarrier as judgeInterruptBarrier, endBodyFact, finishBodyFact, deleteStopControl, insertActivityFact, insertTurnEndFact, insertTurnStartFact, insertKillFact, insertSessionFact, insertStopControl, killFactForBody, latestBodyFact, insertInterruptBarrier, isTellProtectedByPause, latestKillFact, latestSessionFact, latestTurnFactForBody, lastAnsweredTurnFact, pauseFact, sessionFactForCoordinate, stopFact, turnFact, } from "./rows.js";
4
+ import { answeredTurnFact, completeInterruptBarrier as judgeInterruptBarrier, endBodyFact, finishBodyFact, deleteStopControl, insertReportFact, insertActivityFact, insertTurnEndFact, insertTurnStartFact, insertKillFact, insertSessionFact, insertStopControl, killFactForBody, hasBodyHandoffControl, latestBodyFact, insertInterruptBarrier, isTellProtectedByPause, latestKillFact, latestSessionFact, latestTurnFactForBody, lastAnsweredTurnFact, pauseFact, sessionFactForCoordinate, stopFact, turnFact, openTurnsForBody, terminalTurnForTell, } from "./rows.js";
5
5
  import { insertTellDeliveryFact, dispositionSnapshotProven, insertTellBindingFact, insertTellDispositionSnapshot, insertTellReceiptFact, insertUndeliveredTellReceipts, latestOpenTellDisposition, openBoundTurns, openTellDispositionIds, pendingTellFacts, hasPendingTell, resolveTellDispositionSnapshot, sameTellInput, tellDispositionResolved, tellFact, tellIdsForFence, turnParticipants, } from "./tells.js";
6
6
  import { activityFactSlice, statusFacts, readStatusBatch, decodeTimelineBatch, timelineDataVersion, lastActivityAt as readLastActivityAt, pruneActivityFacts, } from "./timeline.js";
7
7
  import { createHeartReader, isHeartAbsent, readSealFromLeash, recordPendingTell, withHeartTransaction, withReadOnlyHeart, } from "./storage.js";
@@ -121,6 +121,7 @@ function sameStatusBatch(left, right) {
121
121
  sameRawRows(left.rows, right.rows) &&
122
122
  sameRawRows(left.turns, right.turns) &&
123
123
  sameRawRows(left.calls, right.calls) &&
124
+ sameRawRows(left.reports, right.reports) &&
124
125
  sameRawRows(left.activity, right.activity) &&
125
126
  sameRawRows(left.tells.rows, right.tells.rows) &&
126
127
  sameRawRows(left.tells.deliveries, right.tells.deliveries) &&
@@ -134,7 +135,9 @@ export function createStatusFactReader(paths) {
134
135
  life: (signal) => readLifeSnapshot(paths, signal, reader.read),
135
136
  tellOutcome: (id, signal) => reader.read((heart) => {
136
137
  const tell = tellFact(heart, id);
137
- const end = tell?.binding === undefined ? undefined : turnFact(heart, tell.binding.turnSequence)?.end;
138
+ // A pending Tell may be retried by a successor after cancellation. Its
139
+ // first retained terminal still belongs to this exact admission.
140
+ const end = terminalTurnForTell(heart, id)?.end;
138
141
  return { tell, end };
139
142
  }, signal),
140
143
  clear() {
@@ -227,6 +230,36 @@ export async function recordTell(paths, tell, options = {}) {
227
230
  return existing === null ? judgeNewTell(inputs) : judgeExistingTell(inputs, existing);
228
231
  }, options.signal);
229
232
  }
233
+ export async function recordReport(paths, input, signal) {
234
+ try {
235
+ return await withHeartTransaction(paths, (heart) => {
236
+ if (soulFact(heart) === null)
237
+ return { kind: "refused", reason: "not-born" };
238
+ const body = latestBodyFact(heart);
239
+ const turn = body === null ? null : latestTurnFactForBody(heart, body.sequence);
240
+ if (body === null ||
241
+ body.sequence !== input.bodySequence ||
242
+ body.end !== undefined ||
243
+ body.hung !== undefined ||
244
+ turn === null ||
245
+ turn.sequence !== input.turnSequence ||
246
+ turn.end !== undefined)
247
+ return { kind: "refused", reason: "not-current-execution" };
248
+ const report = insertReportFact(heart, input);
249
+ pruneActivityFacts(heart, ACTIVITY_LIMIT);
250
+ return {
251
+ kind: "recorded",
252
+ report,
253
+ ...(turn.initiator === undefined ? {} : { initiator: turn.initiator }),
254
+ };
255
+ }, signal);
256
+ }
257
+ catch (error) {
258
+ if (isHeartAbsent(error))
259
+ return { kind: "refused", reason: "not-born" };
260
+ throw error;
261
+ }
262
+ }
230
263
  export async function readTell(paths, id) {
231
264
  return await withReadOnlyHeart(paths, (heart) => tellFact(heart, id));
232
265
  }
@@ -264,36 +297,39 @@ export async function recordTellReceipt(paths, input) {
264
297
  pruneActivityFacts(heart, ACTIVITY_LIMIT, changed);
265
298
  });
266
299
  }
267
- export async function decidePendingTellDisposition(paths, input) {
268
- return await withHeartTransaction(paths, (heart) => {
269
- const body = latestBodyFact(heart);
270
- if (body === null || body.sequence !== input.bodySequence)
271
- return null;
272
- if (tellDispositionResolved(heart, input.bodySequence) === true)
273
- return null;
274
- const existing = openTellDispositionIds(heart, input.bodySequence);
275
- if (existing !== null)
276
- return { bodySequence: input.bodySequence, tellIds: existing };
277
- if (pauseFact(heart)?.bodySequence === input.bodySequence) {
278
- if (input.handoff && body.end === undefined)
279
- endBodyFact(heart, { sequence: input.bodySequence, end: "put-down", at: input.at });
280
- return null;
281
- }
282
- const pending = pendingTellFacts(heart);
283
- const tellIds = pending.map((tell) => tell.id);
284
- if (tellIds.length === 0)
285
- return null;
286
- if (body.end === undefined) {
287
- if (!input.handoff)
288
- return null;
300
+ function decidePendingTellDispositionInHeart(heart, input) {
301
+ const body = latestBodyFact(heart);
302
+ if (body === null || body.sequence !== input.bodySequence)
303
+ return null;
304
+ if (tellDispositionResolved(heart, input.bodySequence) === true)
305
+ return null;
306
+ if (hasBodyHandoffControl(heart, input.bodySequence)) {
307
+ if (input.handoff && body.end === undefined)
289
308
  endBodyFact(heart, { sequence: input.bodySequence, end: "put-down", at: input.at });
290
- }
291
- else if (body.end === "broke-off") {
309
+ return null;
310
+ }
311
+ const existing = openTellDispositionIds(heart, input.bodySequence);
312
+ if (existing !== null)
313
+ return { bodySequence: input.bodySequence, tellIds: existing };
314
+ const tellIds = pendingTellFacts(heart).map((tell) => tell.id);
315
+ if (tellIds.length === 0)
316
+ return null;
317
+ if (body.end === undefined) {
318
+ if (!input.handoff)
292
319
  return null;
293
- }
294
- insertTellDispositionSnapshot(heart, input.bodySequence, tellIds, input.at);
320
+ endBodyFact(heart, { sequence: input.bodySequence, end: "put-down", at: input.at });
321
+ }
322
+ else if (body.end === "broke-off") {
323
+ return null;
324
+ }
325
+ insertTellDispositionSnapshot(heart, input.bodySequence, tellIds, input.at);
326
+ return { bodySequence: input.bodySequence, tellIds };
327
+ }
328
+ export async function decidePendingTellDisposition(paths, input) {
329
+ return await withHeartTransaction(paths, (heart) => {
330
+ const decided = decidePendingTellDispositionInHeart(heart, input);
295
331
  pruneActivityFacts(heart, ACTIVITY_LIMIT);
296
- return { bodySequence: input.bodySequence, tellIds };
332
+ return decided;
297
333
  });
298
334
  }
299
335
  export async function readOpenPendingTellDisposition(paths) {
@@ -307,6 +343,11 @@ export async function resolvePendingTellDisposition(paths, bodySequence, at, out
307
343
  const tellIds = openTellDispositionIds(heart, bodySequence);
308
344
  if (tellIds === null)
309
345
  throw new HeartAuthorityCorruptionError(`Akuma disposition ${bodySequence} is missing`);
346
+ if (hasBodyHandoffControl(heart, bodySequence)) {
347
+ resolveTellDispositionSnapshot(heart, bodySequence, at);
348
+ pruneActivityFacts(heart, ACTIVITY_LIMIT, true);
349
+ return true;
350
+ }
310
351
  if (outcome === "undelivered")
311
352
  insertUndeliveredTellReceipts(heart, tellIds, at);
312
353
  if (!dispositionSnapshotProven(heart, tellIds))
@@ -332,6 +373,9 @@ export async function requestStop(paths, at, signal) {
332
373
  // exited Body remains already-stopped, and hung custody is untouched.
333
374
  if (body.hung === undefined && body.end !== "exited") {
334
375
  insertKillFact(heart, body.sequence, at);
376
+ const released = tellDispositionResolved(heart, body.sequence) === false;
377
+ resolveTellDispositionSnapshot(heart, body.sequence, at);
378
+ pruneActivityFacts(heart, ACTIVITY_LIMIT, released);
335
379
  if (stopFact(heart)?.bodySequence === body.sequence)
336
380
  deleteStopControl(heart);
337
381
  return { kind: "witnessed", body };
@@ -349,6 +393,28 @@ export async function requestStop(paths, at, signal) {
349
393
  export async function breakBody(paths, input) {
350
394
  await withHeartTransaction(paths, (heart) => endBodyFact(heart, input));
351
395
  }
396
+ /** The owning Body calls this only after proving its provider custody retired. */
397
+ export async function cancelBodyTurns(paths, input) {
398
+ return await withHeartTransaction(paths, (heart) => {
399
+ const body = latestBodyFact(heart);
400
+ if (body?.sequence !== input.bodySequence ||
401
+ body.hung !== undefined ||
402
+ (body.end !== undefined && body.end !== "put-down"))
403
+ throw new Error(`Akuma Body ${input.bodySequence} cannot settle controlled cancellation`);
404
+ const sequences = openTurnsForBody(heart, input.bodySequence);
405
+ for (const turnSequence of sequences)
406
+ insertTurnEndFact(heart, {
407
+ kind: "turn-end",
408
+ turnSequence,
409
+ outcome: { kind: "canceled" },
410
+ completedAt: input.at,
411
+ });
412
+ endBodyFact(heart, { sequence: input.bodySequence, end: "put-down", at: input.at });
413
+ const disposition = input.handoff === true ? decidePendingTellDispositionInHeart(heart, { ...input, handoff: true }) : null;
414
+ pruneActivityFacts(heart, ACTIVITY_LIMIT, sequences.length > 0);
415
+ return { canceled: sequences, disposition };
416
+ });
417
+ }
352
418
  export async function beginTurn(paths, input) {
353
419
  return await withHeartTransaction(paths, (heart) => {
354
420
  if (pauseFact(heart)?.bodySequence === input.bodySequence)
@@ -412,7 +478,6 @@ export async function endTurn(paths, input) {
412
478
  export async function finishBodyIfIdle(paths, input) {
413
479
  return await withHeartTransaction(paths, (heart) => {
414
480
  if (stopFact(heart)?.bodySequence === input.sequence || pauseFact(heart)?.bodySequence === input.sequence) {
415
- endBodyFact(heart, { ...input, end: "put-down" });
416
481
  return { kind: "controlled" };
417
482
  }
418
483
  const pending = pendingTellFacts(heart);
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import type { DatabaseSync } from "node:sqlite";
3
- import type { BodyEnd, BodyFact, CallFact, InterruptCompletion, KillFact, PauseFact, ResumeCoordinate, SessionFact, StopFact, TurnFact, TurnStartFact, TurnEndFact } from "./facts.js";
3
+ import type { BodyEnd, BodyFact, CallFact, InterruptCompletion, KillFact, PauseFact, ReportFact, ResumeCoordinate, SessionFact, StopFact, TurnFact, TurnStartFact, TurnEndFact } from "./facts.js";
4
4
  import { type AgentEvent } from "./activity-schema.js";
5
5
  export type SealRow = Readonly<{
6
6
  evidence: string;
@@ -72,7 +72,7 @@ export type TurnRow = Readonly<{
72
72
  body_sequence: number;
73
73
  started_at: string;
74
74
  end_sequence: number | null;
75
- outcome: "answered" | "failed" | "invalid-output" | null;
75
+ outcome: "answered" | "failed" | "invalid-output" | "canceled" | null;
76
76
  history_id: string | null;
77
77
  session_json: string | null;
78
78
  answer: string | null;
@@ -113,6 +113,14 @@ export declare function decodeSessionRow(row: SessionRow): SessionFact;
113
113
  export declare function decodeBodyRow(row: BodyRow): BodyFact;
114
114
  export declare function decodeTurnRow(row: TurnRow): TurnFact;
115
115
  export declare function decodeCallRow(row: CallRow): CallFact;
116
+ export type ReportRow = Readonly<{
117
+ sequence: number;
118
+ body_sequence: number;
119
+ turn_sequence: number;
120
+ text: string;
121
+ at: string;
122
+ }>;
123
+ export declare function decodeReportRow(row: ReportRow): ReportFact;
116
124
  export declare function encodeActivityEvent(event: unknown): string;
117
125
  export declare function decodeActivityRow(row: ActivityRow): ActivityFact;
118
126
  export declare function sealExists(database: DatabaseSync): boolean;
@@ -130,6 +138,7 @@ export declare function insertBodyFact(database: DatabaseSync, input: Readonly<{
130
138
  leashTakenAt: string;
131
139
  }>): number;
132
140
  export declare function insertSessionFact(database: DatabaseSync, input: Omit<SessionFact, "sequence">): number;
141
+ export declare function insertReportFact(database: DatabaseSync, input: Omit<ReportFact, "kind" | "sequence">): ReportFact;
133
142
  export declare function insertActivityFact(database: DatabaseSync, input: Readonly<{
134
143
  turnSequence: number;
135
144
  event: AgentEvent;
@@ -144,6 +153,7 @@ export declare function insertInterruptBarrier(database: DatabaseSync, input: Re
144
153
  export declare function completeInterruptBarrier(database: DatabaseSync, expected?: PauseFact, consume?: boolean): InterruptCompletion;
145
154
  export declare function stopFact(database: DatabaseSync): StopFact | null;
146
155
  export declare function pauseFact(database: DatabaseSync): PauseFact | null;
156
+ export declare function hasBodyHandoffControl(database: DatabaseSync, bodySequence: number): boolean;
147
157
  export declare function bodyFact(database: DatabaseSync, sequence: number): BodyFact | null;
148
158
  export declare function isTellProtectedByPause(database: DatabaseSync, tellId: string, bodySequence: number): boolean;
149
159
  export declare function insertKillFact(database: DatabaseSync, bodySequence: number, at: string): void;
@@ -164,6 +174,8 @@ export declare function insertTurnStartFact(database: DatabaseSync, input: Reado
164
174
  schemaJson?: string;
165
175
  initiator?: string;
166
176
  }>): TurnStartFact;
177
+ export declare function openTurnsForBody(database: DatabaseSync, bodySequence: number): readonly number[];
178
+ export declare function terminalTurnForTell(database: DatabaseSync, tellId: string): TurnFact | null;
167
179
  export declare function insertTurnEndFact(database: DatabaseSync, input: Omit<TurnEndFact, "sequence">): TurnEndFact;
168
180
  export declare function finishBodyFact(database: DatabaseSync, input: Readonly<{
169
181
  sequence: number;