@astrosheep/keiyaku 4.5.19 → 4.5.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 (197) hide show
  1. package/README.md +4 -4
  2. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +14 -7
  3. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +53 -20
  4. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/references/automation.md +237 -0
  5. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +6 -7
  6. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-task/SKILL.md +2 -4
  7. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +3 -3
  8. package/build/src/akuma/akuma-handle.d.ts +5 -1
  9. package/build/src/akuma/akuma-handle.js +4 -1
  10. package/build/src/akuma/akuma-instance.d.ts +7 -2
  11. package/build/src/akuma/akuma-instance.js +14 -7
  12. package/build/src/akuma/akuma-observe.d.ts +11 -0
  13. package/build/src/akuma/akuma-observe.js +23 -0
  14. package/build/src/akuma/akuma-product.d.ts +2 -0
  15. package/build/src/akuma/akuma-product.js +6 -0
  16. package/build/src/akuma/akuma.d.ts +20 -1
  17. package/build/src/akuma/akuma.js +2 -0
  18. package/build/src/akuma/allowed.d.ts +20 -0
  19. package/build/src/akuma/allowed.js +11 -0
  20. package/build/src/akuma/archetype.js +3 -0
  21. package/build/src/akuma/body.d.ts +25 -5
  22. package/build/src/akuma/body.js +70 -31
  23. package/build/src/akuma/call-request.d.ts +5 -0
  24. package/build/src/akuma/call-request.js +14 -4
  25. package/build/src/akuma/fleet-execution.d.ts +28 -0
  26. package/build/src/akuma/fleet-execution.js +36 -4
  27. package/build/src/akuma/fleet-observation.d.ts +54 -0
  28. package/build/src/akuma/fleet-request.d.ts +10 -0
  29. package/build/src/akuma/fleet-request.js +13 -3
  30. package/build/src/akuma/heart/facts.d.ts +2 -0
  31. package/build/src/akuma/heart/index.d.ts +1 -0
  32. package/build/src/akuma/heart/index.js +4 -1
  33. package/build/src/akuma/heart/rows.d.ts +2 -0
  34. package/build/src/akuma/heart/rows.js +7 -5
  35. package/build/src/akuma/heart/schema.d.ts +1 -1
  36. package/build/src/akuma/heart/schema.js +3 -1
  37. package/build/src/akuma/heart/tells.js +6 -5
  38. package/build/src/akuma/heart/timeline.js +1 -1
  39. package/build/src/akuma/provider-recipe.d.ts +1 -0
  40. package/build/src/akuma/provider-recipe.js +5 -0
  41. package/build/src/akuma/provider.d.ts +1 -1
  42. package/build/src/akuma/provider.js +1 -1
  43. package/build/src/akuma/providers/acp/core.js +3 -6
  44. package/build/src/akuma/providers/acp/index.js +3 -0
  45. package/build/src/akuma/providers/claude/index.js +10 -10
  46. package/build/src/akuma/providers/codex-app-server/index.js +12 -11
  47. package/build/src/akuma/providers/execution-environment.d.ts +6 -0
  48. package/build/src/akuma/providers/execution-environment.js +24 -0
  49. package/build/src/akuma/providers/grok-build/index.js +3 -0
  50. package/build/src/akuma/providers/opencode-sdk/index.js +32 -23
  51. package/build/src/akuma/providers/opencode-sdk/session.d.ts +8 -1
  52. package/build/src/akuma/providers/opencode-sdk/session.js +6 -5
  53. package/build/src/akuma/providers/pi/events.js +7 -2
  54. package/build/src/akuma/providers/pi/index.js +5 -2
  55. package/build/src/akuma/request-observation.d.ts +45 -0
  56. package/build/src/akuma/request-observation.js +162 -0
  57. package/build/src/akuma/request-rendezvous.d.ts +6 -3
  58. package/build/src/akuma/request-rendezvous.js +80 -14
  59. package/build/src/akuma/request-serve.js +27 -4
  60. package/build/src/akuma/request-wire.d.ts +5 -0
  61. package/build/src/akuma/request-wire.js +2 -0
  62. package/build/src/akuma/turn-drive.d.ts +1 -0
  63. package/build/src/akuma/turn-drive.js +2 -0
  64. package/build/src/akuma-body.js +5 -0
  65. package/build/src/body/amend.js +6 -4
  66. package/build/src/body/decode.d.ts +3 -1
  67. package/build/src/body/decode.js +53 -17
  68. package/build/src/body/region.d.ts +9 -1
  69. package/build/src/body/region.js +87 -12
  70. package/build/src/body/render.js +1 -1
  71. package/build/src/body/verification.d.ts +3 -1
  72. package/build/src/body/verification.js +4 -1
  73. package/build/src/cli/accepted.js +19 -2
  74. package/build/src/cli/commands/akuma-invoke.d.ts +3 -1
  75. package/build/src/cli/commands/akuma-invoke.js +134 -14
  76. package/build/src/cli/commands/akuma.js +24 -12
  77. package/build/src/cli/commands/contract-help.d.ts +15 -15
  78. package/build/src/cli/commands/contract-help.js +45 -19
  79. package/build/src/cli/commands/contract-invoke.d.ts +3 -0
  80. package/build/src/cli/commands/contract-invoke.js +25 -11
  81. package/build/src/cli/commands/contract.js +6 -4
  82. package/build/src/cli/commands/install.d.ts +1 -1
  83. package/build/src/cli/commands/install.js +1 -1
  84. package/build/src/cli/commands/status-set.js +15 -24
  85. package/build/src/cli/commands/task.js +18 -18
  86. package/build/src/cli/invoke.d.ts +3 -0
  87. package/build/src/cli/invoke.js +58 -23
  88. package/build/src/cli/main.js +5 -0
  89. package/build/src/cli/parse.d.ts +3 -0
  90. package/build/src/cli/parse.js +26 -4
  91. package/build/src/cli/render/akuma-activity.d.ts +100 -2
  92. package/build/src/cli/render/akuma-activity.js +485 -40
  93. package/build/src/cli/render/akuma.d.ts +10 -0
  94. package/build/src/cli/render/akuma.js +31 -21
  95. package/build/src/cli/render/audit.js +1 -1
  96. package/build/src/cli/render/board.js +1 -1
  97. package/build/src/cli/render/catalog.js +16 -13
  98. package/build/src/cli/render/contract-history.js +39 -10
  99. package/build/src/cli/render/contract-observation.js +10 -6
  100. package/build/src/cli/render/contract.js +109 -106
  101. package/build/src/cli/render/execution-progress.d.ts +26 -0
  102. package/build/src/cli/render/execution-progress.js +178 -0
  103. package/build/src/cli/render/kanshi-akuma.js +27 -39
  104. package/build/src/cli/render/kanshi.js +33 -26
  105. package/build/src/cli/render/nuke.js +1 -1
  106. package/build/src/cli/render/receipt.d.ts +1 -1
  107. package/build/src/cli/render/receipt.js +6 -6
  108. package/build/src/cli/render/refusal.js +10 -7
  109. package/build/src/cli/render/region.js +1 -1
  110. package/build/src/cli/render/settings.js +12 -2
  111. package/build/src/cli/render/status-line.d.ts +30 -0
  112. package/build/src/cli/render/status-line.js +58 -0
  113. package/build/src/cli/render/status-set.js +1 -1
  114. package/build/src/cli/render/task.d.ts +2 -0
  115. package/build/src/cli/render/task.js +11 -7
  116. package/build/src/cli/render/terminal.d.ts +0 -1
  117. package/build/src/cli/render/terminal.js +47 -15
  118. package/build/src/cli/result.d.ts +1 -0
  119. package/build/src/cli/runtime.d.ts +2 -0
  120. package/build/src/cli/runtime.js +36 -4
  121. package/build/src/cli/usage.js +2 -2
  122. package/build/src/cli/version.d.ts +1 -0
  123. package/build/src/cli/version.js +25 -0
  124. package/build/src/git/hooks.d.ts +13 -1
  125. package/build/src/git/hooks.js +2 -1
  126. package/build/src/git/workspace.d.ts +1 -1
  127. package/build/src/git/workspace.js +1 -1
  128. package/build/src/identity/mint.d.ts +13 -0
  129. package/build/src/identity/mint.js +26 -0
  130. package/build/src/identity/normalize.d.ts +4 -0
  131. package/build/src/identity/normalize.js +30 -0
  132. package/build/src/index.d.ts +2 -0
  133. package/build/src/index.js +1 -0
  134. package/build/src/kanshi/read.js +17 -4
  135. package/build/src/kanshi/report.d.ts +1 -0
  136. package/build/src/library/akuma-creation.d.ts +2 -0
  137. package/build/src/library/akuma-creation.js +34 -18
  138. package/build/src/library/audit.d.ts +2 -0
  139. package/build/src/library/audit.js +1 -0
  140. package/build/src/library/bind.js +18 -16
  141. package/build/src/library/composition.d.ts +1 -0
  142. package/build/src/library/contract-bind.d.ts +3 -1
  143. package/build/src/library/contract-bind.js +5 -3
  144. package/build/src/library/contract-execution.d.ts +2 -0
  145. package/build/src/library/contract-execution.js +15 -1
  146. package/build/src/library/contract-forwarding.d.ts +4 -0
  147. package/build/src/library/contract-forwarding.js +3 -0
  148. package/build/src/library/contract-handle.d.ts +7 -0
  149. package/build/src/library/contract-handle.js +25 -0
  150. package/build/src/library/contract-operations.d.ts +7 -2
  151. package/build/src/library/contract-operations.js +21 -1
  152. package/build/src/library/contract-types.d.ts +1 -0
  153. package/build/src/library/execution-result.d.ts +2 -2
  154. package/build/src/library/execution.d.ts +8 -0
  155. package/build/src/library/execution.js +102 -0
  156. package/build/src/library/fleet.d.ts +4 -1
  157. package/build/src/library/fleet.js +32 -8
  158. package/build/src/library/keiyaku.d.ts +2 -0
  159. package/build/src/library/mutation.js +15 -7
  160. package/build/src/plugin/akuma-signals.d.ts +6 -0
  161. package/build/src/plugin/akuma-signals.js +9 -1
  162. package/build/src/plugin/public.d.ts +6 -2
  163. package/build/src/plugin/runtime.d.ts +1 -3
  164. package/build/src/plugin/runtime.js +95 -103
  165. package/build/src/protocol/amend.js +4 -3
  166. package/build/src/protocol/attempt.d.ts +2 -0
  167. package/build/src/protocol/bind.js +41 -14
  168. package/build/src/protocol/completion.d.ts +3 -0
  169. package/build/src/protocol/completion.js +35 -11
  170. package/build/src/protocol/deliver.js +165 -92
  171. package/build/src/protocol/execution-observation.d.ts +181 -0
  172. package/build/src/protocol/execution-observation.js +45 -0
  173. package/build/src/protocol/intent.d.ts +18 -22
  174. package/build/src/protocol/intent.js +63 -41
  175. package/build/src/protocol/operations.d.ts +2 -0
  176. package/build/src/protocol/placement.js +5 -1
  177. package/build/src/protocol/progress.d.ts +5 -1
  178. package/build/src/protocol/progress.js +10 -1
  179. package/build/src/protocol/read/status.d.ts +1 -0
  180. package/build/src/protocol/read/status.js +10 -2
  181. package/build/src/protocol/reintegrate.d.ts +1 -1
  182. package/build/src/protocol/reintegrate.js +70 -43
  183. package/build/src/protocol/result-codec.js +27 -11
  184. package/build/src/protocol/review.js +53 -30
  185. package/build/src/protocol/run.d.ts +83 -36
  186. package/build/src/protocol/run.js +90 -75
  187. package/build/src/runtime/proc/run.d.ts +16 -4
  188. package/build/src/runtime/proc/run.js +64 -22
  189. package/build/src/runtime/proc/windows-launch.exe +0 -0
  190. package/build/src/settings.js +5 -14
  191. package/build/src/task/document.d.ts +5 -1
  192. package/build/src/task/document.js +6 -1
  193. package/build/src/verification/execution.d.ts +10 -2
  194. package/build/src/verification/execution.js +143 -42
  195. package/build/src/verification/observation.d.ts +41 -0
  196. package/build/src/verification/observation.js +29 -0
  197. package/package.json +3 -1
