@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,42 @@
1
+ import type { BrowserEngine } from "../../../core/browser-engine.ts";
2
+ import type { ExtensionMode } from "../../../core/extensions/types.ts";
3
+ /**
4
+ * What a tool body running in the tool child may see of its extension context: plain, serializable data only.
5
+ * No functions, no `ui`, no `sessionManager` - those stay on the server and are reached through host calls.
6
+ */
7
+ export interface ChildToolContext {
8
+ cwd: string;
9
+ agentDir: string;
10
+ mode: ExtensionMode;
11
+ toolCallId: string;
12
+ projectTrusted: boolean;
13
+ sessionId?: string;
14
+ model?: {
15
+ provider: string;
16
+ id: string;
17
+ };
18
+ thinkingLevel?: string;
19
+ serviceTier?: string;
20
+ browserEngine?: BrowserEngine;
21
+ goalStoreFile?: string;
22
+ }
23
+ /** The fields `narrowContext` reads; `ExtensionContext` satisfies it structurally. */
24
+ export interface NarrowableContext {
25
+ readonly cwd: string;
26
+ readonly agentDir: string;
27
+ readonly mode: ExtensionMode;
28
+ readonly model: {
29
+ readonly provider: string;
30
+ readonly id: string;
31
+ } | undefined;
32
+ readonly thinkingLevel?: string | undefined;
33
+ readonly serviceTier?: string | undefined;
34
+ readonly browserEngine?: BrowserEngine | undefined;
35
+ readonly goalStoreFile?: string | undefined;
36
+ isProjectTrusted(): boolean;
37
+ }
38
+ export declare function narrowContext(ctx: NarrowableContext, ids: {
39
+ sessionId?: string | undefined;
40
+ toolCallId: string;
41
+ }): ChildToolContext;
42
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1,23 @@
1
+ export function narrowContext(ctx, ids) {
2
+ const narrowed = {
3
+ cwd: ctx.cwd,
4
+ agentDir: ctx.agentDir,
5
+ mode: ctx.mode,
6
+ toolCallId: ids.toolCallId,
7
+ projectTrusted: ctx.isProjectTrusted(),
8
+ };
9
+ if (ids.sessionId !== undefined)
10
+ narrowed.sessionId = ids.sessionId;
11
+ if (ctx.model !== undefined)
12
+ narrowed.model = { provider: ctx.model.provider, id: ctx.model.id };
13
+ if (ctx.thinkingLevel !== undefined)
14
+ narrowed.thinkingLevel = ctx.thinkingLevel;
15
+ if (ctx.serviceTier !== undefined)
16
+ narrowed.serviceTier = ctx.serviceTier;
17
+ if (ctx.browserEngine !== undefined)
18
+ narrowed.browserEngine = ctx.browserEngine;
19
+ if (ctx.goalStoreFile !== undefined)
20
+ narrowed.goalStoreFile = ctx.goalStoreFile;
21
+ return narrowed;
22
+ }
23
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1,24 @@
1
+ import { type ToolChildMessage, type ToolChildMessageOf } from "./protocol.ts";
2
+ export declare class WakeAdmissionError extends Error {
3
+ readonly code = "wake_too_large";
4
+ constructor(bytes: number);
5
+ }
6
+ /**
7
+ * Wake notifications awaiting `wake_ack`, bounded by count and summed bytes. A wake past either bound waits, FIFO,
8
+ * until an ack frees room: wakes are durable wake-source demand and are never dropped.
9
+ */
10
+ export declare class WakeAdmission {
11
+ #private;
12
+ get pending(): number;
13
+ get blocked(): number;
14
+ send(wakeId: string, bytes: number): Promise<void>;
15
+ ack(wakeId: string): void;
16
+ }
17
+ /** Per-second budget for `trace` frames, the only lossy class: an over-budget frame is dropped and counted. */
18
+ export declare class TraceBudget {
19
+ #private;
20
+ get dropped(): number;
21
+ admit(_frame: ToolChildMessageOf<"trace">, nowMs: number): boolean;
22
+ }
23
+ export declare function isLossy(message: ToolChildMessage): boolean;
24
+ //# sourceMappingURL=flow-control.d.ts.map
@@ -0,0 +1,80 @@
1
+ import { LOSSY_FRAME_TYPES, TOOL_CHILD_LIMITS } from "./protocol.js";
2
+ export class WakeAdmissionError extends Error {
3
+ constructor(bytes) {
4
+ super(`wake_too_large: ${bytes} bytes exceeds the ${TOOL_CHILD_LIMITS.wakePendingBytes}-byte pending budget`);
5
+ this.code = "wake_too_large";
6
+ this.name = "WakeAdmissionError";
7
+ }
8
+ }
9
+ /**
10
+ * Wake notifications awaiting `wake_ack`, bounded by count and summed bytes. A wake past either bound waits, FIFO,
11
+ * until an ack frees room: wakes are durable wake-source demand and are never dropped.
12
+ */
13
+ export class WakeAdmission {
14
+ #pending = new Map();
15
+ #blocked = [];
16
+ #pendingBytes = 0;
17
+ get pending() {
18
+ return this.#pending.size;
19
+ }
20
+ get blocked() {
21
+ return this.#blocked.length;
22
+ }
23
+ send(wakeId, bytes) {
24
+ if (bytes > TOOL_CHILD_LIMITS.wakePendingBytes)
25
+ return Promise.reject(new WakeAdmissionError(bytes));
26
+ if (this.#blocked.length === 0 && this.#fits(bytes)) {
27
+ this.#admit(wakeId, bytes);
28
+ return Promise.resolve();
29
+ }
30
+ return new Promise((release) => {
31
+ this.#blocked.push({ wakeId, bytes, release });
32
+ });
33
+ }
34
+ ack(wakeId) {
35
+ const bytes = this.#pending.get(wakeId);
36
+ if (bytes === undefined)
37
+ return;
38
+ this.#pending.delete(wakeId);
39
+ this.#pendingBytes -= bytes;
40
+ for (let next = this.#blocked[0]; next !== undefined && this.#fits(next.bytes); next = this.#blocked[0]) {
41
+ this.#blocked.shift();
42
+ this.#admit(next.wakeId, next.bytes);
43
+ next.release();
44
+ }
45
+ }
46
+ #fits(bytes) {
47
+ return (this.#pending.size < TOOL_CHILD_LIMITS.wakePending &&
48
+ this.#pendingBytes + bytes <= TOOL_CHILD_LIMITS.wakePendingBytes);
49
+ }
50
+ #admit(wakeId, bytes) {
51
+ this.#pending.set(wakeId, bytes);
52
+ this.#pendingBytes += bytes;
53
+ }
54
+ }
55
+ /** Per-second budget for `trace` frames, the only lossy class: an over-budget frame is dropped and counted. */
56
+ export class TraceBudget {
57
+ #second = Number.NaN;
58
+ #admitted = 0;
59
+ #dropped = 0;
60
+ get dropped() {
61
+ return this.#dropped;
62
+ }
63
+ admit(_frame, nowMs) {
64
+ const second = Math.floor(nowMs / 1000);
65
+ if (second !== this.#second) {
66
+ this.#second = second;
67
+ this.#admitted = 0;
68
+ }
69
+ if (this.#admitted >= TOOL_CHILD_LIMITS.tracePerSecond) {
70
+ this.#dropped += 1;
71
+ return false;
72
+ }
73
+ this.#admitted += 1;
74
+ return true;
75
+ }
76
+ }
77
+ export function isLossy(message) {
78
+ return LOSSY_FRAME_TYPES.has(message.type);
79
+ }
80
+ //# sourceMappingURL=flow-control.js.map
@@ -0,0 +1,9 @@
1
+ import type { PayloadRef } from "./protocol.ts";
2
+ export declare function refGenerationDir(root: string, generation: number): string;
3
+ export declare function refFilePath(root: string, ref: Pick<PayloadRef, "refId" | "generation">): string;
4
+ export declare function spoolDir(root: string, generation: number): string;
5
+ /** Generation teardown: every ref of that generation, acknowledged or not, and its `spool/`. */
6
+ export declare function teardownRefGeneration(root: string, generation: number): Promise<void>;
7
+ /** Removes the directory of every generation not in `live` (left by a dead child or server); returns them, sorted. */
8
+ export declare function sweepRefGenerations(root: string, live: ReadonlySet<number>): Promise<number[]>;
9
+ //# sourceMappingURL=payload-ref-paths.d.ts.map
@@ -0,0 +1,45 @@
1
+ import { readdir, rm } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { isRefId } from "./protocol-guard.js";
4
+ /**
5
+ * Layout under the channel's ref root: `gen-<generation>/refs/<refId>` and `gen-<generation>/spool/`. Refs of both
6
+ * directions live under the child generation that owns the channel, so one teardown removes all of them.
7
+ */
8
+ const GENERATION_DIR = /^gen-(\d+)$/;
9
+ export function refGenerationDir(root, generation) {
10
+ return join(root, `gen-${generation}`);
11
+ }
12
+ export function refFilePath(root, ref) {
13
+ if (!isRefId(ref.refId))
14
+ throw new Error(`invalid ref id: ${JSON.stringify(ref.refId)}`);
15
+ return join(refGenerationDir(root, ref.generation), "refs", ref.refId);
16
+ }
17
+ export function spoolDir(root, generation) {
18
+ return join(refGenerationDir(root, generation), "spool");
19
+ }
20
+ /** Generation teardown: every ref of that generation, acknowledged or not, and its `spool/`. */
21
+ export async function teardownRefGeneration(root, generation) {
22
+ await rm(refGenerationDir(root, generation), { recursive: true, force: true });
23
+ }
24
+ /** Removes the directory of every generation not in `live` (left by a dead child or server); returns them, sorted. */
25
+ export async function sweepRefGenerations(root, live) {
26
+ let names;
27
+ try {
28
+ names = await readdir(root);
29
+ }
30
+ catch (error) {
31
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
32
+ return [];
33
+ throw error;
34
+ }
35
+ const stale = names
36
+ .map((name) => GENERATION_DIR.exec(name)?.[1])
37
+ .filter((digits) => digits !== undefined)
38
+ .map(Number)
39
+ .filter((generation) => !live.has(generation))
40
+ .sort((left, right) => left - right);
41
+ for (const generation of stale)
42
+ await teardownRefGeneration(root, generation);
43
+ return stale;
44
+ }
45
+ //# sourceMappingURL=payload-ref-paths.js.map
@@ -0,0 +1,58 @@
1
+ import { type PayloadRef, type RefControl, type RefNackReason } from "./protocol.ts";
2
+ export type PayloadRefErrorCode = "ref_budget_exceeded" | "ref_rejected" | "ref_not_owned";
3
+ export declare class PayloadRefError extends Error {
4
+ readonly code: PayloadRefErrorCode;
5
+ readonly reason: RefNackReason | undefined;
6
+ constructor(code: PayloadRefErrorCode, message: string, reason?: RefNackReason);
7
+ }
8
+ export interface PayloadRefWriterOptions {
9
+ root: string;
10
+ generation: number;
11
+ budget?: number | undefined;
12
+ now?: (() => number) | undefined;
13
+ }
14
+ /**
15
+ * Writer side of an acknowledged payload reference. The writer owns a ref only until the receiver answers:
16
+ * `ref_ack` transfers ownership (the writer forgets the file and never deletes it), `ref_nack` makes the writer
17
+ * delete it. Settling the sending operation changes nothing; only generation teardown removes an unanswered ref.
18
+ */
19
+ export declare class PayloadRefWriter {
20
+ #private;
21
+ readonly generation: number;
22
+ readonly budget: number;
23
+ constructor(options: PayloadRefWriterOptions);
24
+ get ownedBytes(): number;
25
+ create(payload: Uint8Array): Promise<PayloadRef>;
26
+ /** A `ref_ack` or `ref_nack` from the receiver; an answer for a ref this writer does not hold is ignored. */
27
+ handle(control: RefControl): Promise<void>;
28
+ pendingIds(): string[];
29
+ /** Refs still unanswered after `refAckMs`; reported for liveness handling, never deleted here. */
30
+ overdue(): string[];
31
+ teardown(): Promise<void>;
32
+ }
33
+ export interface PayloadRefReaderOptions {
34
+ root: string;
35
+ generation: number;
36
+ budget?: number | undefined;
37
+ send: (control: RefControl) => void;
38
+ }
39
+ export type RefAcceptance = {
40
+ ok: true;
41
+ } | {
42
+ ok: false;
43
+ reason: RefNackReason;
44
+ };
45
+ /**
46
+ * Receiver side. `accept` verifies size and sha256 and answers `ref_ack` (taking ownership) or `ref_nack`.
47
+ * The owner deletes the file when it consumes it; a dead owner's files go with its generation's teardown.
48
+ */
49
+ export declare class PayloadRefReader {
50
+ #private;
51
+ constructor(options: PayloadRefReaderOptions);
52
+ accept(ref: PayloadRef, signal?: AbortSignal): Promise<RefAcceptance>;
53
+ /** Answers `ref_nack` for a ref this side will not take, e.g. one whose job was cancelled before it was opened. */
54
+ refuse(ref: PayloadRef, reason: RefNackReason): void;
55
+ consume(refId: string): Promise<Uint8Array>;
56
+ ownedIds(): string[];
57
+ }
58
+ //# sourceMappingURL=payload-ref.d.ts.map
@@ -0,0 +1,166 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
4
+ import { dirname } from "node:path";
5
+ import { refFilePath, teardownRefGeneration } from "./payload-ref-paths.js";
6
+ import { TOOL_CHILD_LIMITS } from "./protocol.js";
7
+ import { isRefId } from "./protocol-guard.js";
8
+ export class PayloadRefError extends Error {
9
+ constructor(code, message, reason) {
10
+ super(`${code}: ${message}`);
11
+ this.name = "PayloadRefError";
12
+ this.code = code;
13
+ this.reason = reason;
14
+ }
15
+ }
16
+ /**
17
+ * Writer side of an acknowledged payload reference. The writer owns a ref only until the receiver answers:
18
+ * `ref_ack` transfers ownership (the writer forgets the file and never deletes it), `ref_nack` makes the writer
19
+ * delete it. Settling the sending operation changes nothing; only generation teardown removes an unanswered ref.
20
+ */
21
+ export class PayloadRefWriter {
22
+ #root;
23
+ #now;
24
+ #pending = new Map();
25
+ #ownedBytes = 0;
26
+ constructor(options) {
27
+ this.generation = options.generation;
28
+ this.budget = options.budget ?? TOOL_CHILD_LIMITS.refsBudget;
29
+ this.#root = options.root;
30
+ this.#now = options.now ?? Date.now;
31
+ }
32
+ get ownedBytes() {
33
+ return this.#ownedBytes;
34
+ }
35
+ async create(payload) {
36
+ const size = payload.byteLength;
37
+ if (this.#ownedBytes + size > this.budget) {
38
+ throw new PayloadRefError("ref_budget_exceeded", `${size} bytes on top of ${this.#ownedBytes} owned exceeds ${this.budget}`);
39
+ }
40
+ this.#ownedBytes += size;
41
+ const ref = {
42
+ refId: randomBytes(12).toString("hex"),
43
+ generation: this.generation,
44
+ bytes: size,
45
+ sha256: createHash("sha256").update(payload).digest("hex"),
46
+ };
47
+ const path = refFilePath(this.#root, ref);
48
+ try {
49
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
50
+ await writeFile(path, payload, { flag: "wx", mode: 0o600 });
51
+ }
52
+ catch (error) {
53
+ this.#ownedBytes -= size;
54
+ await rm(path, { force: true });
55
+ throw error;
56
+ }
57
+ this.#pending.set(ref.refId, { bytes: size, createdAtMs: this.#now() });
58
+ return ref;
59
+ }
60
+ /** A `ref_ack` or `ref_nack` from the receiver; an answer for a ref this writer does not hold is ignored. */
61
+ async handle(control) {
62
+ const pending = this.#pending.get(control.refId);
63
+ if (pending === undefined)
64
+ return;
65
+ this.#pending.delete(control.refId);
66
+ this.#ownedBytes -= pending.bytes;
67
+ if (control.type === "ref_nack") {
68
+ await rm(refFilePath(this.#root, { refId: control.refId, generation: this.generation }), { force: true });
69
+ }
70
+ }
71
+ pendingIds() {
72
+ return [...this.#pending.keys()];
73
+ }
74
+ /** Refs still unanswered after `refAckMs`; reported for liveness handling, never deleted here. */
75
+ overdue() {
76
+ const deadline = this.#now() - TOOL_CHILD_LIMITS.refAckMs;
77
+ return [...this.#pending].filter(([, ref]) => ref.createdAtMs <= deadline).map(([refId]) => refId);
78
+ }
79
+ async teardown() {
80
+ this.#pending.clear();
81
+ this.#ownedBytes = 0;
82
+ await teardownRefGeneration(this.#root, this.generation);
83
+ }
84
+ }
85
+ const isMissingFile = (error) => error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
86
+ async function hashFile(path, signal) {
87
+ const hash = createHash("sha256");
88
+ for await (const chunk of createReadStream(path)) {
89
+ if (signal?.aborted)
90
+ return "cancelled";
91
+ hash.update(chunk);
92
+ }
93
+ return hash.digest("hex");
94
+ }
95
+ /**
96
+ * Receiver side. `accept` verifies size and sha256 and answers `ref_ack` (taking ownership) or `ref_nack`.
97
+ * The owner deletes the file when it consumes it; a dead owner's files go with its generation's teardown.
98
+ */
99
+ export class PayloadRefReader {
100
+ #root;
101
+ #generation;
102
+ #budget;
103
+ #send;
104
+ #owned = new Map();
105
+ #ownedBytes = 0;
106
+ constructor(options) {
107
+ this.#root = options.root;
108
+ this.#generation = options.generation;
109
+ this.#budget = options.budget ?? TOOL_CHILD_LIMITS.refsBudget;
110
+ this.#send = options.send;
111
+ }
112
+ async accept(ref, signal) {
113
+ const reason = await this.#verify(ref, signal);
114
+ if (reason !== undefined) {
115
+ this.refuse(ref, reason);
116
+ return { ok: false, reason };
117
+ }
118
+ this.#owned.set(ref.refId, ref);
119
+ this.#ownedBytes += ref.bytes;
120
+ this.#send({ type: "ref_ack", refId: ref.refId });
121
+ return { ok: true };
122
+ }
123
+ /** Answers `ref_nack` for a ref this side will not take, e.g. one whose job was cancelled before it was opened. */
124
+ refuse(ref, reason) {
125
+ if (this.#owned.has(ref.refId))
126
+ return;
127
+ this.#send({ type: "ref_nack", refId: ref.refId, reason });
128
+ }
129
+ async consume(refId) {
130
+ const ref = this.#owned.get(refId);
131
+ if (ref === undefined)
132
+ throw new PayloadRefError("ref_not_owned", `ref ${refId} is not owned by this side`);
133
+ const path = refFilePath(this.#root, ref);
134
+ const bytes = await readFile(path);
135
+ await rm(path, { force: true });
136
+ this.#owned.delete(refId);
137
+ this.#ownedBytes -= ref.bytes;
138
+ return bytes;
139
+ }
140
+ ownedIds() {
141
+ return [...this.#owned.keys()];
142
+ }
143
+ async #verify(ref, signal) {
144
+ if (signal?.aborted)
145
+ return "cancelled";
146
+ if (ref.generation !== this.#generation || !isRefId(ref.refId) || this.#owned.has(ref.refId))
147
+ return "foreign";
148
+ if (this.#ownedBytes + ref.bytes > this.#budget)
149
+ return "budget";
150
+ const path = refFilePath(this.#root, ref);
151
+ try {
152
+ if ((await stat(path)).size !== ref.bytes)
153
+ return "corrupt";
154
+ const digest = await hashFile(path, signal);
155
+ if (digest === "cancelled")
156
+ return "cancelled";
157
+ return digest === ref.sha256 ? undefined : "corrupt";
158
+ }
159
+ catch (error) {
160
+ if (isMissingFile(error))
161
+ return "missing";
162
+ throw error;
163
+ }
164
+ }
165
+ }
166
+ //# sourceMappingURL=payload-ref.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=payload-ref.test.d.ts.map