@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
@@ -8,7 +8,7 @@ import { HostEnsureRefusedError } from "./host-decision.js";
8
8
  import { probeSocketReachable } from "./host-probe.js";
9
9
  import { hostChildAlive, hostLoopStalled, stallRefusalWindowMs } from "./host-stalled-evidence.js";
10
10
  import { loopLagErrorMs } from "./loop-lag-threshold.js";
11
- import { statSocketIdentity } from "./socket-ownership.js";
11
+ import { socketEntryListenerState, statSocketIdentity } from "./socket-ownership.js";
12
12
  /**
13
13
  * IS-5: a generation nothing can reach whose loop is measurably stalled is still serving its sessions, so
14
14
  * it is neither stopped nor stranded beside a new generation: no signal, no successor, registration intact.
@@ -47,4 +47,21 @@ export async function publicEndpointAccepts(socket) {
47
47
  return true;
48
48
  return probeSocketReachable(socket, FOREIGN_ENDPOINT_PROBE_TIMEOUT_MS);
49
49
  }
50
+ /**
51
+ * Whether something stands behind a public path that no registration names: the gate before an
52
+ * ensure would start a host there. Unlike `publicEndpointAccepts` it also counts a listener that
53
+ * refuses only because its backlog is full (`socketEntryListenerState`), because a start there
54
+ * could only lose the bind; the registered paths keep `publicEndpointAccepts`, whose "not
55
+ * accepting" leads to the stall check (IS-5).
56
+ */
57
+ export async function unregisteredEndpointHeld(socket) {
58
+ if (process.platform === "win32" || socket.startsWith("\0"))
59
+ return false;
60
+ const entry = await statSocketIdentity(socket).then((identity) => (identity === undefined ? "absent" : "present"), () => "unknown");
61
+ if (entry === "absent")
62
+ return false;
63
+ if (entry === "unknown")
64
+ return true;
65
+ return (await socketEntryListenerState(socket)).verdict === "live";
66
+ }
50
67
  //# sourceMappingURL=host-ensure-liveness.js.map
