@code-yeongyu/senpi 2026.10.18 → 2026.10.19

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 (190) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/bundle/chunks/{anthropic-messages-DDBWM4JZ.js → anthropic-messages-EMTBHJMM.js} +2 -2
  3. package/dist/bundle/chunks/{args-2MYPVFPH.js → args-LWR7VFKS.js} +1 -1
  4. package/dist/bundle/chunks/{azure-openai-responses-3UG5TQU7.js → azure-openai-responses-SNWKFDXK.js} +1 -1
  5. package/dist/bundle/chunks/chatgpt-subscription.js +1 -1
  6. package/dist/bundle/chunks/{chunk-SOF44GFZ.js → chunk-35M5MVUZ.js} +149 -145
  7. package/dist/bundle/chunks/chunk-3DNYAO3P.js +2 -0
  8. package/dist/bundle/chunks/{chunk-T2MHSEQ2.js → chunk-5AOKDC6G.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-MYXNQ7YZ.js → chunk-7QPFFAJT.js} +3 -3
  10. package/dist/bundle/chunks/{chunk-R36U2DJC.js → chunk-AUZXSLZN.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-PMTG4YNS.js → chunk-BOCN744P.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-XTAWER7O.js → chunk-CICJOV6I.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-4UCLDXEF.js → chunk-FM7NDZR5.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-TBRWPNA4.js → chunk-FSENE7JK.js} +2 -2
  15. package/dist/bundle/chunks/{chunk-TUD7XRIS.js → chunk-G7LLAQXI.js} +3 -3
  16. package/dist/bundle/chunks/{chunk-BGD3MZ47.js → chunk-GOKAWZUO.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-XC6UG5SI.js → chunk-IM6ZWYTW.js} +2 -2
  18. package/dist/bundle/chunks/{chunk-KHATAB7P.js → chunk-KM4FMQII.js} +1 -1
  19. package/dist/bundle/chunks/chunk-LAWS3FYQ.js +11 -0
  20. package/dist/bundle/chunks/{chunk-W6YQYGE2.js → chunk-MVNVDGYF.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-W4BO6S7F.js → chunk-PMGEQYEP.js} +2 -2
  22. package/dist/bundle/chunks/chunk-QGJLZ6BI.js +2 -0
  23. package/dist/bundle/chunks/{chunk-5RQNCWS2.js → chunk-QJ6MUFXY.js} +1 -1
  24. package/dist/bundle/chunks/{chunk-57Q7Q627.js → chunk-QRGAMVEE.js} +1 -1
  25. package/dist/bundle/chunks/{chunk-W2257FYP.js → chunk-QXEWAKXA.js} +1 -1
  26. package/dist/bundle/chunks/{chunk-UUMKAA7S.js → chunk-RL5QMGQD.js} +9 -9
  27. package/dist/bundle/chunks/{chunk-D2UQYZVK.js → chunk-SHKXJWK3.js} +1 -1
  28. package/dist/bundle/chunks/chunk-T36LJ6MW.js +3 -0
  29. package/dist/bundle/chunks/{chunk-S4V72F76.js → chunk-VCD7FD64.js} +1 -1
  30. package/dist/bundle/chunks/{chunk-MYPTZCPF.js → chunk-VEKNDMUA.js} +1 -1
  31. package/dist/bundle/chunks/{chunk-IRBAGET4.js → chunk-VPV7ADF3.js} +4 -4
  32. package/dist/bundle/chunks/{chunk-6DDAEDY7.js → chunk-WEHIEACU.js} +1 -1
  33. package/dist/bundle/chunks/{chunk-C2HSPFXN.js → chunk-WQLMQGOW.js} +1 -1
  34. package/dist/bundle/chunks/{chunk-QZIKKUUP.js → chunk-X6TWPBI6.js} +2 -2
  35. package/dist/bundle/chunks/{chunk-VYWODOAM.js → chunk-YUMRZIKJ.js} +6 -6
  36. package/dist/bundle/chunks/chunk-ZPSJQHDK.js +395 -0
  37. package/dist/bundle/chunks/{cli-main-MVVPTGMS.js → cli-main-EABMHWFV.js} +1 -1
  38. package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-H4ZPUBX4.js → cloudflare-workers-ai-system-one-PF7FU63D.js} +1 -1
  39. package/dist/bundle/chunks/{google-generative-ai-YLAYNQLO.js → google-generative-ai-UFCHYLRE.js} +1 -1
  40. package/dist/bundle/chunks/{google-vertex-THFXM5R2.js → google-vertex-2FWDY7XX.js} +1 -1
  41. package/dist/bundle/chunks/{help-fast-path-H4ERPCR5.js → help-fast-path-BYWJN6YX.js} +1 -1
  42. package/dist/bundle/chunks/{host-command-2CMRIKC3.js → host-command-5T34JLBP.js} +1 -1
  43. package/dist/bundle/chunks/{host-lifecycle-6Y2EKT4U.js → host-lifecycle-GSRIQP6Z.js} +1 -1
  44. package/dist/bundle/chunks/host-lifecycle.js +5 -5
  45. package/dist/bundle/chunks/{host-session-command-MVHY4TKA.js → host-session-command-LF6JODPX.js} +1 -1
  46. package/dist/bundle/chunks/{interactive-mode-2ZQ43N73.js → interactive-mode-GMWHBIUC.js} +1 -1
  47. package/dist/bundle/chunks/{list-tips-AWDAUHF2.js → list-tips-VSWED3PT.js} +1 -1
  48. package/dist/bundle/chunks/{mistral-conversations-GUGTVRCL.js → mistral-conversations-QHZLDTSP.js} +1 -1
  49. package/dist/bundle/chunks/{multi-session-host-NO4PIOVG.js → multi-session-host-3J7STKGD.js} +6 -6
  50. package/dist/bundle/chunks/openai-codex-responses-7YOBPZ2N.js +10 -0
  51. package/dist/bundle/chunks/{openai-completions-I5N2UIEK.js → openai-completions-HSZPYXEA.js} +1 -1
  52. package/dist/bundle/chunks/{openai-responses-SGLUJVZW.js → openai-responses-7W2KF3W7.js} +1 -1
  53. package/dist/bundle/chunks/openrouter.js +1 -1
  54. package/dist/bundle/chunks/{package-manager-cli-HTZQXYNI.js → package-manager-cli-LWQZSWKL.js} +1 -1
  55. package/dist/bundle/chunks/radius.js +1 -1
  56. package/dist/bundle/chunks/{rotation-stream-TOOIK43C.js → rotation-stream-QRRWL73X.js} +1 -1
  57. package/dist/bundle/chunks/{rpc-mode-6ZWGPICX.js → rpc-mode-6LSWRKXR.js} +1 -1
  58. package/dist/bundle/chunks/{schedule-command-2YW4B655.js → schedule-command-7CXMW4WK.js} +1 -1
  59. package/dist/bundle/chunks/{session-control-endpoint-XC2LPLCD.js → session-control-endpoint-E265WAIQ.js} +1 -1
  60. package/dist/bundle/chunks/{session-picker-7W7GIRJT.js → session-picker-XRB3LP5W.js} +1 -1
  61. package/dist/bundle/chunks/session-worker.js +315 -310
  62. package/dist/bundle/chunks/{typesafe-system-one-VBWWTJTF.js → typesafe-system-one-YA75435G.js} +1 -1
  63. package/dist/bundle/cli.js +2 -2
  64. package/dist/bundle/index.js +1 -1
  65. package/dist/bundle/rpc-entry.js +1 -1
  66. package/dist/bundle/runtime-manifest.json +1 -1
  67. package/dist/core/agent-run-continuation.d.ts +24 -0
  68. package/dist/core/agent-run-continuation.js +57 -0
  69. package/dist/core/agent-session.d.ts +38 -1
  70. package/dist/core/agent-session.js +122 -13
  71. package/dist/core/extension-handler-stall-report.d.ts +11 -0
  72. package/dist/core/extension-handler-stall-report.js +26 -0
  73. package/dist/core/extensions/builtin/anthropic-subscription/account-command.js +1 -0
  74. package/dist/core/extensions/builtin/anthropic-subscription/affinity.d.ts +13 -1
  75. package/dist/core/extensions/builtin/anthropic-subscription/affinity.js +15 -4
  76. package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +2 -0
  77. package/dist/core/extensions/builtin/anthropic-subscription/executable.d.ts +5 -0
  78. package/dist/core/extensions/builtin/anthropic-subscription/executable.js +11 -1
  79. package/dist/core/extensions/builtin/anthropic-subscription/failover.d.ts +2 -0
  80. package/dist/core/extensions/builtin/anthropic-subscription/failover.js +3 -2
  81. package/dist/core/extensions/builtin/anthropic-subscription/guidance.d.ts +8 -0
  82. package/dist/core/extensions/builtin/anthropic-subscription/guidance.js +29 -0
  83. package/dist/core/extensions/builtin/anthropic-subscription/settings.d.ts +2 -0
  84. package/dist/core/extensions/builtin/anthropic-subscription/settings.js +4 -0
  85. package/dist/core/extensions/builtin/anthropic-subscription/stream-guidance.js +4 -2
  86. package/dist/core/extensions/handler-budget.d.ts +84 -0
  87. package/dist/core/extensions/handler-budget.js +136 -0
  88. package/dist/core/extensions/runner.d.ts +23 -0
  89. package/dist/core/extensions/runner.js +184 -99
  90. package/dist/core/footer-data-provider.js +2 -1
  91. package/dist/core/keybindings.d.ts +1 -1
  92. package/dist/core/keybindings.js +3 -2
  93. package/dist/core/model-resolver.js +1 -1
  94. package/dist/core/pending-input-journal.d.ts +6 -1
  95. package/dist/core/pending-input-journal.js +90 -34
  96. package/dist/core/pending-input-public.d.ts +11 -0
  97. package/dist/core/pending-input-public.js +10 -0
  98. package/dist/core/pending-input-session-port.d.ts +15 -0
  99. package/dist/core/pending-input-session-port.js +93 -0
  100. package/dist/core/pending-input-session.d.ts +108 -0
  101. package/dist/core/pending-input-session.js +220 -0
  102. package/dist/core/pending-input-store.d.ts +32 -1
  103. package/dist/core/pending-input-store.js +74 -26
  104. package/dist/core/pending-input-types.d.ts +25 -4
  105. package/dist/core/pending-input.js +36 -18
  106. package/dist/core/session-entry-file.js +1 -0
  107. package/dist/core/session-log.js +1 -1
  108. package/dist/core/session-manager.d.ts +1 -0
  109. package/dist/core/session-manager.js +5 -1
  110. package/dist/core/session-move/bundle.d.ts +39 -0
  111. package/dist/core/session-move/bundle.js +108 -0
  112. package/dist/core/session-move/export.d.ts +18 -0
  113. package/dist/core/session-move/export.js +83 -0
  114. package/dist/core/session-move/finish.d.ts +24 -0
  115. package/dist/core/session-move/finish.js +81 -0
  116. package/dist/core/session-move/import.d.ts +32 -0
  117. package/dist/core/session-move/import.js +124 -0
  118. package/dist/core/session-move/move-ledger.d.ts +47 -0
  119. package/dist/core/session-move/move-ledger.js +151 -0
  120. package/dist/core/session-move/ownership.d.ts +61 -0
  121. package/dist/core/session-move/ownership.js +89 -0
  122. package/dist/core/session-move/requeue.d.ts +8 -0
  123. package/dist/core/session-move/requeue.js +18 -0
  124. package/dist/core/session-move/write-guard.d.ts +22 -0
  125. package/dist/core/session-move/write-guard.js +44 -0
  126. package/dist/core/settings-manager.d.ts +13 -0
  127. package/dist/core/settings-manager.js +17 -0
  128. package/dist/core/turn-deadline.d.ts +2 -0
  129. package/dist/core/turn-deadline.js +10 -3
  130. package/dist/modes/interactive/components/ask-user-async-widget.d.ts +7 -0
  131. package/dist/modes/interactive/components/ask-user-async-widget.js +20 -6
  132. package/dist/modes/rpc/connection-handler.js +3 -0
  133. package/dist/modes/rpc/effect-commands.js +3 -4
  134. package/dist/modes/rpc/host-decision.d.ts +1 -1
  135. package/dist/modes/rpc/host-decision.js +2 -0
  136. package/dist/modes/rpc/host-ensure-liveness.d.ts +8 -0
  137. package/dist/modes/rpc/host-ensure-liveness.js +18 -1
  138. package/dist/modes/rpc/host-ensure-types.d.ts +2 -0
  139. package/dist/modes/rpc/host-ensure.js +8 -2
  140. package/dist/modes/rpc/host-lifecycle-proxy.d.ts +9 -2
  141. package/dist/modes/rpc/host-lifecycle-proxy.js +36 -8
  142. package/dist/modes/rpc/host-observe-request.js +1 -0
  143. package/dist/modes/rpc/multi-session-host.js +3 -33
  144. package/dist/modes/rpc/rpc-client.d.ts +10 -0
  145. package/dist/modes/rpc/rpc-client.js +11 -0
  146. package/dist/modes/rpc/rpc-input-validation.js +8 -0
  147. package/dist/modes/rpc/rpc-types.d.ts +25 -1
  148. package/dist/modes/rpc/session-command-activity.js +1 -0
  149. package/dist/modes/rpc/session-control-actions-data.js +17 -8
  150. package/dist/modes/rpc/session-held.js +1 -0
  151. package/dist/modes/rpc/session-release.js +16 -1
  152. package/dist/modes/rpc/socket-ownership.d.ts +17 -0
  153. package/dist/modes/rpc/socket-ownership.js +52 -0
  154. package/dist/modes/rpc/tool-child/admission.d.ts +42 -0
  155. package/dist/modes/rpc/tool-child/admission.js +81 -0
  156. package/dist/modes/rpc/tool-child/admission.test.d.ts +2 -0
  157. package/dist/modes/rpc/tool-child/admission.test.js +166 -0
  158. package/dist/modes/rpc/tool-child/codec.d.ts +21 -0
  159. package/dist/modes/rpc/tool-child/codec.js +137 -0
  160. package/dist/modes/rpc/tool-child/codec.test.d.ts +2 -0
  161. package/dist/modes/rpc/tool-child/codec.test.js +234 -0
  162. package/dist/modes/rpc/tool-child/context.d.ts +42 -0
  163. package/dist/modes/rpc/tool-child/context.js +23 -0
  164. package/dist/modes/rpc/tool-child/flow-control.d.ts +24 -0
  165. package/dist/modes/rpc/tool-child/flow-control.js +80 -0
  166. package/dist/modes/rpc/tool-child/payload-ref-paths.d.ts +9 -0
  167. package/dist/modes/rpc/tool-child/payload-ref-paths.js +45 -0
  168. package/dist/modes/rpc/tool-child/payload-ref.d.ts +58 -0
  169. package/dist/modes/rpc/tool-child/payload-ref.js +166 -0
  170. package/dist/modes/rpc/tool-child/payload-ref.test.d.ts +2 -0
  171. package/dist/modes/rpc/tool-child/payload-ref.test.js +254 -0
  172. package/dist/modes/rpc/tool-child/payload-wire.d.ts +13 -0
  173. package/dist/modes/rpc/tool-child/payload-wire.js +22 -0
  174. package/dist/modes/rpc/tool-child/protocol-guard.d.ts +9 -0
  175. package/dist/modes/rpc/tool-child/protocol-guard.js +128 -0
  176. package/dist/modes/rpc/tool-child/protocol.d.ts +192 -0
  177. package/dist/modes/rpc/tool-child/protocol.js +54 -0
  178. package/dist/utils/wsl.d.ts +2 -2
  179. package/dist/utils/wsl.js +4 -4
  180. package/docs/providers.md +2 -0
  181. package/docs/rpc.md +20 -2
  182. package/docs/settings.md +1 -0
  183. package/docs/terminal-setup.md +2 -0
  184. package/package.json +10 -6
  185. package/dist/bundle/chunks/chunk-COJK5FDH.js +0 -2
  186. package/dist/bundle/chunks/chunk-ES3GHZE5.js +0 -395
  187. package/dist/bundle/chunks/chunk-IDXW4FAO.js +0 -2
  188. package/dist/bundle/chunks/chunk-PFNBY43W.js +0 -2
  189. package/dist/bundle/chunks/chunk-SA2ZLWBD.js +0 -11
  190. package/dist/bundle/chunks/openai-codex-responses-5LRKZCBW.js +0 -10