@@ -0,0 +1,24 @@
1
+ export const AKUMA_REQUESTS_ENV = "AKUMA_REQUESTS";
2
+ const PARENT_HARNESS_IDENTITY_KEYS = [
3
+ "CLAUDE_CODE_SESSION_ID",
4
+ "CLAUDE_CODE_CHILD_SESSION",
5
+ "CLAUDECODE",
6
+ "CODEX_THREAD_ID",
7
+ "OPENCODE_SESSION_ID",
8
+ "PI_SESSION_ID",
9
+ "PI_SESSION_FILE",
10
+ "PASEO_AGENT_ID",
11
+ "SQUARE_PARTICIPANT_NAME",
12
+ ];
13
+ /** Construct the environment for one isolated Akuma execution or native child. */
14
+ export function akumaExecutionEnvironment(inherited, overrides = {}, requests, nativeIdentity = []) {
15
+ const preserved = Object.fromEntries(nativeIdentity.flatMap((key) => (inherited[key] === undefined ? [] : [[key, inherited[key]]])));
16
+ const environment = Object.fromEntries(Object.entries({ ...inherited, ...overrides }).flatMap(([key, value]) => value === undefined ? [] : [[key, value]]));
17
+ for (const key of PARENT_HARNESS_IDENTITY_KEYS)
18
+ delete environment[key];
19
+ Object.assign(environment, preserved);
20
+ delete environment[AKUMA_REQUESTS_ENV];
21
+ if (requests !== undefined)
22
+ environment[AKUMA_REQUESTS_ENV] = requests;
23
+ return environment;
24
+ }
@@ -67,6 +67,9 @@ export const interpretGrokTool = (update) => {
67
67
  fileChangeCall(name, input));