@@ -26,6 +26,8 @@ export interface EnsureHostOptions {
26
26
  readonly stoppingTimeoutMs?: number;
27
27
  readonly _test?: {
28
28
  readonly readinessTimeoutMs?: number;
29
+ /** How long the existing-host probe waits for get_protocol_info; tests shorten it. */
30
+ readonly existingHostProbeTimeoutMs?: number;
29
31
  readonly stopTimeoutMs?: number;
30
32
  readonly spawn?: {
31
33
  readonly command: string;
@@ -7,7 +7,7 @@ import { clearHostRegistration, readHostRegistration, writtenByThisProcess, } fr
7
7
  import { claimHostOwner } from "./host-daemon-state.js";
8
8
  import { decideHostAction, HostEnsureRefusedError } from "./host-decision.js";
9
9
  import { ensureClient } from "./host-ensure-client.js";
10
- import { publicEndpointAccepts, refuseIfStalled } from "./host-ensure-liveness.js";
10
+ import { publicEndpointAccepts, refuseIfStalled, unregisteredEndpointHeld } from "./host-ensure-liveness.js";
11
11
  import { hostEnsureLockOptions, hostEnsureLockTarget } from "./host-ensure-lock.js";
12
12
  import { appendStderr, DEFAULT_READINESS_TIMEOUT_MS, DEFAULT_STOP_TIMEOUT_MS, startHost } from "./host-ensure-start.js";
13
13
  import { delay, ensureSender, matchesPidFileOrUnknown, resolvePidFileOwnership, STOP_WAIT_BUDGET_MS, stopManagedHost, } from "./host-ensure-stop.js";
@@ -82,7 +82,7 @@ async function ensureHostLocked(paths, socket, options, stopDeadline = Date.now(
82
82
  (await hostChildAlive(generationPaths(paths, registered.instanceId))))
83
83
  throw new HostEnsureRefusedError(socket, "host_stalled", undefined);
84
84
  // A reusable host is held from the connection that proved it compatible, never re-probed later.
85
- const held = await holdProtocolInfo(socket, EXISTING_HOST_PROBE_TIMEOUT_MS);
85
+ const held = await holdProtocolInfo(socket, testOptions?.existingHostProbeTimeoutMs ?? EXISTING_HOST_PROBE_TIMEOUT_MS);
86
86
  const protocol = held?.info;
87
87
  if (protocol?.lifecycle === "stopping") {
88
88
  held?.hold.release();
@@ -184,6 +184,12 @@ async function ensureHostLocked(paths, socket, options, stopDeadline = Date.now(
184
184
  throw new HostEnsureRefusedError(socket, "legacy_host", protocol, legacyRefusal);
185
185
  if (stranded !== undefined)
186
186
  return startHost(paths, socket, options, stranded.generation + 1);
187
+ // Something no registration names still ACCEPTS at the public path and did not answer the probe.
188
+ // A start can only lose the bind ("address already in use") and exit without a reason, and the
189
+ // holder is not ours to remove (I1): refuse with the reason instead. A stale entry nobody listens
190
+ // behind reads as free here, so the start below still replaces it.
191
+ if (await unregisteredEndpointHeld(socket))
192
+ throw new HostEnsureRefusedError(socket, "endpoint_occupied", protocol, "no host registration names this socket and its listener did not answer get_protocol_info");
187
193
  if (registeredHere)
188
194
  await clearHostRegistration(paths);
189
195
  return startHost(paths, socket, options);
@@ -1,7 +1,7 @@
1
1
  import { type Server } from "node:net";
2
2
  import type { HostAdmission } from "./host-admission.ts";
3
3
  import type { ClientOccupancy } from "./host-client-occupancy.ts";
4
- import { type SocketFileIdentity } from "./socket-ownership.ts";
4
+ import { type ListenerState, type SocketFileIdentity } from "./socket-ownership.ts";
5
5
  export interface PublicProxyOptions {
6
6
  readonly internalSocket: string;
7
7
  readonly internalSecret?: Buffer;
@@ -32,7 +32,14 @@ export declare function adoptPublicSocket(bindSocket: string, publicSocket: stri
32
32
  */
33
33
  export declare function ensurePublicSocketSecret(publicSocket: string): Promise<Buffer>;
34
34
  export declare function waitForListener(socketPath: string, timeoutMs: number, secret?: Uint8Array): Promise<void>;
35
- export declare function prepareSocketPath(socketPath: string): Promise<void>;
35
+ /** Test-only seams for `prepareSocketPath`'s stale-entry check. */
36
+ export interface PrepareSocketPathHooks {
37
+ /** Runs after the entry was probed dead and before it is unlinked. */
38
+ readonly afterStaleProbe?: () => Promise<void>;
39
+ /** Replaces the listener probe, so a test can pin each verdict. */
40
+ readonly listenerState?: (socketPath: string) => Promise<ListenerState>;
41
+ }
42
+ export declare function prepareSocketPath(socketPath: string, hooks?: PrepareSocketPathHooks): Promise<void>;
36
43
  export declare function listen(server: Server, socketPath: string, secret?: Uint8Array): Promise<void>;
37
44
  export declare function closeServer(server: Server): Promise<void>;
38
45
  //# sourceMappingURL=host-lifecycle-proxy.d.ts.map
@@ -3,11 +3,11 @@
3
3
  * adoption of the public name, and the readiness probes of the private hop. Split out of
4
4
  * `host-lifecycle.ts`, which keeps the supervisor's orchestration (senpi#2566).
5
5
  */
6
- import { access, chmod, mkdir, rename, unlink } from "node:fs/promises";
6
+ import { chmod, mkdir, rename, stat, unlink } from "node:fs/promises";
7
7
  import { createConnection, createServer } from "node:net";
8
8
  import { dirname } from "node:path";
9
9
  import { proxyWithAdmission } from "./host-admission-proxy.js";
10
- import { MAX_SOCKET_PATH_BYTES, statSocketIdentity } from "./socket-ownership.js";
10
+ import { MAX_SOCKET_PATH_BYTES, socketEntryListenerState, statSocketIdentity, } from "./socket-ownership.js";
11
11
  import { authenticateSocket, ensureSocketSecret, resolveSocketTransportAddress, sendSocketHandshake, socketSecretPath, } from "./socket-transport.js";
12
12
  export function createPublicProxy(options) {
13
13
  const { internalSocket, internalSecret, publicSecret, clients } = options;
@@ -100,8 +100,20 @@ function canConnect(socketPath, secret) {
100
100
  socket.once("error", () => settle(false));
101
101
  });
102
102
  }
103
- export async function prepareSocketPath(socketPath) {
104
- if (process.platform === "win32")
103
+ async function stampOf(socketPath) {
104
+ try {
105
+ const entry = await stat(socketPath, { bigint: true });
106
+ return { dev: entry.dev, ino: entry.ino, ctimeNs: entry.ctimeNs, socket: entry.isSocket() };
107
+ }
108
+ catch (cause) {
109
+ if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
110
+ return undefined;
111
+ throw cause;
112
+ }
113
+ }
114
+ const sameStamp = (a, b) => a.dev === b.dev && a.ino === b.ino && a.ctimeNs === b.ctimeNs;
115
+ export async function prepareSocketPath(socketPath, hooks = {}) {
116
+ if (process.platform === "win32" || socketPath.startsWith("\0"))
105
117
  return;
106
118
  // A path the kernel would truncate binds a DIFFERENT endpoint than the one every client was
107
119
  // told about, and the failure surfaces much later as "the host does not answer". Refuse here.
@@ -109,17 +121,33 @@ export async function prepareSocketPath(socketPath) {
109
121
  throw new Error(`${socketPath}: socket path is ${Buffer.byteLength(socketPath)} bytes, over the ${MAX_SOCKET_PATH_BYTES}-byte limit.`);
110
122
  }
111
123
  await mkdir(dirname(socketPath), { recursive: true, mode: 0o700 });
124
+ const probed = await stampOf(socketPath);
125
+ if (probed === undefined)
126
+ return;
127
+ const inUse = (evidence) => new Error(`${socketPath}: address already in use by a live server (${evidence}).`);
128
+ // Linux refuses a connect to a regular file too; never remove what is not a socket.
129
+ if (!probed.socket)
130
+ throw inUse("the entry is not a socket");
131
+ const state = await (hooks.listenerState ?? socketEntryListenerState)(socketPath);
132
+ if (state.verdict === "gone")
133
+ return;
134
+ if (state.verdict === "live")
135
+ throw inUse(state.evidence);
136
+ await hooks.afterStaleProbe?.();
137
+ // Something may have replaced the entry since the probe; unlink only the entry probed dead.
138
+ const current = await stampOf(socketPath);
139
+ if (current === undefined)
140
+ return;
141
+ if (!sameStamp(probed, current))
142
+ throw inUse("the entry changed after it was probed");
112
143
  try {
113
- await access(socketPath);
144
+ await unlink(socketPath);
114
145
  }
115
146
  catch (cause) {
116
147
  if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
117
148
  return;
118
149
  throw cause;
119
150
  }
120
- if (await canConnect(socketPath))
121
- throw new Error(`${socketPath}: address already in use by a live server.`);
122
- await unlink(socketPath);
123
151
  }
124
152
  export function listen(server, socketPath, secret) {
125
153
  return new Promise((resolve, reject) => {
@@ -31,6 +31,7 @@ const OBSERVING_COMMANDS = new Set([
31
31
  "subscribe_sessions",
32
32
  "unsubscribe_sessions",
33
33
  "get_effect_status",
34
+ "get_pending_inputs",
34
35
  "get_subscription_limits",
35
36
  ]);
36
37
  export function isObservingRequest(line) {
@@ -1,5 +1,5 @@
1
- import { access, chmod, mkdir, unlink } from "node:fs/promises";
2
- import { createConnection, createServer } from "node:net";
1
+ import { chmod } from "node:fs/promises";
2
+ import { createServer } from "node:net";
3
3
  import { basename, dirname, join } from "node:path";
4
4
  import { envValue } from "../../core/brand.js";
5
5
  import { HostMcpRegistry } from "../../core/extensions/builtin/mcp/host-registry.js";
@@ -16,6 +16,7 @@ import { performIdleHandover } from "./host-handover-wire.js";
16
16
  import { EXPECTED_RUNTIME_BUILD_ID_ENV } from "./host-idle-handover.js";
17
17
  import { RPC_CLOSE_GRACE_MS_ENV, resolveHostIdlePolicy } from "./host-idle-policy.js";
18
18
  import { parseIdleExitMs } from "./host-lifecycle.js";
19
+ import { prepareSocketPath } from "./host-lifecycle-proxy.js";
19
20
  import { createZeroSessionTrim, startHostObservers } from "./host-observers.js";
20
21
  import { runAsHostGenerationProcess } from "./host-process-role.js";
21
22
  import { createEndpointReservations } from "./host-reservations.js";
@@ -539,34 +540,6 @@ function resolveSocketPath(value, agentDir) {
539
540
  function formatSocketAddress(socketPath) {
540
541
  return socketPath.startsWith("\0") ? `unix://@${socketPath.slice(1)}` : `unix://${socketPath}`;
541
542
  }
542
- async function prepareSocketPath(socketPath) {
543
- if (process.platform === "win32" || socketPath.startsWith("\0"))
544
- return;
545
- await mkdir(dirname(socketPath), { recursive: true, mode: 0o700 });
546
- try {
547
- await access(socketPath);
548
- }
549
- catch (cause) {
550
- if (isNodeErrorCode(cause, "ENOENT"))
551
- return;
552
- throw cause;
553
- }
554
- if (await probeSocket(socketPath))
555
- throw new Error(`${socketPath}: address already in use by a live server.`);
556
- await unlink(socketPath);
557
- }
558
- function probeSocket(socketPath) {
559
- return new Promise((resolve) => {
560
- const socket = createConnection(resolveSocketTransportAddress(socketPath, process.platform));
561
- const settle = (live) => {
562
- socket.destroy();
563
- resolve(live);
564
- };
565
- socket.once("connect", () => settle(true));
566
- socket.once("error", () => settle(false));
567
- socket.setTimeout(1_000, () => settle(false));
568
- });
569
- }
570
543
  function listen(server, socketPath, secret) {
571
544
  return new Promise((resolve, reject) => {
572
545
  server.once("error", reject);
@@ -595,9 +568,6 @@ function closeServer(server) {
595
568
  function hostLog(message) {
596
569
  process.stderr.write(`senpi rpc host: ${message}\n`);
597
570
  }
598
- function isNodeErrorCode(cause, code) {
599
- return cause instanceof Error && "code" in cause && cause.code === code;
600
- }
601
571
  function registerShutdownSignals(shutdown) {
602
572
  for (const signal of process.platform === "win32" ? ["SIGTERM"] : ["SIGTERM", "SIGHUP"]) {
603
573
  process.on(signal, () => {
@@ -86,6 +86,11 @@ export type RpcEffectStatusAnswer = Extract<RpcResponse, {
86
86
  command: "get_effect_status";
87
87
  success: true;
88
88
  }>["data"];
89
+ /** `get_pending_inputs`: pending inputs in durable custody, and an outcome for each known id. */
90
+ export type RpcPendingInputsAnswer = Extract<RpcResponse, {
91
+ command: "get_pending_inputs";
92
+ success: true;
93
+ }>["data"];
89
94
  export declare class RpcClientOpenInFlightError extends Error {
90
95
  readonly code: "open_session_in_flight";
91
96
  constructor();
@@ -251,6 +256,11 @@ export declare class RpcClient {
251
256
  getEffectStatus(effectId: string, options?: {
252
257
  readonly sessionPath?: string;
253
258
  }): Promise<RpcEffectStatusAnswer>;
259
+ /**
260
+ * The pending inputs in durable custody (queued, sending, failed) and an explicit outcome for each id in
261
+ * `knownClientMessageIds` (at most 64), consistent through `throughCursor`.
262
+ */
263
+ getPendingInputs(knownClientMessageIds?: readonly string[]): Promise<RpcPendingInputsAnswer>;
254
264
  /** Abort with host refusals and transport loss surfaced to machine callers. */
255
265
  abortStrict(): Promise<void>;
256
266
  abortCompaction(): Promise<void>;
@@ -480,6 +480,17 @@ export class RpcClient {
480
480
  };
481
481
  return this.getData(await this.send(command, options.sessionPath === undefined));
482
482
  }
483
+ /**
484
+ * The pending inputs in durable custody (queued, sending, failed) and an explicit outcome for each id in
485
+ * `knownClientMessageIds` (at most 64), consistent through `throughCursor`.
486
+ */
487
+ async getPendingInputs(knownClientMessageIds) {
488
+ const command = {
489
+ type: "get_pending_inputs",
490
+ ...(knownClientMessageIds === undefined ? {} : { knownClientMessageIds: [...knownClientMessageIds] }),
491
+ };
492
+ return this.getData(await this.send(command));
493
+ }
483
494
  /** Abort with host refusals and transport loss surfaced to machine callers. */
484
495
  async abortStrict() {
485
496
  this.getData(await this.send({ type: "abort" }));
@@ -119,6 +119,14 @@ export function rpcCommandPayloadError(command) {
119
119
  !(typeof value.effectId === "string" && value.effectId.length > 0 && value.effectId.length <= 256)) {
120
120
  return "effectId must be a non-empty string of at most 256 characters.";
121
121
  }
122
+ if (value.type === "get_pending_inputs" && value.knownClientMessageIds !== undefined) {
123
+ const ids = value.knownClientMessageIds;
124
+ if (!Array.isArray(ids) ||
125
+ ids.length > 64 ||
126
+ !ids.every((id) => typeof id === "string" && id.length > 0 && id.length <= 256)) {
127
+ return "knownClientMessageIds must be an array of at most 64 non-empty strings of at most 256 characters.";
128
+ }
129
+ }
122
130
  if (value.type === "get_effect_status" && value.sessionPath !== undefined && typeof value.sessionPath !== "string") {
123
131
  return "sessionPath must be a string.";
124
132
  }
@@ -19,6 +19,7 @@ import type { EngineOrdinal } from "../../core/engine-build-identity.ts";
19
19
  import type { ServiceTier } from "../../core/extensions/builtin/service-tier.ts";
20
20
  import type { ContextUsage, SessionControlAdmission, SessionKind } from "../../core/extensions/types.ts";
21
21
  import type { ResolvedMessagePath } from "../../core/message-path-resolution.ts";
22
+ import type { PendingInputSnapshot, PendingInputUpdate } from "../../core/pending-input-session.ts";
22
23
  import type { SessionEntry, SessionMessageEntry, SessionTreeNode, UsageTotals } from "../../core/session-manager.ts";
23
24
  import type { SourceInfo } from "../../core/source-info.ts";
24
25
  import type { ApiKeyVerification } from "../../core/verify-api-key.ts";
@@ -165,6 +166,16 @@ type RpcSessionCommand = {
165
166
  type: "get_effect_status";
166
167
  effectId: string;
167
168
  sessionPath?: string;
169
+ }
170
+ /**
171
+ * The pending inputs in durable custody (queued, sending, failed) and an explicit outcome for each listed id,
172
+ * consistent through `throughCursor` (senpi#3549). Subscribe to `pending_input_update` first, then apply this
173
+ * snapshot and only updates after its cursor. At most 64 ids.
174
+ */
175
+ | {
176
+ id?: string;
177
+ type: "get_pending_inputs";
178
+ knownClientMessageIds?: string[];
168
179
  } | {
169
180
  id?: string;
170
181
  type: "set_model";
@@ -689,7 +700,10 @@ export type RpcCommand = (RpcSessionCommand & {
689
700
  */
690
701
  type: "release_session";
691
702
  sessionId: string;
692
- reason: "takeover";
703
+ /** `move` seals the file for a move to another device (`moveId` required); writes are refused here after. */
704
+ reason: "takeover" | "move";
705
+ moveId?: string;
706
+ target?: string;
693
707
  interrupt?: boolean;
694
708
  force?: boolean;
695
709
  } | {
@@ -1202,6 +1216,12 @@ export type RpcResponse = {
1202
1216
  found: false;
1203
1217
  searched: "complete";
1204
1218
  };
1219
+ } | {
1220
+ id?: string;
1221
+ type: "response";
1222
+ command: "get_pending_inputs";
1223
+ success: true;
1224
+ data: PendingInputSnapshot;
1205
1225
  } | {
1206
1226
  id?: string;
1207
1227
  type: "response";
@@ -1824,6 +1844,10 @@ export interface RpcHighReasoningWarningEvent {
1824
1844
  provider: string;
1825
1845
  thinkingLevel: ThinkingLevel;
1826
1846
  }
1847
+ /** A pending input in durable custody changed; `changes` are complete projections, in cursor order. */
1848
+ export type RpcPendingInputUpdateEvent = {
1849
+ type: "pending_input_update";
1850
+ } & PendingInputUpdate;
1827
1851
  /** Emitted after explicit skill tokens are expanded for a user-authored request. */
1828
1852
  export interface RpcSkillInvocationEvent {
1829
1853
  type: "skill_invocation";
@@ -2,6 +2,7 @@
2
2
  const observations = new Set([
3
3
  "get_state",
4
4
  "get_effect_status",
5
+ "get_pending_inputs",
5
6
  "get_messages",
6
7
  "get_entries",
7
8
  "get_tree",
@@ -6,15 +6,24 @@
6
6
  import { modelSupportsAssistantPrefill } from "@earendil-works/pi-ai";
7
7
  import { getSupportedThinkingLevels } from "../../core/thinking-levels.js";
8
8
  export async function availableModelsData(session) {
9
- const models = await session.modelRegistry.modelRuntime.getAvailable();
9
+ const registry = session.modelRegistry;
10
+ const models = await registry.modelRuntime.getAvailable();
10
11
  return {
11
- models: models.map((model) => ({
12
- ...model,
13
- supportedThinkingLevels: getSupportedThinkingLevels(model),
14
- supportsAssistantPrefill: modelSupportsAssistantPrefill(model, {
15
- thinkingEnabled: session.thinkingLevel !== "off",
16
- }),
17
- })),
12
+ models: models.map((model) => {
13
+ // A models.json or extension definition can declare the priority pairing without putting it on
14
+ // the Model object; clients must see the values the session itself acts on (#3666).
15
+ const serviceTier = registry.getServiceTier(model);
16
+ const upstreamModelId = registry.getUpstreamModelId(model);
17
+ return {
18
+ ...model,
19
+ ...(serviceTier === undefined ? {} : { serviceTier }),
20
+ ...(upstreamModelId === undefined ? {} : { upstreamModelId }),
21
+ supportedThinkingLevels: getSupportedThinkingLevels(model),
22
+ supportsAssistantPrefill: modelSupportsAssistantPrefill(model, {
23
+ thinkingEnabled: session.thinkingLevel !== "off",
24
+ }),
25
+ };
26
+ }),
18
27
  };
19
28
  }
20
29
  /** The refusal of a level the active model cannot run; callers match this text. */
@@ -9,6 +9,7 @@ import { RpcSessionRegistryError } from "./session-registry-types.js";
9
9
  const READ_ONLY_OR_CONTROL = new Set([
10
10
  "get_state",
11
11
  "get_effect_status",
12
+ "get_pending_inputs",
12
13
  "get_steering_messages",
13
14
  "get_follow_up_messages",
14
15
  "get_available_models",
@@ -1,3 +1,4 @@
1
+ import { SessionMovedAwayError } from "../../core/session-move/ownership.js";
1
2
  import { isHandoffBusy } from "./handoff-activity.js";
2
3
  import { RPC_ERROR_ATTACHED, RPC_ERROR_HOST_DRAINING, RPC_ERROR_INVALID_RELEASE_REASON, RPC_ERROR_RELEASE_FAILED, RPC_ERROR_RELEASE_UNSUPPORTED, RPC_ERROR_SESSION_BUSY, RPC_ERROR_SESSION_CLOSING, RPC_ERROR_TURN_ACTIVE, } from "./rpc-types.js";
3
4
  import { abortAndSettle, NOTHING_DROPPED, takeQueuedInput, } from "./session-release-interrupt.js";
@@ -8,7 +9,8 @@ export const RELEASED_ADMISSION_CLOSED = "release_session handed this session to
8
9
  /** `errorData.hint` of a refusal whose `busy` names `queued`: only an interrupt hands that input back. */
9
10
  export const RELEASE_QUEUED_HINT = "queued user input is still owed a turn; release with interrupt: true to take it out and receive it in dropped.user_messages";
10
11
  export async function releaseSession(port, command) {
11
- if (command.reason !== "takeover")
12
+ const validMove = command.reason === "move" && typeof command.moveId === "string" && command.moveId !== "";
13
+ if (command.reason !== "takeover" && !validMove)
12
14
  return refusal(command.id, RPC_ERROR_INVALID_RELEASE_REASON);
13
15
  if (port.draining())
14
16
  return refusal(command.id, RPC_ERROR_HOST_DRAINING);
@@ -30,6 +32,8 @@ export async function releaseSession(port, command) {
30
32
  admission.close(RELEASED_ADMISSION_CLOSED);
31
33
  dropped = takeQueuedInput(first.session);
32
34
  await abortAndSettle(port, command.sessionId, first.session);
35
+ // What the clear retired is on disk before `session_released` seals the file (senpi#3549).
36
+ await first.session.whenQueueClearRecorded();
33
37
  }
34
38
  // A never-written session gets its file here, before the final check: its `session_released` entry
35
39
  // must reach disk, and a header write that fails is this release's failure, never a stray rejection.
@@ -41,6 +45,10 @@ export async function releaseSession(port, command) {
41
45
  return answer;
42
46
  }
43
47
  catch (cause) {
48
+ if (cause instanceof SessionMovedAwayError) {
49
+ answer = refusal(command.id, cause.code, { detail: cause.message });
50
+ return answer;
51
+ }
44
52
  answer = refusal(command.id, RPC_ERROR_RELEASE_FAILED, {
45
53
  detail: cause instanceof Error ? cause.message : String(cause),
46
54
  ...(interrupted ? { interrupted: true, dropped } : {}),
@@ -78,6 +86,13 @@ async function claimAndRelease(port, command, interrupted, dropped) {
78
86
  // On disk or not at all: a failed write leaves no entry a later append could chain onto.
79
87
  ready.session.sessionManager.appendCustomEntry(SESSION_RELEASED_ENTRY_TYPE, {
80
88
  reason: command.reason,
89
+ ...(command.reason === "move"
90
+ ? {
91
+ move_id: command.moveId,
92
+ target: command.target ?? "",
93
+ session_id: ready.session.sessionManager.getSessionId(),
94
+ }
95
+ : {}),
81
96
  interrupted,
82
97
  attachments: ready.attachments,
83
98
  host_instance: port.hostInstance ?? null,
@@ -26,6 +26,23 @@ export declare const MAX_SOCKET_PATH_BYTES = 103;
26
26
  * public path: that path belongs to the running host until the successor is proven to work.
27
27
  */
28
28
  export declare function generationBindPath(publicSocket: string, generation: number): string;
29
+ /**
30
+ * What one failed connect says about the process behind a socket entry. Only a refusal can mean
31
+ * "nobody": on Linux a full backlog answers EAGAIN, so ECONNREFUSED is proof of absence there,
32
+ * but macOS (and the BSDs) refuse a live listener whose backlog is full, so a refusal there needs
33
+ * a second witness. ENOENT means the entry is gone; every other error is a live owner (I1).
34
+ */
35
+ export type ConnectVerdict = "live" | "dead" | "gone" | "confirm-holder";
36
+ export declare function classifyConnectError(code: string | undefined, platform: NodeJS.Platform): ConnectVerdict;
37
+ /** Whether any process holds the entry open: pids from `lsof -t`, none, or unknown. */
38
+ export type SocketHolders = (socketPath: string) => Promise<readonly number[] | undefined>;
39
+ export declare const lsofSocketHolders: SocketHolders;
40
+ /** Whether a live process stands behind a socket entry, and the evidence for the answer. */
41
+ export interface ListenerState {
42
+ readonly verdict: "live" | "dead" | "gone";
43
+ readonly evidence: string;
44
+ }
45
+ export declare function socketEntryListenerState(socketPath: string, platform?: NodeJS.Platform, holders?: SocketHolders): Promise<ListenerState>;
29
46
  /** Reads the current identity of a socket path entry; `undefined` once absent. */
30
47
  export declare function statSocketIdentity(socketPath: string): Promise<SocketFileIdentity | undefined>;
31
48
  /**
@@ -10,8 +10,11 @@
10
10
  * capture the bound entry's filesystem identity (dev + ino) at listen time and
11
11
  * unlink only while the path still stat-matches that identity.
12
12
  */
13
+ import { execFile } from "node:child_process";
13
14
  import { mkdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
15
+ import { createConnection } from "node:net";
14
16
  import { dirname } from "node:path";
17
+ import { resolveSocketTransportAddress } from "./socket-transport.js";
15
18
  /**
16
19
  * Name of the sidecar, inside the lifecycle supervisor's private scratch
17
20
  * directory, that records which public socket entry that supervisor bound. The
@@ -38,6 +41,55 @@ export function generationBindPath(publicSocket, generation) {
38
41
  function sameSocketIdentity(a, b) {
39
42
  return a.dev === b.dev && a.ino === b.ino;
40
43
  }
44
+ export function classifyConnectError(code, platform) {
45
+ if (code === "ENOENT")
46
+ return "gone";
47
+ if (code === "ECONNREFUSED")
48
+ return platform === "linux" ? "dead" : "confirm-holder";
49
+ return "live";
50
+ }
51
+ const HOLDER_PROBE_TIMEOUT_MS = 5_000;
52
+ export const lsofSocketHolders = (socketPath) => new Promise((resolve) => {
53
+ execFile("lsof", ["-t", "--", socketPath], { timeout: HOLDER_PROBE_TIMEOUT_MS }, (error, stdout) => {
54
+ const pids = String(stdout)
55
+ .split(/\s+/)
56
+ .filter((part) => /^\d+$/.test(part))
57
+ .map(Number);
58
+ if (pids.length > 0)
59
+ return resolve(pids);
60
+ // lsof exits 1 with no output when nothing holds the file; anything else proves nothing.
61
+ const exitCode = error === null ? undefined : Reflect.get(error, "code");
62
+ resolve(error === null || exitCode === 1 ? [] : undefined);
63
+ });
64
+ });
65
+ const CONNECT_PROBE_TIMEOUT_MS = 2_000;
66
+ /** One connect to the entry: `connected`, or the error code it failed with (`timeout` when it hung). */
67
+ function connectOnce(socketPath) {
68
+ return new Promise((resolve) => {
69
+ const socket = createConnection(resolveSocketTransportAddress(socketPath, process.platform));
70
+ const settle = (value) => {
71
+ socket.destroy();
72
+ resolve(value);
73
+ };
74
+ socket.setTimeout(CONNECT_PROBE_TIMEOUT_MS, () => settle({ code: "timeout" }));
75
+ socket.once("connect", () => settle({ connected: true }));
76
+ socket.once("error", (cause) => settle({ code: cause.code }));
77
+ });
78
+ }
79
+ export async function socketEntryListenerState(socketPath, platform = process.platform, holders = lsofSocketHolders) {
80
+ const attempt = await connectOnce(socketPath);
81
+ if ("connected" in attempt)
82
+ return { verdict: "live", evidence: "accepting connections" };
83
+ const verdict = classifyConnectError(attempt.code, platform);
84
+ if (verdict !== "confirm-holder")
85
+ return { verdict, evidence: `connect ${attempt.code ?? "failed"}` };
86
+ const pids = await holders(socketPath);
87
+ if (pids === undefined)
88
+ return { verdict: "live", evidence: "connect ECONNREFUSED, holders unknown" };
89
+ if (pids.length > 0)
90
+ return { verdict: "live", evidence: `connect ECONNREFUSED, held open by pid ${pids.join(",")}` };
91
+ return { verdict: "dead", evidence: "connect ECONNREFUSED, held by nobody" };
92
+ }
41
93
  /** Reads the current identity of a socket path entry; `undefined` once absent. */
42
94
  export async function statSocketIdentity(socketPath) {
43
95
  try {
@@ -0,0 +1,42 @@
1
+ import { type ToolChildMessageOf } from "./protocol.ts";
2
+ /**
3
+ * The one host-call capacity both sides enforce: `HOST_CALL_ACTIVE` running plus `HOST_CALL_WAITING` queued (264).
4
+ * The child admits against it before sending, the server admits against it on receipt; there is no second constant.
5
+ */
6
+ export declare function hostCallCapacity(): number;
7
+ export type HostCallRefusalCode = "host_call_capacity" | "host_call_duplicate";
8
+ export type HostCallDecision = {
9
+ state: "active";
10
+ } | {
11
+ state: "waiting";
12
+ } | {
13
+ state: "refused";
14
+ frame: ToolChildMessageOf<"host_call_refused">;
15
+ };
16
+ /** Lossless host-call admission: a call is running, queued FIFO, or refused with a frame naming its `callId`. */
17
+ export declare class HostCallAdmission {
18
+ #private;
19
+ get active(): number;
20
+ get waiting(): number;
21
+ submit(callId: string): HostCallDecision;
22
+ /** Ends a call (or withdraws a queued one) and returns the call promoted into the freed active slot, if any. */
23
+ settle(callId: string): string | undefined;
24
+ }
25
+ export type JobRefusalCode = "tool_child_jobs_full" | "tool_child_nested_full" | "tool_child_parent_unknown" | "tool_child_job_duplicate";
26
+ export type JobDecision = {
27
+ admitted: true;
28
+ } | {
29
+ admitted: false;
30
+ frame: ToolChildMessageOf<"job_refused">;
31
+ };
32
+ /**
33
+ * Per-child job budget. Ordinary jobs are capped at `TOOL_CHILD_LIMITS.jobs`. A nested job (started on behalf of a
34
+ * running job, e.g. a tool an eval cell calls) is admitted past that cap, so a full child cannot deadlock its own
35
+ * callers; nested jobs arrive through host calls and are bounded by `hostCallCapacity()`.
36
+ */
37
+ export declare class JobAdmission {
38
+ #private;
39
+ admit(jobId: string, parentJobId?: string): JobDecision;
40
+ finish(jobId: string): void;
41
+ }
42
+ //# sourceMappingURL=admission.d.ts.map