@@ -0,0 +1,254 @@
1
+ import { createHash } from "node:crypto";
2
+ import { access, mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
+ import { encodeFrame, FrameDecoder, ToolChildCodecError } from "./codec.js";
7
+ import { PayloadRefError, PayloadRefReader, PayloadRefWriter } from "./payload-ref.js";
8
+ import { refFilePath, spoolDir, sweepRefGenerations, teardownRefGeneration } from "./payload-ref-paths.js";
9
+ import { packPayload, unpackPayload } from "./payload-wire.js";
10
+ import { TOOL_CHILD_LIMITS } from "./protocol.js";
11
+ const MIB = 1024 * 1024;
12
+ let root = "";
13
+ beforeEach(async () => {
14
+ root = await mkdtemp(join(tmpdir(), "senpi-tool-child-refs-"));
15
+ });
16
+ afterEach(async () => {
17
+ await rm(root, { recursive: true, force: true });
18
+ });
19
+ const exists = (path) => access(path).then(() => true, () => false);
20
+ function deferred() {
21
+ let resolve = () => undefined;
22
+ const promise = new Promise((done) => {
23
+ resolve = done;
24
+ });
25
+ return { promise, resolve };
26
+ }
27
+ function bytesOf(size, seed = 7) {
28
+ const bytes = new Uint8Array(size);
29
+ for (let index = 0; index < size; index += 1)
30
+ bytes[index] = (index * 31 + seed) & 0xff;
31
+ return bytes;
32
+ }
33
+ /** A fake transport: every ref control the receiver sends is delivered to the writer, in order. */
34
+ function link(writer, options = {}) {
35
+ const sent = [];
36
+ const deliveries = [];
37
+ const reader = new PayloadRefReader({
38
+ root,
39
+ generation: writer.generation,
40
+ budget: options.budget,
41
+ send: (control) => {
42
+ sent.push(control);
43
+ deliveries.push(writer.handle(control));
44
+ },
45
+ });
46
+ return { reader, sent, delivered: () => Promise.all(deliveries) };
47
+ }
48
+ function received(frame) {
49
+ const [message] = new FrameDecoder().push(frame);
50
+ if (message === undefined)
51
+ throw new Error("no frame decoded");
52
+ return message;
53
+ }
54
+ /**
55
+ * The delayed-consumption scenario: the writer sends a 5 MiB `result_ref`, its sender-side operation settles, and the
56
+ * receiver opens the file only later, behind an event gate. `onSenderSettled` is the writer's settlement behavior.
57
+ */
58
+ async function delayedConsumption(onSenderSettled) {
59
+ const writer = new PayloadRefWriter({ root, generation: 1 });
60
+ const { reader, sent, delivered } = link(writer);
61
+ const payload = bytesOf(5 * MIB);
62
+ const ref = await writer.create(payload);
63
+ const frame = received(encodeFrame({ type: "result_ref", jobId: "j1", ref }));
64
+ if (frame.type !== "result_ref")
65
+ throw new Error("expected result_ref");
66
+ const gate = deferred();
67
+ const consumer = (async () => {
68
+ await gate.promise;
69
+ const acceptance = await reader.accept(frame.ref);
70
+ await delivered();
71
+ const existsAfterAck = await exists(refFilePath(root, frame.ref));
72
+ const bytes = acceptance.ok ? await reader.consume(frame.ref.refId) : undefined;
73
+ return { acceptance, existsAfterAck, bytes };
74
+ })();
75
+ await onSenderSettled(ref);
76
+ const existsAfterSettlement = await exists(refFilePath(root, ref));
77
+ gate.resolve();
78
+ const consumed = await consumer;
79
+ return {
80
+ existsAfterSettlement,
81
+ ...consumed,
82
+ sha256Verified: consumed.bytes !== undefined && createHash("sha256").update(consumed.bytes).digest("hex") === ref.sha256,
83
+ sent,
84
+ writerPending: writer.pendingIds(),
85
+ existsAfterConsume: await exists(refFilePath(root, ref)),
86
+ };
87
+ }
88
+ const meetsOwnershipTransfer = (result) => result.existsAfterSettlement &&
89
+ result.acceptance.ok &&
90
+ result.existsAfterAck &&
91
+ result.sha256Verified &&
92
+ result.sent.length === 1 &&
93
+ result.sent[0]?.type === "ref_ack" &&
94
+ result.writerPending.length === 0 &&
95
+ !result.existsAfterConsume;
96
+ describe("payload ref lifetime", () => {
97
+ it("delayed consumption: the file outlives sender-side settlement, ownership moves on ref_ack, the owner deletes", async () => {
98
+ const result = await delayedConsumption(async () => undefined);
99
+ expect(result.existsAfterSettlement).toBe(true);
100
+ expect(result.acceptance).toEqual({ ok: true });
101
+ expect(result.sent).toEqual([{ type: "ref_ack", refId: expect.any(String) }]);
102
+ expect(result.existsAfterAck).toBe(true);
103
+ expect(result.sha256Verified).toBe(true);
104
+ expect(result.existsAfterConsume).toBe(false);
105
+ expect(meetsOwnershipTransfer(result)).toBe(true);
106
+ });
107
+ it("delayed consumption negative control: the old writer-deletes-on-settlement rule is rejected", async () => {
108
+ const oldRule = (ref) => rm(refFilePath(root, ref), { force: true });
109
+ const result = await delayedConsumption(oldRule);
110
+ expect(result.acceptance).toEqual({ ok: false, reason: "missing" });
111
+ expect(result.sent).toEqual([{ type: "ref_nack", refId: expect.any(String), reason: "missing" }]);
112
+ expect(meetsOwnershipTransfer(result)).toBe(false);
113
+ });
114
+ it("a corrupted file is nacked and the writer deletes it", async () => {
115
+ const writer = new PayloadRefWriter({ root, generation: 1 });
116
+ const { reader, sent, delivered } = link(writer);
117
+ const ref = await writer.create(bytesOf(1024));
118
+ await writeFile(refFilePath(root, ref), bytesOf(1024, 99));
119
+ expect(await reader.accept(ref)).toEqual({ ok: false, reason: "corrupt" });
120
+ await delivered();
121
+ expect(sent).toEqual([{ type: "ref_nack", refId: ref.refId, reason: "corrupt" }]);
122
+ expect(await exists(refFilePath(root, ref))).toBe(false);
123
+ expect(writer.pendingIds()).toEqual([]);
124
+ expect(writer.ownedBytes).toBe(0);
125
+ });
126
+ it("cancellation before ack sends ref_nack{cancelled} and the writer deletes", async () => {
127
+ const writer = new PayloadRefWriter({ root, generation: 1 });
128
+ const { reader, sent, delivered } = link(writer);
129
+ const first = await writer.create(bytesOf(2048));
130
+ const second = await writer.create(bytesOf(2048));
131
+ const controller = new AbortController();
132
+ controller.abort();
133
+ expect(await reader.accept(first, controller.signal)).toEqual({ ok: false, reason: "cancelled" });
134
+ reader.refuse(second, "cancelled");
135
+ await delivered();
136
+ expect(sent).toEqual([
137
+ { type: "ref_nack", refId: first.refId, reason: "cancelled" },
138
+ { type: "ref_nack", refId: second.refId, reason: "cancelled" },
139
+ ]);
140
+ expect(await exists(refFilePath(root, first))).toBe(false);
141
+ expect(await exists(refFilePath(root, second))).toBe(false);
142
+ });
143
+ it("receiver death before ack: generation teardown deletes the ref and spool/", async () => {
144
+ const writer = new PayloadRefWriter({ root, generation: 4 });
145
+ const ref = await writer.create(bytesOf(4096));
146
+ received(encodeFrame({ type: "result_ref", jobId: "j1", ref }));
147
+ await mkdir(spoolDir(root, 4), { recursive: true });
148
+ await writeFile(join(spoolDir(root, 4), "entry-1.chunk"), "spooled");
149
+ await writer.teardown();
150
+ expect(await exists(refFilePath(root, ref))).toBe(false);
151
+ expect(await exists(spoolDir(root, 4))).toBe(false);
152
+ expect(writer.pendingIds()).toEqual([]);
153
+ });
154
+ it("death before ack of an earlier process: the generation sweep removes every generation that is not live", async () => {
155
+ const stale = new PayloadRefWriter({ root, generation: 2 });
156
+ const staleRef = await stale.create(bytesOf(64));
157
+ await mkdir(spoolDir(root, 2), { recursive: true });
158
+ const live = new PayloadRefWriter({ root, generation: 3 });
159
+ const liveRef = await live.create(bytesOf(64));
160
+ expect(await sweepRefGenerations(root, new Set([3]))).toEqual([2]);
161
+ expect(await exists(refFilePath(root, staleRef))).toBe(false);
162
+ expect(await exists(spoolDir(root, 2))).toBe(false);
163
+ expect(await exists(refFilePath(root, liveRef))).toBe(true);
164
+ });
165
+ it("an acked ref whose owner dies is deleted by the teardown sweep", async () => {
166
+ const writer = new PayloadRefWriter({ root, generation: 1 });
167
+ const { reader, delivered } = link(writer);
168
+ const ref = await writer.create(bytesOf(8192));
169
+ expect(await reader.accept(ref)).toEqual({ ok: true });
170
+ await delivered();
171
+ expect(writer.pendingIds()).toEqual([]);
172
+ expect(reader.ownedIds()).toEqual([ref.refId]);
173
+ expect(await exists(refFilePath(root, ref))).toBe(true);
174
+ await teardownRefGeneration(root, 1);
175
+ expect(await exists(refFilePath(root, ref))).toBe(false);
176
+ });
177
+ it("refuses a ref past refsBudget with ref_budget_exceeded, on both sides", async () => {
178
+ expect(new PayloadRefWriter({ root, generation: 1 }).budget).toBe(TOOL_CHILD_LIMITS.refsBudget);
179
+ const writer = new PayloadRefWriter({ root, generation: 1, budget: 6 * MIB });
180
+ const first = await writer.create(bytesOf(5 * MIB));
181
+ await expect(writer.create(bytesOf(2 * MIB))).rejects.toMatchObject({ code: "ref_budget_exceeded" });
182
+ await expect(writer.create(bytesOf(2 * MIB))).rejects.toBeInstanceOf(PayloadRefError);
183
+ expect(await readdir(join(refFilePath(root, first), ".."))).toEqual([first.refId]);
184
+ const { reader, sent, delivered } = link(writer, { budget: 4 * MIB });
185
+ expect(await reader.accept(first)).toEqual({ ok: false, reason: "budget" });
186
+ await delivered();
187
+ expect(sent).toEqual([{ type: "ref_nack", refId: first.refId, reason: "budget" }]);
188
+ expect(writer.ownedBytes).toBe(0);
189
+ await writer.create(bytesOf(5 * MIB));
190
+ });
191
+ it("reports an unacknowledged ref overdue after refAckMs without deleting it", async () => {
192
+ let now = 1_000;
193
+ const writer = new PayloadRefWriter({ root, generation: 1, now: () => now });
194
+ const ref = await writer.create(bytesOf(16));
195
+ now += TOOL_CHILD_LIMITS.refAckMs - 1;
196
+ expect(writer.overdue()).toEqual([]);
197
+ now += 1;
198
+ expect(writer.overdue()).toEqual([ref.refId]);
199
+ expect(await exists(refFilePath(root, ref))).toBe(true);
200
+ });
201
+ });
202
+ describe("payloads above the reference threshold, in both directions", () => {
203
+ it("a 17 MiB host_call.args (child -> server) round-trips by acknowledged reference", async () => {
204
+ const childWriter = new PayloadRefWriter({ root, generation: 5 });
205
+ const { reader: serverReader, sent, delivered } = link(childWriter);
206
+ const args = { path: "big.txt", content: "a".repeat(17 * MIB) };
207
+ const inlineAttempt = () => encodeFrame({ type: "host_call", callId: "c1", jobId: "j1", tool: "write", args });
208
+ expect(inlineAttempt).toThrow(ToolChildCodecError);
209
+ const packed = await packPayload(args, childWriter);
210
+ if (packed.inline)
211
+ throw new Error("17 MiB args must move by reference");
212
+ const frame = received(encodeFrame({ type: "host_call", callId: "c1", jobId: "j1", tool: "write", argsRef: packed.ref }));
213
+ if (frame.type !== "host_call" || frame.argsRef === undefined)
214
+ throw new Error("expected host_call with argsRef");
215
+ expect(await unpackPayload(frame.argsRef, serverReader)).toEqual(args);
216
+ await delivered();
217
+ expect(sent).toEqual([{ type: "ref_ack", refId: packed.ref.refId }]);
218
+ expect(await exists(refFilePath(root, packed.ref))).toBe(false);
219
+ expect(childWriter.pendingIds()).toEqual([]);
220
+ });
221
+ it("a 20 MiB host_reply.value (server -> child) round-trips by acknowledged reference", async () => {
222
+ const serverWriter = new PayloadRefWriter({ root, generation: 5 });
223
+ const { reader: childReader, sent, delivered } = link(serverWriter);
224
+ const value = "v".repeat(20 * MIB);
225
+ const packed = await packPayload(value, serverWriter);
226
+ if (packed.inline)
227
+ throw new Error("20 MiB value must move by reference");
228
+ const frame = received(encodeFrame({ type: "host_reply", callId: "c1", valueRef: packed.ref }));
229
+ if (frame.type !== "host_reply" || frame.valueRef === undefined)
230
+ throw new Error("expected host_reply with valueRef");
231
+ expect(await unpackPayload(frame.valueRef, childReader)).toEqual(value);
232
+ await delivered();
233
+ expect(sent).toEqual([{ type: "ref_ack", refId: packed.ref.refId }]);
234
+ expect(await exists(refFilePath(root, packed.ref))).toBe(false);
235
+ });
236
+ it("keeps a payload at or below REF_THRESHOLD inline and writes no file", async () => {
237
+ const writer = new PayloadRefWriter({ root, generation: 1 });
238
+ expect(await packPayload({ small: true }, writer)).toEqual({ inline: true, value: { small: true } });
239
+ expect(await exists(join(root, "gen-1"))).toBe(false);
240
+ });
241
+ it("unpackPayload rejects a ref it had to nack", async () => {
242
+ const writer = new PayloadRefWriter({ root, generation: 1 });
243
+ const { reader } = link(writer);
244
+ const packed = await packPayload("x".repeat(TOOL_CHILD_LIMITS.REF_THRESHOLD + 1), writer);
245
+ if (packed.inline)
246
+ throw new Error("expected a ref");
247
+ await rm(refFilePath(root, packed.ref));
248
+ await expect(unpackPayload(packed.ref, reader)).rejects.toMatchObject({
249
+ code: "ref_rejected",
250
+ reason: "missing",
251
+ });
252
+ });
253
+ });
254
+ //# sourceMappingURL=payload-ref.test.js.map
@@ -0,0 +1,13 @@
1
+ import { type PayloadRefReader, type PayloadRefWriter } from "./payload-ref.ts";
2
+ import { type PayloadRef } from "./protocol.ts";
3
+ export type PackedPayload = {
4
+ inline: true;
5
+ value: unknown;
6
+ } | {
7
+ inline: false;
8
+ ref: PayloadRef;
9
+ };
10
+ /** Inline at or below `REF_THRESHOLD` JSON bytes; above it, by acknowledged reference. Same rule in both directions. */
11
+ export declare function packPayload(value: unknown, writer: PayloadRefWriter): Promise<PackedPayload>;
12
+ export declare function unpackPayload(ref: PayloadRef, reader: PayloadRefReader, signal?: AbortSignal): Promise<unknown>;
13
+ //# sourceMappingURL=payload-wire.d.ts.map
@@ -0,0 +1,22 @@
1
+ import { PayloadRefError } from "./payload-ref.js";
2
+ import { TOOL_CHILD_LIMITS } from "./protocol.js";
3
+ /** Inline at or below `REF_THRESHOLD` JSON bytes; above it, by acknowledged reference. Same rule in both directions. */
4
+ export async function packPayload(value, writer) {
5
+ // `undefined` stringifies to nothing (JSON.stringify returns undefined), which no decoder accepts; the explicit
6
+ // null is the encoding the peer's guard takes (senpi#3569 review).
7
+ const normalized = value === undefined ? null : value;
8
+ const json = JSON.stringify(normalized);
9
+ if (json === undefined || Buffer.byteLength(json, "utf8") <= TOOL_CHILD_LIMITS.REF_THRESHOLD) {
10
+ return { inline: true, value: normalized };
11
+ }
12
+ return { inline: false, ref: await writer.create(Buffer.from(json, "utf8")) };
13
+ }
14
+ export async function unpackPayload(ref, reader, signal) {
15
+ const acceptance = await reader.accept(ref, signal);
16
+ if (!acceptance.ok) {
17
+ throw new PayloadRefError("ref_rejected", `ref ${ref.refId} was refused (${acceptance.reason})`, acceptance.reason);
18
+ }
19
+ const bytes = await reader.consume(ref.refId);
20
+ return JSON.parse(Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("utf8"));
21
+ }
22
+ //# sourceMappingURL=payload-wire.js.map
@@ -0,0 +1,9 @@
1
+ import { type ToolChildMessage } from "./protocol.ts";
2
+ export declare const TOOL_CHILD_MESSAGE_TYPES: readonly string[];
3
+ export declare const isRefId: (value: unknown) => value is string;
4
+ /**
5
+ * Boundary check for a decoded frame: a known `type`, every required field of the right kind, no unknown field.
6
+ * An unknown field is refused rather than ignored, so a `trace` frame can never smuggle a result, proof or tick.
7
+ */
8
+ export declare function isToolChildMessage(value: unknown): value is ToolChildMessage;
9
+ //# sourceMappingURL=protocol-guard.d.ts.map
@@ -0,0 +1,128 @@
1
+ import { LAUNCH_APIS, LAUNCH_CONTAINMENTS, LAUNCH_LIFETIMES, LAUNCH_ORIGINS, REF_NACK_REASONS, TRACE_RESERVED_KEYS, } from "./protocol.js";
2
+ const req = (kind) => ({ kind });
3
+ const opt = (kind) => ({ kind, optional: true });
4
+ const SHAPES = {
5
+ hello_ack: { generation: req("number") },
6
+ job: {
7
+ jobId: req("string"),
8
+ kind: req("string"),
9
+ parentJobId: opt("string"),
10
+ args: opt("json"),
11
+ argsRef: opt("ref"),
12
+ },
13
+ cancel: { jobId: req("string") },
14
+ host_reply: { callId: req("string"), value: opt("json"), valueRef: opt("ref"), error: opt("string") },
15
+ host_call_refused: { callId: req("string"), code: req("string") },
16
+ launch_ack: { launchId: req("string") },
17
+ wake_ack: { wakeId: req("string") },
18
+ slot_query: { launchId: req("string") },
19
+ shutdown: {},
20
+ hello: { generation: req("number"), pid: req("number"), protocol: req("number") },
21
+ heartbeat: { seq: req("number") },
22
+ result: { jobId: req("string"), value: req("json") },
23
+ result_ref: { jobId: req("string"), ref: req("ref") },
24
+ job_error: { jobId: req("string"), code: req("string"), message: req("string") },
25
+ job_refused: { jobId: req("string"), code: req("string") },
26
+ host_call: {
27
+ callId: req("string"),
28
+ jobId: req("string"),
29
+ tool: req("string"),
30
+ args: opt("json"),
31
+ argsRef: opt("ref"),
32
+ },
33
+ launch: {
34
+ launchId: req("string"),
35
+ jobId: req("string"),
36
+ origin: req(LAUNCH_ORIGINS),
37
+ api: req(LAUNCH_APIS),
38
+ containment: req(LAUNCH_CONTAINMENTS),
39
+ lifetime: req(LAUNCH_LIFETIMES),
40
+ pid: opt("number"),
41
+ pgid: opt("number"),
42
+ },
43
+ launch_proof: { launchId: req("string"), ackTick: req("number"), invokeTick: req("number") },
44
+ launch_done: { launchId: req("string"), exitCode: opt("number"), signal: opt("string") },
45
+ command_started: {
46
+ launchId: req("string"),
47
+ pid: req("number"),
48
+ startedAtMs: req("number"),
49
+ creatorInJob: req("boolean"),
50
+ breakaway: req("boolean"),
51
+ },
52
+ slot_state: {
53
+ launchId: req("string"),
54
+ ackTick: req("number|null"),
55
+ invokeTick: req("number|null"),
56
+ invocations: req("number"),
57
+ },
58
+ wake: { wakeId: req("string"), source: req("string"), bytes: req("number") },
59
+ batch: { jobId: req("string"), entries: req("entries"), done: req("boolean") },
60
+ trace: { atMs: req("number"), event: req("string"), detail: opt("detail") },
61
+ ref_ack: { refId: req("string") },
62
+ ref_nack: { refId: req("string"), reason: req(REF_NACK_REASONS) },
63
+ };
64
+ export const TOOL_CHILD_MESSAGE_TYPES = Object.keys(SHAPES);
65
+ const REF_ID = /^[A-Za-z0-9_-]{1,64}$/;
66
+ const SHA256 = /^[0-9a-f]{64}$/;
67
+ const RESERVED = new Set(TRACE_RESERVED_KEYS);
68
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
69
+ const isCount = (value) => Number.isSafeInteger(value) && Number(value) >= 0;
70
+ export const isRefId = (value) => typeof value === "string" && REF_ID.test(value);
71
+ function isPayloadRef(value) {
72
+ return (isRecord(value) &&
73
+ isRefId(value.refId) &&
74
+ isCount(value.generation) &&
75
+ isCount(value.bytes) &&
76
+ typeof value.sha256 === "string" &&
77
+ SHA256.test(value.sha256));
78
+ }
79
+ function isEntryMeta(value) {
80
+ return (isRecord(value) &&
81
+ typeof value.id === "string" &&
82
+ (value.parentId === null || typeof value.parentId === "string") &&
83
+ isCount(value.lineOffset) &&
84
+ isCount(value.lineBytes) &&
85
+ Number.isSafeInteger(value.lineDelta));
86
+ }
87
+ function isTraceDetail(value) {
88
+ if (!isRecord(value))
89
+ return false;
90
+ return Object.entries(value).every(([key, item]) => !RESERVED.has(key) && (typeof item === "string" || typeof item === "number" || typeof item === "boolean"));
91
+ }
92
+ function matches(kind, value) {
93
+ if (typeof kind !== "string")
94
+ return typeof value === "string" && kind.includes(value);
95
+ switch (kind) {
96
+ case "string":
97
+ return typeof value === "string";
98
+ case "number":
99
+ return typeof value === "number" && Number.isFinite(value);
100
+ case "boolean":
101
+ return typeof value === "boolean";
102
+ case "json":
103
+ return value !== undefined;
104
+ case "ref":
105
+ return isPayloadRef(value);
106
+ case "entries":
107
+ return Array.isArray(value) && value.every(isEntryMeta);
108
+ case "detail":
109
+ return isTraceDetail(value);
110
+ case "number|null":
111
+ return value === null || (typeof value === "number" && Number.isFinite(value));
112
+ }
113
+ }
114
+ const isMessageType = (value) => typeof value === "string" && Object.hasOwn(SHAPES, value);
115
+ /**
116
+ * Boundary check for a decoded frame: a known `type`, every required field of the right kind, no unknown field.
117
+ * An unknown field is refused rather than ignored, so a `trace` frame can never smuggle a result, proof or tick.
118
+ */
119
+ export function isToolChildMessage(value) {
120
+ if (!isRecord(value) || !isMessageType(value.type))
121
+ return false;
122
+ const rules = SHAPES[value.type];
123
+ for (const key of Object.keys(value))
124
+ if (key !== "type" && !Object.hasOwn(rules, key))
125
+ return false;
126
+ return Object.entries(rules).every(([key, rule]) => value[key] === undefined ? rule.optional === true : matches(rule.kind, value[key]));
127
+ }
128
+ //# sourceMappingURL=protocol-guard.js.map
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Wire vocabulary between the server and a session's tool child (senpi#3566).
3
+ *
4
+ * Every frame crosses the private channel as one length-prefixed binary frame (`codec.ts`), never as NDJSON. Payloads
5
+ * above `REF_THRESHOLD` never travel inline: they move as an acknowledged `PayloadRef` (`payload-ref.ts`) in either
6
+ * direction. `trace` is the only lossy frame class; it never carries a job result, a launch proof or a tick.
7
+ */
8
+ export declare const TOOL_CHILD_LIMITS: {
9
+ /** Largest encoded frame body, in bytes; a larger length header is refused before its body is allocated. */
10
+ readonly frameBytes: number;
11
+ /** Ordinary jobs one child runs at once; nested jobs are bounded by `hostCallCapacity()` instead. */
12
+ readonly jobs: 32;
13
+ /** Wake notifications awaiting `wake_ack`, by count and by summed size; the next one waits (never dropped). */
14
+ readonly wakePending: 64;
15
+ readonly wakePendingBytes: number;
16
+ readonly HOST_CALL_ACTIVE: 8;
17
+ readonly HOST_CALL_WAITING: 256;
18
+ /** A payload whose JSON exceeds this many bytes moves by reference, never inline. */
19
+ readonly REF_THRESHOLD: number;
20
+ /** A ref still unacknowledged after this long is reported overdue (the writer still does not delete it). */
21
+ readonly refAckMs: 30000;
22
+ /** Bytes of ref files one side may own at once. */
23
+ readonly refsBudget: number;
24
+ readonly tracePerSecond: 1024;
25
+ };
26
+ export declare const LAUNCH_CONTAINMENTS: readonly ["own-group", "inherited", "job", "breakaway"];
27
+ export type LaunchContainment = (typeof LAUNCH_CONTAINMENTS)[number];
28
+ export declare const LAUNCH_LIFETIMES: readonly ["owned", "detached"];
29
+ export type LaunchLifetime = (typeof LAUNCH_LIFETIMES)[number];
30
+ /** Which surface asked for the process. */
31
+ export declare const LAUNCH_ORIGINS: readonly ["tool", "terminal", "user_bash", "interpreter", "monitor", "kernel"];
32
+ export type LaunchOrigin = (typeof LAUNCH_ORIGINS)[number];
33
+ /** The launch API the command came through; group semantics differ per API. */
34
+ export declare const LAUNCH_APIS: readonly ["bun.spawn", "bun.spawnSync", "bun.shell", "cp.spawn", "cp.spawnSync", "cp.execFileSync"];
35
+ export type LaunchApi = (typeof LAUNCH_APIS)[number];
36
+ export declare const REF_NACK_REASONS: readonly ["cancelled", "corrupt", "missing", "budget", "foreign"];
37
+ export type RefNackReason = (typeof REF_NACK_REASONS)[number];
38
+ /** A payload moved through a file in the channel generation's directory. The receiver derives the path itself. */
39
+ export interface PayloadRef {
40
+ refId: string;
41
+ /** The channel's child generation; refs of both directions live under that generation's directory. */
42
+ generation: number;
43
+ bytes: number;
44
+ sha256: string;
45
+ }
46
+ /** One parsed transcript entry, with its canonical identity and its exact place in the file. */
47
+ export interface ParsedEntryMeta {
48
+ id: string;
49
+ /** Canonical parent from the file line, never from a compacted in-memory mirror. */
50
+ parentId: string | null;
51
+ lineOffset: number;
52
+ lineBytes: number;
53
+ /** Wire bytes minus line bytes after the canonical transform. */
54
+ lineDelta: number;
55
+ entry?: unknown;
56
+ }
57
+ export type TraceDetail = Readonly<Record<string, string | number | boolean>>;
58
+ /** Keys a `trace` detail may never carry: the lossy class never transports results, proofs or ticks. */
59
+ export declare const TRACE_RESERVED_KEYS: readonly ["value", "valueRef", "result", "ref", "args", "argsRef", "ackTick", "invokeTick"];
60
+ export type ServerToChild = {
61
+ type: "hello_ack";
62
+ generation: number;
63
+ } | {
64
+ type: "job";
65
+ jobId: string;
66
+ kind: string;
67
+ parentJobId?: string;
68
+ args?: unknown;
69
+ argsRef?: PayloadRef;
70
+ } | {
71
+ type: "cancel";
72
+ jobId: string;
73
+ } | {
74
+ type: "host_reply";
75
+ callId: string;
76
+ value?: unknown;
77
+ valueRef?: PayloadRef;
78
+ error?: string;
79
+ } | {
80
+ type: "host_call_refused";
81
+ callId: string;
82
+ code: string;
83
+ } | {
84
+ type: "launch_ack";
85
+ launchId: string;
86
+ } | {
87
+ type: "wake_ack";
88
+ wakeId: string;
89
+ } | {
90
+ type: "slot_query";
91
+ launchId: string;
92
+ } | {
93
+ type: "shutdown";
94
+ };
95
+ export type ChildToServer = {
96
+ type: "hello";
97
+ generation: number;
98
+ pid: number;
99
+ protocol: number;
100
+ } | {
101
+ type: "heartbeat";
102
+ seq: number;
103
+ } | {
104
+ type: "result";
105
+ jobId: string;
106
+ value: unknown;
107
+ } | {
108
+ type: "result_ref";
109
+ jobId: string;
110
+ ref: PayloadRef;
111
+ } | {
112
+ type: "job_error";
113
+ jobId: string;
114
+ code: string;
115
+ message: string;
116
+ } | {
117
+ type: "job_refused";
118
+ jobId: string;
119
+ code: string;
120
+ } | {
121
+ type: "host_call";
122
+ callId: string;
123
+ jobId: string;
124
+ tool: string;
125
+ args?: unknown;
126
+ argsRef?: PayloadRef;
127
+ } | {
128
+ type: "launch";
129
+ launchId: string;
130
+ jobId: string;
131
+ origin: LaunchOrigin;
132
+ api: LaunchApi;
133
+ containment: LaunchContainment;
134
+ lifetime: LaunchLifetime;
135
+ pid?: number;
136
+ pgid?: number;
137
+ } | {
138
+ type: "launch_proof";
139
+ launchId: string;
140
+ ackTick: number;
141
+ invokeTick: number;
142
+ } | {
143
+ type: "launch_done";
144
+ launchId: string;
145
+ exitCode?: number;
146
+ signal?: string;
147
+ } | {
148
+ type: "command_started";
149
+ launchId: string;
150
+ pid: number;
151
+ startedAtMs: number;
152
+ creatorInJob: boolean;
153
+ breakaway: boolean;
154
+ } | {
155
+ type: "slot_state";
156
+ launchId: string;
157
+ ackTick: number | null;
158
+ invokeTick: number | null;
159
+ invocations: number;
160
+ } | {
161
+ type: "wake";
162
+ wakeId: string;
163
+ source: string;
164
+ bytes: number;
165
+ } | {
166
+ type: "batch";
167
+ jobId: string;
168
+ entries: ParsedEntryMeta[];
169
+ done: boolean;
170
+ } | {
171
+ type: "trace";
172
+ atMs: number;
173
+ event: string;
174
+ detail?: TraceDetail;
175
+ };
176
+ /** Either side acknowledges or refuses a ref the other side wrote. */
177
+ export type RefControl = {
178
+ type: "ref_ack";
179
+ refId: string;
180
+ } | {
181
+ type: "ref_nack";
182
+ refId: string;
183
+ reason: RefNackReason;
184
+ };
185
+ export type ToolChildMessage = ServerToChild | ChildToServer | RefControl;
186
+ export type ToolChildMessageType = ToolChildMessage["type"];
187
+ export type ToolChildMessageOf<T extends ToolChildMessageType> = Extract<ToolChildMessage, {
188
+ type: T;
189
+ }>;
190
+ /** The only frame class a full budget may drop (counted). */
191
+ export declare const LOSSY_FRAME_TYPES: ReadonlySet<ToolChildMessageType>;
192
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Wire vocabulary between the server and a session's tool child (senpi#3566).
3
+ *
4
+ * Every frame crosses the private channel as one length-prefixed binary frame (`codec.ts`), never as NDJSON. Payloads
5
+ * above `REF_THRESHOLD` never travel inline: they move as an acknowledged `PayloadRef` (`payload-ref.ts`) in either
6
+ * direction. `trace` is the only lossy frame class; it never carries a job result, a launch proof or a tick.
7
+ */
8
+ const MIB = 1024 * 1024;
9
+ export const TOOL_CHILD_LIMITS = {
10
+ /** Largest encoded frame body, in bytes; a larger length header is refused before its body is allocated. */
11
+ frameBytes: 16 * MIB,
12
+ /** Ordinary jobs one child runs at once; nested jobs are bounded by `hostCallCapacity()` instead. */
13
+ jobs: 32,
14
+ /** Wake notifications awaiting `wake_ack`, by count and by summed size; the next one waits (never dropped). */
15
+ wakePending: 64,
16
+ wakePendingBytes: 1 * MIB,
17
+ HOST_CALL_ACTIVE: 8,
18
+ HOST_CALL_WAITING: 256,
19
+ /** A payload whose JSON exceeds this many bytes moves by reference, never inline. */
20
+ REF_THRESHOLD: 4 * MIB,
21
+ /** A ref still unacknowledged after this long is reported overdue (the writer still does not delete it). */
22
+ refAckMs: 30_000,
23
+ /** Bytes of ref files one side may own at once. */
24
+ refsBudget: 1024 * MIB,
25
+ tracePerSecond: 1024,
26
+ };
27
+ export const LAUNCH_CONTAINMENTS = ["own-group", "inherited", "job", "breakaway"];
28
+ export const LAUNCH_LIFETIMES = ["owned", "detached"];
29
+ /** Which surface asked for the process. */
30
+ export const LAUNCH_ORIGINS = ["tool", "terminal", "user_bash", "interpreter", "monitor", "kernel"];
31
+ /** The launch API the command came through; group semantics differ per API. */
32
+ export const LAUNCH_APIS = [
33
+ "bun.spawn",
34
+ "bun.spawnSync",
35
+ "bun.shell",
36
+ "cp.spawn",
37
+ "cp.spawnSync",
38
+ "cp.execFileSync",
39
+ ];
40
+ export const REF_NACK_REASONS = ["cancelled", "corrupt", "missing", "budget", "foreign"];
41
+ /** Keys a `trace` detail may never carry: the lossy class never transports results, proofs or ticks. */
42
+ export const TRACE_RESERVED_KEYS = [
43
+ "value",
44
+ "valueRef",
45
+ "result",
46
+ "ref",
47
+ "args",
48
+ "argsRef",
49
+ "ackTick",
50
+ "invokeTick",
51
+ ];
52
+ /** The only frame class a full budget may drop (counted). */
53
+ export const LOSSY_FRAME_TYPES = new Set(["trace"]);
54
+ //# sourceMappingURL=protocol.js.map