68
68
  };
69
69
  function optionAdmission(options) {
70
+ if (options.sandbox !== undefined) {
71
+ return { kind: "refused", diagnostic: "Grok Build does not support the sandbox option" };
72
+ }
70
73
  if (options.network !== undefined) {
71
74
  return { kind: "refused", diagnostic: "Grok Build does not support the network option" };
72
75
  }
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { AKUMA_REQUESTS_ENV, AgentEventChannel, createProviderAttempt, } from "../../provider.js";
2
+ import { AgentEventChannel, createProviderAttempt, } from "../../provider.js";
3
3
  import { createEventState, mapEvent } from "./events.js";
4
4
  import { coordinate, loadOpencode, OPENCODE_SDK_PROVIDER, parseModel } from "./session.js";
5
5
  function object(value) {
@@ -22,6 +22,8 @@ function opencodeSessionId(coordinateValue) {
22
22
  return coordinateValue.sessionId;
23
23
  }
24
24
  function admit(options) {
25
+ if (options.sandbox !== undefined)
26
+ throw new Error("OpenCode does not support the sandbox option");
25
27
  if (options.network !== undefined)
26
28
  throw new Error("OpenCode does not support explicit network");
27
29
  if (options.systemPromptMode === "replace") {
@@ -202,12 +204,13 @@ async function forceDisposeOpencode(abortController, close, finish) {
202
204
  }
203
205
  async function loadDriveRuntime(execution, input, signal, loader, onRuntime) {
204
206
  const runtime = await loadOpencode({
205
- ...execution,
206
- env: {
207
- ...execution.env,
208
- ...(input.requests === undefined ? {} : { [AKUMA_REQUESTS_ENV]: input.requests.dir }),
209
- },
210
- }, input.cwd, signal, loader, onRuntime);
207
+ execution,
208
+ cwd: input.cwd,
209
+ signal,
210
+ ...(loader === undefined ? {} : { loader }),
211
+ ...(onRuntime === undefined ? {} : { onRuntime }),
212
+ ...(input.requests === undefined ? {} : { requests: input.requests.dir }),
213
+ });
211
214
  if (signal.aborted) {
212
215
  await runtime.close();
213
216
  signal.throwIfAborted();
@@ -341,22 +344,28 @@ export function createOpencodeProvider(input = { name: OPENCODE_SDK_PROVIDER, ki
341
344
  fork: (input) => createProviderAttempt(new AbortController().signal, async (custody) => {
342
345
  const sessionId = opencodeSessionId(input.session);
343
346
  let close;
344
- const runtime = await loadOpencode(execution, input.cwd, custody.signal, loader, (ready) => {
345
- let closing;
346
- let settleRuntimeClosed;
347
- let rejectRuntimeClosed;
348
- const runtimeClosed = new Promise((resolve, reject) => {
349
- settleRuntimeClosed = resolve;
350
- rejectRuntimeClosed = reject;
351
- });
352
- close = () => {
353
- if (closing === undefined) {
354
- closing = ready.close();
355
- void closing.then(settleRuntimeClosed, rejectRuntimeClosed);
356
- }
357
- return closing;
358
- };
359
- custody.own({ closed: runtimeClosed, abort: close, forceDispose: close });
347
+ const runtime = await loadOpencode({
348
+ execution,
349
+ cwd: input.cwd,
350
+ signal: custody.signal,
351
+ ...(loader === undefined ? {} : { loader }),
352
+ onRuntime: (ready) => {
353
+ let closing;
354
+ let settleRuntimeClosed;
355
+ let rejectRuntimeClosed;
356
+ const runtimeClosed = new Promise((resolve, reject) => {
357
+ settleRuntimeClosed = resolve;
358
+ rejectRuntimeClosed = reject;
359
+ });
360
+ close = () => {
361
+ if (closing === undefined) {
362
+ closing = ready.close();
363
+ void closing.then(settleRuntimeClosed, rejectRuntimeClosed);
364
+ }
365
+ return closing;
366
+ };
367
+ custody.own({ closed: runtimeClosed, abort: close, forceDispose: close });
368
+ },
360
369
  });
361
370
  try {
362
371
  const result = await runtime.client.session.fork({
@@ -23,5 +23,12 @@ export type OpencodeRuntime = Readonly<{
23
23
  };
24
24
  close: () => Promise<void>;
25
25
  }>;
26
- export declare function loadOpencode(execution: ProviderExecution, cwd: string, signal: AbortSignal, loader?: OpencodeSdkLoader, onRuntime?: (runtime: OpencodeRuntime) => void): Promise<OpencodeRuntime>;
26
+ export declare function loadOpencode(input: Readonly<{
27
+ execution: ProviderExecution;
28
+ cwd: string;
29
+ signal: AbortSignal;
30
+ loader?: OpencodeSdkLoader;
31
+ onRuntime?: (runtime: OpencodeRuntime) => void;
32
+ requests?: string;
33
+ }>): Promise<OpencodeRuntime>;
27
34
  export declare function coordinate(sessionId: string): ResumeCoordinate;
@@ -1,5 +1,6 @@
1
1
  import net from "node:net";
2
2
  import { abortable } from "../../abort.js";
3
+ import { akumaExecutionEnvironment } from "../execution-environment.js";
3
4
  import { spawnDetachedProcess } from "../../../runtime/proc/run.js";
4
5
  export const OPENCODE_SDK_PROVIDER = "opencode-sdk";
5
6
  export function parseModel(model) {
@@ -37,9 +38,10 @@ async function availablePort() {
37
38
  });
38
39
  });
39
40
  }
40
- export async function loadOpencode(execution, cwd, signal, loader, onRuntime) {
41
+ export async function loadOpencode(input) {
42
+ const { execution, cwd, signal, loader, onRuntime, requests } = input;
41
43
  if (loader) {
42
- const loaded = await abortable(loader(cwd, execution, signal), signal);
44
+ const loaded = await abortable(loader(cwd, { ...execution, env: akumaExecutionEnvironment(process.env, execution.env, requests) }, signal), signal);
43
45
  let closing;
44
46
  const runtime = {
45
47
  client: loaded.client,
@@ -64,11 +66,10 @@ export async function loadOpencode(execution, cwd, signal, loader, onRuntime) {
64
66
  const owned = await spawnDetachedProcess({
65
67
  argv: [execution.executable ?? "opencode", "serve", "--hostname", "127.0.0.1", "--port", String(port)],
66
68
  cwd,
67
- env: {
68
- ...process.env,
69
+ env: akumaExecutionEnvironment(process.env, {
69
70
  ...(execution.config === undefined ? {} : { OPENCODE_CONFIG_CONTENT: JSON.stringify(execution.config) }),
70
71
  ...execution.env,
71
- },
72
+ }, requests),
72
73
  log: `${cwd}/.opencode.log`,
73
74
  });
74
75
  const { createOpencodeClient } = await import("@opencode-ai/sdk");
@@ -89,7 +89,9 @@ function searchCall(name, value) {
89
89
  };
90
90
  }
91
91
  function fileChangeCall(name, value) {
92
- if (name !== "edit" || typeof value.path !== "string")
92
+ // Native write cannot prove creation versus overwrite and reports no patch, so
93
+ // it joins the conservative update operation with unknown diffstat.
94
+ if ((name !== "edit" && name !== "write") || typeof value.path !== "string")
93
95
  return undefined;
94
96
  return { kind: "fileChange", changes: [{ op: "update", path: value.path }] };
95
97
  }
@@ -158,7 +160,10 @@ function translateMessage(event, state) {
158
160
  }
159
161
  const text = textContent(message.content);
160
162
  state.answer = text;
161
- if (hasTextContent(message.content)) {
163
+ // Gemini emits an empty text block alongside every tool-use message. It is
164
+ // transport scaffolding, not narration; an ordinary terminal empty answer
165
+ // remains public evidence.
166
+ if (hasTextContent(message.content) && !(message.stopReason === "toolUse" && text.length === 0)) {
162
167
  translated.push({ type: "assistant", text });
163
168
  }
164
169
  return translated;
@@ -1,5 +1,6 @@
1
1
  import { abortable } from "../../abort.js";
2
- import { AKUMA_REQUESTS_ENV, AgentEventChannel, createProviderAttempt, } from "../../provider.js";
2
+ import { akumaExecutionEnvironment } from "../execution-environment.js";
3
+ import { AgentEventChannel, createProviderAttempt, } from "../../provider.js";
3
4
  import { piTerminalFailure, translatePiEvent } from "./events.js";
4
5
  const PI_THINKING_LEVELS = new Set(["minimal", "low", "medium", "high", "xhigh", "max"]);
5
6
  const MODEL_PATTERN = /^[^/\s]+\/[^\s]+$/u;
@@ -7,6 +8,8 @@ function diagnostic(error) {
7
8
  return error instanceof Error ? error.message : String(error);
8
9
  }
9
10
  function admitPiOptions(options) {
11
+ if (options.sandbox !== undefined)
12
+ return { kind: "refused", diagnostic: "Pi provider does not support the sandbox option" };
10
13
  if (options.network !== undefined)
11
14
  return { kind: "refused", diagnostic: "Pi provider does not support the network option" };
12
15
  if (options.model !== undefined && !MODEL_PATTERN.test(options.model)) {
@@ -54,7 +57,7 @@ async function piCreateOptions(sdk, input) {
54
57
  (await import("@earendil-works/pi-coding-agent")).createBashToolDefinition(input.cwd, {
55
58
  spawnHook: (context) => ({
56
59
  ...context,
57
- env: { ...context.env, [AKUMA_REQUESTS_ENV]: input.requests.dir },
60
+ env: akumaExecutionEnvironment(context.env, {}, input.requests.dir, ["PI_SESSION_ID", "PI_SESSION_FILE"]),
58
61
  }),
59
62
  }),
60
63
  ];
@@ -0,0 +1,45 @@
1
+ export declare const REQUEST_PROGRESS_WINDOW = 64;
2
+ export type RequestProgressSnapshot = Readonly<{
3
+ nextSequence: number;
4
+ events: readonly Readonly<{
5
+ sequence: number;
6
+ value: unknown;
7
+ }>[];
8
+ }>;
9
+ export declare function progressPath(directory: string, transportId: string): string;
10
+ export declare function cancellationPath(directory: string, transportId: string): string;
11
+ /**
12
+ * The snapshot is a bounded, replaceable observation window. It is deliberately
13
+ * not a progress log: a slow observer receives an explicit sequence gap.
14
+ */
15
+ export declare function publishRequestProgress(input: Readonly<{
16
+ directory: string;
17
+ transportId: string;
18
+ id: string;
19
+ action: string;
20
+ }>): Readonly<{
21
+ progress(value: unknown): void;
22
+ flush(): Promise<void>;
23
+ close(): Promise<void>;
24
+ }>;
25
+ export declare function readRequestProgress(input: Readonly<{
26
+ directory: string;
27
+ transportId: string;
28
+ id: string;
29
+ action: string;
30
+ }>): Promise<RequestProgressSnapshot | undefined>;
31
+ export declare function publishRequestCancellation(input: Readonly<{
32
+ directory: string;
33
+ transportId: string;
34
+ id: string;
35
+ action: string;
36
+ }>): Promise<void>;
37
+ export declare function observeRequestCancellation(input: Readonly<{
38
+ directory: string;
39
+ transportId: string;
40
+ id: string;
41
+ action: string;
42
+ }>): Readonly<{
43
+ signal: AbortSignal;
44
+ close(): Promise<void>;
45
+ }>;
@@ -0,0 +1,162 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { z } from "zod";
3
+ import { abortableDelay } from "./abort.js";
4
+ import { atomicJson } from "./request-wire.js";
5
+ export const REQUEST_PROGRESS_WINDOW = 64;
6
+ const OBSERVATION_POLL_MS = 25;
7
+ const sequenceSchema = z.number().int().nonnegative();
8
+ const nextSequenceSchema = z.number().int().positive();
9
+ const progressSnapshotSchema = z
10
+ .object({
11
+ id: z.string(),
12
+ action: z.string(),
13
+ nextSequence: nextSequenceSchema,
14
+ events: z.array(z.object({ sequence: sequenceSchema, value: z.unknown() }).strict()).max(REQUEST_PROGRESS_WINDOW),
15
+ })
16
+ .strict()
17
+ .superRefine((snapshot, context) => {
18
+ for (let index = 1; index < snapshot.events.length; index += 1) {
19
+ if (snapshot.events[index].sequence !== snapshot.events[index - 1].sequence + 1) {
20
+ context.addIssue({ code: "custom", message: "progress sequences must be contiguous" });
21
+ return;
22
+ }
23
+ }
24
+ if (snapshot.events.at(-1)?.sequence !== undefined && snapshot.events.at(-1).sequence >= snapshot.nextSequence) {
25
+ context.addIssue({ code: "custom", message: "progress sequence exceeds its snapshot" });
26
+ }
27
+ });
28
+ const cancellationSchema = z.object({ id: z.string(), action: z.string() }).strict();
29
+ export function progressPath(directory, transportId) {
30
+ return `${directory}/${transportId}.progress.json`;
31
+ }
32
+ export function cancellationPath(directory, transportId) {
33
+ return `${directory}/${transportId}.cancel.json`;
34
+ }
35
+ /**
36
+ * The snapshot is a bounded, replaceable observation window. It is deliberately
37
+ * not a progress log: a slow observer receives an explicit sequence gap.
38
+ */
39
+ export function publishRequestProgress(input) {
40
+ let nextSequence = 1;
41
+ let events = [];
42
+ let dirty = false;
43
+ let closed = false;
44
+ let writer;
45
+ const snapshot = () => ({ nextSequence, events });
46
+ const write = async () => {
47
+ while (dirty && !closed) {
48
+ dirty = false;
49
+ try {
50
+ await atomicJson(progressPath(input.directory, input.transportId), {
51
+ id: input.id,
52
+ action: input.action,
53
+ ...snapshot(),
54
+ });
55
+ }
56
+ catch {
57
+ // Observation is never allowed to impede service or its durable outcome.
58
+ }
59
+ }
60
+ };
61
+ const startWriter = () => {
62
+ if (writer !== undefined)
63
+ return;
64
+ writer = write().finally(() => {
65
+ writer = undefined;
66
+ if (dirty && !closed)
67
+ startWriter();
68
+ });
69
+ };
70
+ const flush = async () => {
71
+ while (writer !== undefined)
72
+ await writer;
73
+ };
74
+ return {
75
+ progress(value) {
76
+ if (closed)
77
+ return;
78
+ const sequence = nextSequence++;
79
+ try {
80
+ const encoded = JSON.stringify({ value });
81
+ if (encoded === undefined || !Object.hasOwn(JSON.parse(encoded), "value"))
82
+ throw new Error("not JSON transport data");
83
+ }
84
+ catch {
85
+ dirty = true;
86
+ startWriter();
87
+ return;
88
+ }
89
+ if (events.length === REQUEST_PROGRESS_WINDOW) {
90
+ events = events.slice(1);
91
+ }
92
+ events = [...events, { sequence, value }];
93
+ dirty = true;
94
+ startWriter();
95
+ },
96
+ flush,
97
+ async close() {
98
+ await flush();
99
+ closed = true;
100
+ },
101
+ };
102
+ }
103
+ export async function readRequestProgress(input) {
104
+ let bytes;
105
+ try {
106
+ bytes = await readFile(progressPath(input.directory, input.transportId), "utf8");
107
+ }
108
+ catch (error) {
109
+ if (error.code === "ENOENT")
110
+ return undefined;
111
+ return undefined;
112
+ }
113
+ try {
114
+ const parsed = progressSnapshotSchema.safeParse(JSON.parse(bytes));
115
+ if (!parsed.success || parsed.data.id !== input.id || parsed.data.action !== input.action)
116
+ return undefined;
117
+ return { nextSequence: parsed.data.nextSequence, events: parsed.data.events };
118
+ }
119
+ catch {
120
+ return undefined;
121
+ }
122
+ }
123
+ export async function publishRequestCancellation(input) {
124
+ try {
125
+ await atomicJson(cancellationPath(input.directory, input.transportId), { id: input.id, action: input.action });
126
+ }
127
+ catch {
128
+ // The channel may have disappeared; rendezvous will report the truthful unknown outcome.
129
+ }
130
+ }
131
+ export function observeRequestCancellation(input) {
132
+ const local = new AbortController();
133
+ const stop = new AbortController();
134
+ const run = (async () => {
135
+ while (!stop.signal.aborted && !local.signal.aborted) {
136
+ try {
137
+ const parsed = cancellationSchema.safeParse(JSON.parse(await readFile(cancellationPath(input.directory, input.transportId), "utf8")));
138
+ if (parsed.success && parsed.data.id === input.id && parsed.data.action === input.action) {
139
+ local.abort(new Error("request cancelled by caller"));
140
+ return;
141
+ }
142
+ }
143
+ catch {
144
+ // An absent or malformed cancellation notice has no authority.
145
+ }
146
+ try {
147
+ await abortableDelay(OBSERVATION_POLL_MS, stop.signal);
148
+ }
149
+ catch {
150
+ return;
151
+ }
152
+ }
153
+ })();
154
+ return {
155
+ signal: local.signal,
156
+ async close() {
157
+ if (!stop.signal.aborted)
158
+ stop.abort();
159
+ await run;
160
+ },
161
+ };
162
+ }
@@ -18,12 +18,15 @@ type RequestResponse<Output, Reference> = Readonly<{
18
18
  requestId: string;
19
19
  action: string;
20
20
  }>;
21
- /** Generic rendezvous only: operation owners supply their own request and result codecs. */
22
- export declare function requestBodyCommand<Input, Output, Reference>(input: Readonly<{
21
+ type RequestCommandInput<Input, Output, Reference> = Readonly<{
23
22
  directory: string;
24
23
  id?: string;
25
24
  command: RequestProtocol<Input, Output, Reference>;
26
25
  value: Input;
27
26
  signal?: AbortSignal;
28
- }>): Promise<RequestResponse<Output, Reference>>;
27
+ onProgress?(value: unknown): void;
28
+ onProgressGap?(dropped: number): void;
29
+ }>;
30
+ /** Generic rendezvous only: operation owners supply their own request and result codecs. */
31
+ export declare function requestBodyCommand<Input, Output, Reference>(input: RequestCommandInput<Input, Output, Reference>): Promise<RequestResponse<Output, Reference>>;
29
32
  export {};
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { access, readFile } from "node:fs/promises";
3
3
  import { abortableDelay } from "./abort.js";
4
+ import { publishRequestCancellation, readRequestProgress, } from "./request-observation.js";
4
5
  import { atomicJson, decodeReceiptEnvelope, receiptPath, requestPath } from "./request-wire.js";
5
6
  const POLL_MS = 100;
6
7
  export class AkumaBodyRequestError extends Error {
@@ -110,21 +111,86 @@ export async function requestBodyCommand(input) {
110
111
  throw new AkumaBodyRequestError(input.command.action, "unknown", "parent request channel closed before request publication", id);
111
112
  }
112
113
  const path = receiptPath(input.directory, transportId);
113
- for (;;) {
114
- const response = await readRequestReceipt(input, path, id);
115
- if (response !== undefined)
116
- return response;
117
- if (!(await access(input.directory).then(() => true, () => false))) {
118
- throw new AkumaBodyRequestError(input.command.action, "unknown", "parent request channel closed before a receipt", id);
119
- }
120
- try {
121
- await abortableDelay(POLL_MS, input.signal);
122
- }
123
- catch (error) {
124
- if (input.signal?.aborted) {
125
- throw new AkumaBodyRequestError(input.command.action, "unknown", "request was cancelled after publication and before a receipt", id);
114
+ return await observePublishedRequest(input, id, transportId, path);
115
+ }
116
+ async function observePublishedRequest(input, id, transportId, path) {
117
+ let observedSequence = 0;
118
+ const consumeProgress = async () => {
119
+ const snapshot = await readRequestProgress({
120
+ directory: input.directory,
121
+ transportId,
122
+ id,
123
+ action: input.command.action,
124
+ });
125
+ observedSequence = consumeSnapshot(snapshot, observedSequence, input.onProgress, input.onProgressGap);
126
+ };
127
+ const publishCancellation = () => {
128
+ void publishRequestCancellation({ directory: input.directory, transportId, id, action: input.command.action });
129
+ };
130
+ const cancellable = input.command.supportsCancellation === true;
131
+ if (cancellable && input.signal !== undefined) {
132
+ input.signal.addEventListener("abort", publishCancellation, { once: true });
133
+ if (input.signal.aborted)
134
+ publishCancellation();
135
+ }
136
+ try {
137
+ for (;;) {
138
+ let response;
139
+ try {
140
+ response = await readRequestReceipt(input, path, id);
141
+ }
142
+ finally {
143
+ // A visible receipt guarantees its final progress was already published, even on failure.
144
+ await consumeProgress();
145
+ }
146
+ if (response !== undefined)
147
+ return response;
148
+ if (!(await access(input.directory).then(() => true, () => false))) {
149
+ throw new AkumaBodyRequestError(input.command.action, "unknown", "parent request channel closed before a receipt", id);
150
+ }
151
+ try {
152
+ await abortableDelay(POLL_MS, cancellable ? undefined : input.signal);
153
+ }
154
+ catch (error) {
155
+ if (input.signal?.aborted) {
156
+ throw new AkumaBodyRequestError(input.command.action, "unknown", "request was cancelled after publication and before a receipt", id);
157
+ }
158
+ throw error;
126
159
  }
127
- throw error;
128
160
  }
129
161
  }
162
+ finally {
163
+ if (cancellable && input.signal !== undefined)
164
+ input.signal.removeEventListener("abort", publishCancellation);
165
+ }
166
+ }
167
+ function consumeSnapshot(snapshot, observedSequence, onProgress, onProgressGap) {
168
+ if (snapshot === undefined)
169
+ return observedSequence;
170
+ const events = snapshot.events.filter((event) => event.sequence > observedSequence);
171
+ const latestSequence = snapshot.nextSequence - 1;
172
+ const firstAvailable = events[0]?.sequence ?? latestSequence + 1;
173
+ const gap = firstAvailable - observedSequence - 1;
174
+ if (gap > 0) {
175
+ notifyObserver(onProgressGap, gap);
176
+ }
177
+ if (events.length === 0)
178
+ return Math.max(observedSequence, latestSequence);
179
+ for (const event of events) {
180
+ observedSequence = event.sequence;
181
+ notifyObserver(onProgress, event.value);
182
+ }
183
+ return observedSequence;
184
+ }
185
+ function notifyObserver(observer, value) {
186
+ if (observer === undefined)
187
+ return;
188
+ try {
189
+ void Promise.resolve(observer(value)).catch(() => {
190
+ // A caller observation failure cannot affect its in-flight request.
191
+ });
192
+ }
193
+ catch {
194
+ // A caller observation failure cannot affect its in-flight request.
195
+ }
130
196
  }
@@ -1,5 +1,6 @@
1
1
  import { admitRequest, beginRequest, isRequestInputConflict, readHeart, readRequest, serveRequest as serveChildRequest, serveUpstreamRequest, voidRequest, unproveRequest, } from "./heart/index.js";
2
2
  import { atomicJson, receiptPath, } from "./request-wire.js";
3
+ import { observeRequestCancellation, publishRequestProgress } from "./request-observation.js";
3
4
  import { BodyRequestPump as LifecycleBodyRequestPump, settleReserved, } from "./request-lifecycle.js";
4
5
  export { clearBodyRequestTransport, settleBodyRequests } from "./request-lifecycle.js";
5
6
  function replayReference(fact, command) {
@@ -72,16 +73,38 @@ async function serveResolvedCommand(input, command, request, execute) {
72
73
  return true;
73
74
  }
74
75
  try {
76
+ const progress = publishRequestProgress({
77
+ directory: input.directory,
78
+ transportId: input.transportId,
79
+ id: fact.id,
80
+ action: fact.action,
81
+ });
82
+ const cancellation = command.completion === "service" && request.supportsCancellation
83
+ ? observeRequestCancellation({
84
+ directory: input.directory,
85
+ transportId: input.transportId,
86
+ id: fact.id,
87
+ action: fact.action,
88
+ })
89
+ : undefined;
75
90
  const facts = {
76
91
  id: fact.id,
77
92
  admittedAt: fact.admittedAt,
78
93
  requester: fact.requester,
79
- signal: input.signal,
94
+ signal: cancellation === undefined ? input.signal : AbortSignal.any([input.signal, cancellation.signal]),
80
95
  admissionOpen: input.admissionOpen,
96
+ progress: progress.progress,
81
97
  };
82
- const served = await execute(fact, facts);
83
- fact = served.fact;
84
- await projectCommandReceipt(input, fact, command, served.outcome);
98
+ try {
99
+ const served = await execute(fact, facts);
100
+ fact = served.fact;
101
+ await progress.flush();
102
+ await projectCommandReceipt(input, fact, command, served.outcome);
103
+ }
104
+ finally {
105
+ await progress.close();
106
+ await cancellation?.close();
107
+ }
85
108
  }
86
109
  catch (error) {
87
110
  const failure = encodedFailure(request, error);
@@ -71,6 +71,8 @@ export type RequestEnvelope = z.infer<typeof requestEnvelopeSchema>;
71
71
  */
72
72
  export type RequestProtocol<Input, Output, Reference> = Readonly<{
73
73
  action: string;
74
+ /** An opted-in service request accepts cancellation after publication. */
75
+ supportsCancellation?: boolean;
74
76
  encodeRequest(input: Input): unknown;
75
77
  decodeRequest(payload: unknown): Input | null;
76
78
  encodeResult(result: Output): unknown;
@@ -86,6 +88,8 @@ export type ExecutionFacts = Readonly<{
86
88
  requester: string;
87
89
  signal: AbortSignal;
88
90
  admissionOpen(): boolean;
91
+ /** Best-effort, ephemeral progress for this one live request. */
92
+ progress?(value: unknown): void;
89
93
  }>;
90
94
  export type ChildRequestCommand<Input, Output, Reference> = Readonly<{
91
95
  completion: "child";
@@ -109,6 +113,7 @@ export type ServiceRequestCommand<Input, Output, Service, Reference> = Readonly<
109
113
  }>;
110
114
  type ErasedRequest = Readonly<{
111
115
  payloadJson: string;
116
+ supportsCancellation: boolean;
112
117
  isPermitted(allowed: readonly string[]): boolean;
113
118
  encodeFailure(error: unknown): unknown | null;
114
119
  }>;
@@ -53,6 +53,7 @@ export function eraseRequestCommand(command) {
53
53
  return null;
54
54
  return {
55
55
  payloadJson: JSON.stringify(protocol.encodeRequest(input)),
56
+ supportsCancellation: protocol.supportsCancellation === true,
56
57
  isPermitted: (allowed) => protocol.isPermitted(allowed),
57
58
  execute: async (facts) => {
58
59
  const served = await command.execute(input, facts);
@@ -73,6 +74,7 @@ export function eraseRequestCommand(command) {
73
74
  return null;
74
75
  return {
75
76
  payloadJson: JSON.stringify(protocol.encodeRequest(input)),
77
+ supportsCancellation: protocol.supportsCancellation === true,
76
78
  isPermitted: (allowed) => protocol.isPermitted(allowed),
77
79
  execute: async (facts) => {
78
80
  const served = await command.execute(input, facts);
@@ -25,6 +25,7 @@ export type DriveTurnInput = Readonly<{
25
25
  body: string;
26
26
  call?: string;
27
27
  launchTells: readonly TellFact[];
28
+ initiator?: string;
28
29
  schemaJson?: string;
29
30
  supervisor: BodySupervisor;
30
31
  runtimeSpawn(launch: AkumaCallRequestChildLaunch): Promise<OwnedProcess | void>;