@code-yeongyu/senpi 2026.10.21-1 → 2026.10.21-2

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 (204) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/bundle/chunks/{anthropic-messages-S5BTP7U2.js → anthropic-messages-2IQD75YN.js} +1 -1
  3. package/dist/bundle/chunks/args-SV2KM4KA.js +2 -0
  4. package/dist/bundle/chunks/azure-openai-responses-HFI6SDH5.js +2 -0
  5. package/dist/bundle/chunks/{chunk-WQLMQGOW.js → chunk-2AJB5S4C.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-XSRM367N.js → chunk-3AK3WUN4.js} +2 -2
  7. package/dist/bundle/chunks/{chunk-6TL72TUD.js → chunk-4HJTPWZY.js} +1 -1
  8. package/dist/bundle/chunks/chunk-4NPBDGC4.js +26 -0
  9. package/dist/bundle/chunks/{chunk-E4WOCULF.js → chunk-4XR5HDWC.js} +72 -69
  10. package/dist/bundle/chunks/{chunk-6IWSMXBA.js → chunk-5GC4IQ3A.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-7I7ZMSBF.js → chunk-A4B4CMOR.js} +1 -1
  12. package/dist/bundle/chunks/chunk-B4N4N7BR.js +8 -0
  13. package/dist/bundle/chunks/{chunk-X23XXKMH.js → chunk-BVFJAXWO.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-25B6JQC4.js → chunk-C3BJRXEV.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-DKBJM22C.js → chunk-CQDU6S6C.js} +2 -2
  16. package/dist/bundle/chunks/{chunk-MF57DS3V.js → chunk-EV4TWAJI.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-JJXDWX46.js → chunk-F3JGTOLH.js} +1 -1
  18. package/dist/bundle/chunks/{chunk-CRY5CP3B.js → chunk-G42JHX5Q.js} +3 -3
  19. package/dist/bundle/chunks/{chunk-5VRI3CME.js → chunk-G7VYQRE7.js} +2 -2
  20. package/dist/bundle/chunks/chunk-GVSBCJFI.js +2 -0
  21. package/dist/bundle/chunks/{chunk-KUZPBZUK.js → chunk-HKDGQE6D.js} +2 -2
  22. package/dist/bundle/chunks/{chunk-HD67U37N.js → chunk-HRV7NEOJ.js} +1 -1
  23. package/dist/bundle/chunks/chunk-JDETIKL7.js +7 -0
  24. package/dist/bundle/chunks/{chunk-MVNVDGYF.js → chunk-JGMG227A.js} +1 -1
  25. package/dist/bundle/chunks/chunk-MHD7SSHF.js +2 -0
  26. package/dist/bundle/chunks/{chunk-CEQMTVYS.js → chunk-MXLEWTWC.js} +2 -2
  27. package/dist/bundle/chunks/{chunk-C5W3D56I.js → chunk-N6UN2SDS.js} +3 -3
  28. package/dist/bundle/chunks/chunk-NTK2MHJ5.js +2 -0
  29. package/dist/bundle/chunks/chunk-OMN4QWFA.js +3 -0
  30. package/dist/bundle/chunks/chunk-PLQ5WRXN.js +2 -0
  31. package/dist/bundle/chunks/{chunk-2MOGCBPT.js → chunk-PUMCQ5HS.js} +1 -1
  32. package/dist/bundle/chunks/{chunk-55YTTFNY.js → chunk-PUQFKAB4.js} +1 -1
  33. package/dist/bundle/chunks/{chunk-IJT3GSB2.js → chunk-SN7PTZTC.js} +1 -1
  34. package/dist/bundle/chunks/{chunk-FWHSGCWR.js → chunk-T2IIUI3J.js} +3 -3
  35. package/dist/bundle/chunks/{chunk-5AOKDC6G.js → chunk-TSETMVJP.js} +1 -1
  36. package/dist/bundle/chunks/{chunk-SR7DBNWN.js → chunk-V6AYNGMX.js} +1 -1
  37. package/dist/bundle/chunks/chunk-VDPJ7W52.js +11 -0
  38. package/dist/bundle/chunks/{chunk-EPQK674G.js → chunk-VLKULV6F.js} +1 -1
  39. package/dist/bundle/chunks/{chunk-SL7TSFR5.js → chunk-VXACHCLB.js} +1 -1
  40. package/dist/bundle/chunks/{chunk-FM7NDZR5.js → chunk-VYSRUDEU.js} +1 -1
  41. package/dist/bundle/chunks/chunk-YMFGBSO3.js +2 -0
  42. package/dist/bundle/chunks/cli-main-RA6EBOOU.js +3 -0
  43. package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-4QEQ7MGK.js → cloudflare-workers-ai-system-one-BVENEDT6.js} +1 -1
  44. package/dist/bundle/chunks/cursor-agent.js +3 -3
  45. package/dist/bundle/chunks/google-generative-ai-ONS2KSGN.js +2 -0
  46. package/dist/bundle/chunks/google-vertex-5VS7ZESH.js +2 -0
  47. package/dist/bundle/chunks/help-fast-path-FZY53UXB.js +2 -0
  48. package/dist/bundle/chunks/host-command-TNUIHEFP.js +2 -0
  49. package/dist/bundle/chunks/{host-lifecycle-EZQ5VCQV.js → host-lifecycle-LDOHAYLK.js} +1 -1
  50. package/dist/bundle/chunks/host-session-command-OKASUQPA.js +20 -0
  51. package/dist/bundle/chunks/interactive-mode-PSGTSURY.js +2 -0
  52. package/dist/bundle/chunks/{mistral-conversations-5EHLE7PZ.js → mistral-conversations-RLDMF2BI.js} +1 -1
  53. package/dist/bundle/chunks/{multi-session-host-OMJZ5VXM.js → multi-session-host-J5BVVUG3.js} +7 -6
  54. package/dist/bundle/chunks/openai-codex-responses-VRIJIZJG.js +10 -0
  55. package/dist/bundle/chunks/{openai-completions-NHDMDAS7.js → openai-completions-EZBOIXHP.js} +2 -2
  56. package/dist/bundle/chunks/{openai-images-PNXQDBSP.js → openai-images-KKM343F2.js} +1 -1
  57. package/dist/bundle/chunks/openai-responses-CMBH3DCY.js +3 -0
  58. package/dist/bundle/chunks/{package-manager-cli-7GQDB5DX.js → package-manager-cli-JHVRG35I.js} +1 -1
  59. package/dist/bundle/chunks/{role-WQWYL4ZD.js → role-LNF5MFGC.js} +1 -1
  60. package/dist/bundle/chunks/rotation-stream-JK3Q4IWI.js +2 -0
  61. package/dist/bundle/chunks/rpc-mode-HVSPPRTT.js +2 -0
  62. package/dist/bundle/chunks/{rpc-monitor-source-FI7H7LGY.js → rpc-monitor-source-2DQZLFFJ.js} +1 -1
  63. package/dist/bundle/chunks/{runtime-session-IQAKNRTV.js → runtime-session-CLNODAYE.js} +1 -1
  64. package/dist/bundle/chunks/{schedule-command-XXJC4QQN.js → schedule-command-OMI4VWUV.js} +1 -1
  65. package/dist/bundle/chunks/session-control-endpoint-UPYF4OFP.js +3 -0
  66. package/dist/bundle/chunks/session-picker-NR7CRVNQ.js +2 -0
  67. package/dist/bundle/chunks/session-worker.js +143 -139
  68. package/dist/bundle/chunks/{typesafe-system-one-W2ZTYF7X.js → typesafe-system-one-PJIWA5SP.js} +1 -1
  69. package/dist/bundle/cli.js +2 -2
  70. package/dist/bundle/index.js +1 -1
  71. package/dist/bundle/rpc-entry.js +1 -1
  72. package/dist/bundle/runtime-manifest.json +1 -1
  73. package/dist/cli/host-command.d.ts +2 -0
  74. package/dist/cli/host-command.js +14 -19
  75. package/dist/cli/host-handover-args.d.ts +4 -0
  76. package/dist/cli/host-handover-args.js +20 -0
  77. package/dist/core/agent-session.d.ts +29 -1
  78. package/dist/core/agent-session.js +74 -19
  79. package/dist/core/auth-storage.d.ts +44 -1
  80. package/dist/core/auth-storage.js +120 -3
  81. package/dist/core/client-message-identity.d.ts +3 -0
  82. package/dist/core/credential-accounts.d.ts +2 -0
  83. package/dist/core/credential-accounts.js +8 -1
  84. package/dist/core/credential-pool/rotation-slots.js +3 -2
  85. package/dist/core/extensions/builtin/account/index.js +6 -1
  86. package/dist/core/extensions/builtin/compaction/openai-remote.d.ts +2 -1
  87. package/dist/core/extensions/builtin/compaction/openai-remote.js +6 -5
  88. package/dist/core/extensions/builtin/goal/provider-retry.d.ts +2 -2
  89. package/dist/core/extensions/builtin/goal/provider-retry.js +18 -6
  90. package/dist/core/extensions/builtin/terminal/command-monitor-source.d.ts +17 -4
  91. package/dist/core/extensions/builtin/terminal/command-monitor-source.js +33 -6
  92. package/dist/core/extensions/builtin/terminal/durable-command.js +1 -0
  93. package/dist/core/extensions/builtin/terminal/extension.js +28 -0
  94. package/dist/core/extensions/builtin/terminal/monitor-notify-details.d.ts +6 -0
  95. package/dist/core/extensions/builtin/terminal/monitor-notify-details.js +19 -0
  96. package/dist/core/extensions/builtin/terminal/monitor-notify-format.d.ts +18 -0
  97. package/dist/core/extensions/builtin/terminal/monitor-notify-format.js +52 -0
  98. package/dist/core/extensions/builtin/terminal/monitor-notify.d.ts +8 -1
  99. package/dist/core/extensions/builtin/terminal/monitor-notify.js +65 -119
  100. package/dist/core/extensions/builtin/terminal/monitor-overflow-file.d.ts +7 -0
  101. package/dist/core/extensions/builtin/terminal/monitor-overflow-file.js +67 -0
  102. package/dist/core/extensions/builtin/terminal/monitor-overflow.d.ts +37 -0
  103. package/dist/core/extensions/builtin/terminal/monitor-overflow.js +75 -0
  104. package/dist/core/extensions/builtin/terminal/monitor-registry.d.ts +10 -0
  105. package/dist/core/extensions/builtin/terminal/monitor-registry.js +24 -1
  106. package/dist/core/extensions/builtin/terminal/monitor-types.d.ts +5 -0
  107. package/dist/core/extensions/builtin/terminal/monitor-wake-fit.d.ts +9 -0
  108. package/dist/core/extensions/builtin/terminal/monitor-wake-fit.js +34 -0
  109. package/dist/core/extensions/builtin/terminal/monitor-wake-pacing.d.ts +10 -0
  110. package/dist/core/extensions/builtin/terminal/monitor-wake-pacing.js +41 -0
  111. package/dist/core/extensions/builtin/terminal/restore-session.js +2 -0
  112. package/dist/core/extensions/builtin/terminal/rpc-monitor-observer.js +5 -1
  113. package/dist/core/extensions/builtin/terminal/settings.d.ts +10 -0
  114. package/dist/core/extensions/builtin/terminal/settings.js +13 -1
  115. package/dist/core/extensions/builtin/terminal/shared.d.ts +17 -10
  116. package/dist/core/extensions/builtin/terminal/shared.js +19 -10
  117. package/dist/core/extensions/builtin/terminal/terminal-manifest-model.d.ts +3 -0
  118. package/dist/core/extensions/builtin/terminal/terminal-manifest-parse.js +7 -0
  119. package/dist/core/extensions/builtin/terminal/terminal-manifest.js +1 -0
  120. package/dist/core/extensions/builtin/terminal/tools/monitor-schema.d.ts +1 -0
  121. package/dist/core/extensions/builtin/terminal/tools/monitor-schema.js +6 -1
  122. package/dist/core/extensions/builtin/terminal/tools/monitor.d.ts +1 -0
  123. package/dist/core/extensions/builtin/terminal/tools/monitor.js +9 -1
  124. package/dist/core/extensions/builtin/tps.js +1 -8
  125. package/dist/core/extensions/loader.js +3 -1
  126. package/dist/core/extensions/runner.d.ts +1 -0
  127. package/dist/core/extensions/runner.js +12 -1
  128. package/dist/core/extensions/types.d.ts +3 -1
  129. package/dist/core/session-manager.d.ts +10 -1
  130. package/dist/core/session-manager.js +4 -3
  131. package/dist/core/terminal-settings.d.ts +3 -0
  132. package/dist/index.d.ts +1 -1
  133. package/dist/main.d.ts +14 -0
  134. package/dist/main.js +25 -8
  135. package/dist/modes/index.d.ts +1 -0
  136. package/dist/modes/index.js +1 -0
  137. package/dist/modes/interactive/components/footer.js +4 -2
  138. package/dist/modes/interactive/interactive-mode.d.ts +5 -0
  139. package/dist/modes/interactive/interactive-mode.js +13 -1
  140. package/dist/modes/rpc/client-admission-record.d.ts +5 -0
  141. package/dist/modes/rpc/client-admission-record.js +2 -0
  142. package/dist/modes/rpc/connection-handler.d.ts +3 -0
  143. package/dist/modes/rpc/connection-handler.js +33 -5
  144. package/dist/modes/rpc/connection-roles.d.ts +30 -0
  145. package/dist/modes/rpc/connection-roles.js +53 -0
  146. package/dist/modes/rpc/custom-capability.d.ts +6 -0
  147. package/dist/modes/rpc/custom-capability.js +6 -0
  148. package/dist/modes/rpc/host-capabilities.js +2 -1
  149. package/dist/modes/rpc/host-decision.d.ts +4 -1
  150. package/dist/modes/rpc/host-decision.js +6 -3
  151. package/dist/modes/rpc/host-ensure-liveness.d.ts +11 -0
  152. package/dist/modes/rpc/host-ensure-liveness.js +26 -0
  153. package/dist/modes/rpc/host-ensure.js +2 -3
  154. package/dist/modes/rpc/host-holder.d.ts +27 -0
  155. package/dist/modes/rpc/host-holder.js +18 -0
  156. package/dist/modes/rpc/host-runner.d.ts +2 -0
  157. package/dist/modes/rpc/host-runner.js +8 -1
  158. package/dist/modes/rpc/host-status-all.js +1 -0
  159. package/dist/modes/rpc/host-status.d.ts +1 -0
  160. package/dist/modes/rpc/host-status.js +5 -0
  161. package/dist/modes/rpc/host-stop-generation.d.ts +22 -0
  162. package/dist/modes/rpc/host-stop-generation.js +36 -0
  163. package/dist/modes/rpc/host-stop-start-time.d.ts +15 -0
  164. package/dist/modes/rpc/host-stop-start-time.js +30 -0
  165. package/dist/modes/rpc/host-stop.d.ts +13 -1
  166. package/dist/modes/rpc/host-stop.js +78 -9
  167. package/dist/modes/rpc/rpc-client.d.ts +9 -0
  168. package/dist/modes/rpc/rpc-client.js +15 -0
  169. package/dist/modes/rpc/rpc-types.d.ts +18 -0
  170. package/dist/modes/rpc/rpc-types.js +2 -0
  171. package/dist/modes/rpc/session-binding.d.ts +1 -1
  172. package/dist/modes/rpc/session-command-router.d.ts +1 -0
  173. package/dist/modes/rpc/session-command-router.js +13 -0
  174. package/dist/modes/rpc/session-worker-client.d.ts +1 -1
  175. package/dist/modes/rpc/session-worker-client.js +12 -1
  176. package/dist/modes/rpc/session-worker-protocol.d.ts +3 -0
  177. package/dist/modes/rpc/session-worker.js +9 -0
  178. package/docs/rpc.md +54 -2
  179. package/docs/session-format.md +5 -0
  180. package/package.json +6 -6
  181. package/dist/bundle/chunks/args-CRXNC7IS.js +0 -2
  182. package/dist/bundle/chunks/azure-openai-responses-2DCY6QKO.js +0 -2
  183. package/dist/bundle/chunks/chunk-5ZBDN3LB.js +0 -26
  184. package/dist/bundle/chunks/chunk-AEJUOQ7U.js +0 -2
  185. package/dist/bundle/chunks/chunk-LAWS3FYQ.js +0 -11
  186. package/dist/bundle/chunks/chunk-LM76EMEJ.js +0 -7
  187. package/dist/bundle/chunks/chunk-M237EDHB.js +0 -2
  188. package/dist/bundle/chunks/chunk-V6RY5A6T.js +0 -2
  189. package/dist/bundle/chunks/chunk-WIAXG52U.js +0 -2
  190. package/dist/bundle/chunks/chunk-WTFD4TII.js +0 -8
  191. package/dist/bundle/chunks/chunk-Z33PKFXM.js +0 -3
  192. package/dist/bundle/chunks/cli-main-RKC5Z25H.js +0 -3
  193. package/dist/bundle/chunks/google-generative-ai-3SKUSCIL.js +0 -2
  194. package/dist/bundle/chunks/google-vertex-YHSIIE4G.js +0 -2
  195. package/dist/bundle/chunks/help-fast-path-FWS5ARO5.js +0 -2
  196. package/dist/bundle/chunks/host-command-L66Y7ZL5.js +0 -2
  197. package/dist/bundle/chunks/host-session-command-2YNQNTEH.js +0 -20
  198. package/dist/bundle/chunks/interactive-mode-XOKTNBLA.js +0 -2
  199. package/dist/bundle/chunks/openai-codex-responses-4BIPACKV.js +0 -10
  200. package/dist/bundle/chunks/openai-responses-2UOPEJLP.js +0 -3
  201. package/dist/bundle/chunks/rotation-stream-J2NUPRMG.js +0 -2
  202. package/dist/bundle/chunks/rpc-mode-WM5WH7TH.js +0 -2
  203. package/dist/bundle/chunks/session-control-endpoint-MTOAYRRY.js +0 -3
  204. package/dist/bundle/chunks/session-picker-LU7BPLNI.js +0 -2
@@ -24,6 +24,7 @@ export interface HostStatusReport {
24
24
  readonly lifecycle: "ready" | "stopping" | null;
25
25
  readonly socket: string;
26
26
  readonly pid: number | null;
27
+ readonly processStartTime: string | null;
27
28
  readonly instanceId: string | null;
28
29
  readonly generation: number | null;
29
30
  readonly engineVersion: string | null;
@@ -13,6 +13,7 @@
13
13
  import { isHostCrash, readHostCrashRecords } from "./host-crash-record.js";
14
14
  import { readDaemonEnvKeys } from "./host-daemon-env.js";
15
15
  import { createHostDaemonPaths, parseShardSocket } from "./host-daemon-paths.js";
16
+ import { readHostRegistration } from "./host-daemon-registration.js";
16
17
  import { pruneDeadGenerations, readGenerationRows } from "./host-generations.js";
17
18
  import { OBSERVE_REQUEST_FIELD } from "./host-observe-request.js";
18
19
  import { observeProtocolInfo, requestOnSocket } from "./host-probe.js";
@@ -44,6 +45,7 @@ export async function probeHostStatus(options, read = {}) {
44
45
  await pruneDeadGenerations(paths);
45
46
  const generations = await readGenerationRows(paths, { includeDead: !prune });
46
47
  const current = generations.find((row) => row.current && row.alive);
48
+ const registered = await readHostRegistration(paths);
47
49
  const metrics = current ? await readHostProcessMetrics(current.pid) : UNOBSERVED_METRICS;
48
50
  // A socket that did not answer who it is will not answer what it holds: asking again would only
49
51
  // spend a second budget on a hung endpoint.
@@ -56,6 +58,9 @@ export async function probeHostStatus(options, read = {}) {
56
58
  lifecycle: host?.lifecycle ?? null,
57
59
  socket: options.socket,
58
60
  pid: current?.pid ?? null,
61
+ processStartTime: registered?.instanceId === current?.instanceId && registered?.record.pid === current?.pid
62
+ ? (registered?.record.processStartTime ?? null)
63
+ : null,
59
64
  instanceId: host?.instanceId ?? current?.instanceId ?? null,
60
65
  generation: host?.generation ?? current?.generation ?? null,
61
66
  engineVersion: host?.engineVersion ?? current?.engineVersion ?? null,
@@ -0,0 +1,22 @@
1
+ import type { RegisteredHost } from "./host-daemon-registration.ts";
2
+ /**
3
+ * The registration the operator saw and authorized, never a newly resolved target. `processStartTime`
4
+ * is null where the platform cannot read it (Windows); the key itself is always present.
5
+ */
6
+ export interface HostStopGeneration {
7
+ readonly instanceId: string;
8
+ readonly generation: number;
9
+ readonly pid: number;
10
+ readonly processStartTime: string | null;
11
+ }
12
+ export declare function parseHostStopGeneration(text: string): HostStopGeneration | undefined;
13
+ /**
14
+ * How the current registration compares with the confirmed one:
15
+ * - `proven`: same generation, and the start times agree; the live process is still checked before signalling.
16
+ * - `bound_without_start_time`: same pid, instance and generation on a platform that records no start time.
17
+ * - `stale`: a different generation, pid or start time holds the endpoint now.
18
+ * - `unprovable`: same generation, but the start time cannot prove the process (`unknown_owner`).
19
+ */
20
+ export type StopGenerationMatch = "proven" | "bound_without_start_time" | "stale" | "unprovable";
21
+ export declare function compareStopGeneration(registered: RegisteredHost | undefined, expected: HostStopGeneration): StopGenerationMatch;
22
+ //# sourceMappingURL=host-stop-generation.d.ts.map
@@ -0,0 +1,36 @@
1
+ import { parseJson } from "./host-daemon-state.js";
2
+ import { processStartTimeIsUnreadable } from "./host-stop-start-time.js";
3
+ export function parseHostStopGeneration(text) {
4
+ const value = parseJson(text);
5
+ if (!value ||
6
+ typeof value.instanceId !== "string" ||
7
+ value.instanceId.length === 0 ||
8
+ typeof value.generation !== "number" ||
9
+ !Number.isSafeInteger(value.generation) ||
10
+ value.generation < 0 ||
11
+ typeof value.pid !== "number" ||
12
+ !Number.isSafeInteger(value.pid) ||
13
+ value.pid <= 0 ||
14
+ !(value.processStartTime === null ||
15
+ (typeof value.processStartTime === "string" && value.processStartTime.length > 0)))
16
+ return undefined;
17
+ return {
18
+ instanceId: value.instanceId,
19
+ generation: value.generation,
20
+ pid: value.pid,
21
+ processStartTime: value.processStartTime,
22
+ };
23
+ }
24
+ export function compareStopGeneration(registered, expected) {
25
+ if (registered?.instanceId !== expected.instanceId ||
26
+ registered.generation !== expected.generation ||
27
+ registered.record.pid !== expected.pid)
28
+ return "stale";
29
+ const recorded = registered.record.processStartTime;
30
+ if (recorded !== null && expected.processStartTime !== null)
31
+ return recorded === expected.processStartTime ? "proven" : "stale";
32
+ if (recorded === null && expected.processStartTime === null && processStartTimeIsUnreadable())
33
+ return "bound_without_start_time";
34
+ return "unprovable";
35
+ }
36
+ //# sourceMappingURL=host-stop-generation.js.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Whether this platform can record a process start time at registration. On Windows the CIM probe often
3
+ * outlives its 1 s budget while a host starts, so engine hosts record `processStartTime: null` (measured
4
+ * on a Windows runner) and a generation-bound stop cannot compare start times (omo-desktop-app#3561).
5
+ */
6
+ export declare function processStartTimeIsUnreadable(): boolean;
7
+ /** The live start time of `pid` now, or undefined when the process is gone. Throws when unreadable. */
8
+ export declare function readLiveStartTime(pid: number): Promise<string | undefined>;
9
+ /**
10
+ * Proves, without a recorded start time, that `pid` is still the process that wrote `registrationFile`:
11
+ * that process started before it wrote the file, so a live start time LATER than the file's mtime is a
12
+ * reused pid. Gone, unreadable or unparseable is not proven.
13
+ */
14
+ export declare function startedBeforeRegistration(pid: number, registrationFile: string): Promise<boolean>;
15
+ //# sourceMappingURL=host-stop-start-time.d.ts.map
@@ -0,0 +1,30 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { processStartTimeMs, readProcessStartTime } from "../../utils/process-identity.js";
3
+ /**
4
+ * Whether this platform can record a process start time at registration. On Windows the CIM probe often
5
+ * outlives its 1 s budget while a host starts, so engine hosts record `processStartTime: null` (measured
6
+ * on a Windows runner) and a generation-bound stop cannot compare start times (omo-desktop-app#3561).
7
+ */
8
+ export function processStartTimeIsUnreadable() {
9
+ return process.platform === "win32";
10
+ }
11
+ /** A stop is an operator action: give the start-time probe the budget a loaded Windows host needs. */
12
+ const STOP_START_TIME_BUDGET_MS = 10_000;
13
+ /** The live start time of `pid` now, or undefined when the process is gone. Throws when unreadable. */
14
+ export function readLiveStartTime(pid) {
15
+ return readProcessStartTime(pid, process.platform, STOP_START_TIME_BUDGET_MS);
16
+ }
17
+ /**
18
+ * Proves, without a recorded start time, that `pid` is still the process that wrote `registrationFile`:
19
+ * that process started before it wrote the file, so a live start time LATER than the file's mtime is a
20
+ * reused pid. Gone, unreadable or unparseable is not proven.
21
+ */
22
+ export async function startedBeforeRegistration(pid, registrationFile) {
23
+ const live = await readLiveStartTime(pid).catch(() => undefined);
24
+ const startedMs = live === undefined ? undefined : processStartTimeMs(live);
25
+ if (startedMs === undefined)
26
+ return false;
27
+ const written = await stat(registrationFile).catch(() => undefined);
28
+ return written !== undefined && startedMs <= written.mtimeMs;
29
+ }
30
+ //# sourceMappingURL=host-stop-start-time.js.map
@@ -19,6 +19,7 @@
19
19
  * error to a caller that asked for exactly that outcome.
20
20
  */
21
21
  import { type DrainHostResult } from "./host-stop-drain.ts";
22
+ import { type HostStopGeneration } from "./host-stop-generation.ts";
22
23
  export interface StopHostOptions {
23
24
  readonly socket: string;
24
25
  readonly agentDir?: string;
@@ -33,13 +34,15 @@ export interface StopHostOptions {
33
34
  readonly timeoutMs?: number;
34
35
  /** Refuse unless the host answers both reads: a caller that judged it idle must not stop it unseen. */
35
36
  readonly requireAnswer?: boolean;
37
+ /** Refuse rather than signal a generation other than the one the operator confirmed. */
38
+ readonly expectedGeneration?: HostStopGeneration;
36
39
  }
37
40
  export type StopHostResult = DrainHostResult | {
38
41
  readonly action: "stopped";
39
42
  readonly pid: number;
40
43
  } | {
41
44
  readonly action: "refuse";
42
- readonly reason: "unknown_owner" | "drain_unsupported" | "sessions_live";
45
+ readonly reason: "unknown_owner" | "drain_unsupported" | "sessions_live" | "stale_generation";
43
46
  };
44
47
  /**
45
48
  * Ends a running generation. `drain` is always permitted - it ends no work, it only stops the
@@ -47,6 +50,14 @@ export type StopHostResult = DrainHostResult | {
47
50
  * operator never silently kills another client's sessions.
48
51
  */
49
52
  export declare function stopHost(options: StopHostOptions): Promise<StopHostResult>;
53
+ type StopRefusal = Extract<StopHostResult, {
54
+ action: "refuse";
55
+ }>;
56
+ /**
57
+ * Refuses a binding the endpoint no longer matches, before any probe or census (desktop#3561 review): a
58
+ * caller learns its confirmation is stale rather than that the host it no longer names is busy.
59
+ */
60
+ export declare function staleStopGeneration(socket: string, agentDir: string | undefined, expected: HostStopGeneration): Promise<StopRefusal | undefined>;
50
61
  /**
51
62
  * Signals a generation whose owner was just proven, and reports whether the signal was delivered.
52
63
  *
@@ -56,4 +67,5 @@ export declare function stopHost(options: StopHostOptions): Promise<StopHostResu
56
67
  * the request. Any OTHER signalling failure (EPERM against somebody else's process) still throws.
57
68
  */
58
69
  export declare function signalGeneration(pid: number, signal: NodeJS.Signals): boolean;
70
+ export {};
59
71
  //# sourceMappingURL=host-stop.d.ts.map
@@ -18,21 +18,46 @@
18
18
  * failure ESRCH means "already gone", not "something went wrong", and must never surface as a raw
19
19
  * error to a caller that asked for exactly that outcome.
20
20
  */
21
- import { createHostDaemonPaths, generationPaths } from "./host-daemon-paths.js";
21
+ import { createHostDaemonPaths, generationPaths, sameEndpoint } from "./host-daemon-paths.js";
22
22
  import { provenOwner, readHostRegistration, releaseGeneration } from "./host-daemon-registration.js";
23
+ import { acquireHostEnsureLock } from "./host-ensure-lock.js";
23
24
  import { probeProtocolInfo, probeSessionCount } from "./host-probe.js";
24
25
  import { drainHost } from "./host-stop-drain.js";
26
+ import { compareStopGeneration } from "./host-stop-generation.js";
25
27
  import { writeStopIntent } from "./host-stop-intent.js";
28
+ import { startedBeforeRegistration } from "./host-stop-start-time.js";
26
29
  /**
27
30
  * Ends a running generation. `drain` is always permitted - it ends no work, it only stops the
28
31
  * host from taking new work - while a hard stop requires either an empty host or `force`, so an
29
32
  * operator never silently kills another client's sessions.
30
33
  */
31
34
  export async function stopHost(options) {
35
+ // A binding names the generation a HARD stop may signal; a drain targets its own instance and must not hold the lock.
36
+ if (options.expectedGeneration !== undefined && options.drain === true)
37
+ throw new TypeError("stopHost: expectedGeneration binds a hard stop and cannot be combined with drain");
38
+ // GC already holds this lock and uses an unqualified stop. Conditional operator stops acquire it
39
+ // here so ensure/handoff cannot publish a different generation between the check and signal.
40
+ if (options.expectedGeneration === undefined)
41
+ return stopHostLocked(options);
42
+ const release = await acquireHostEnsureLock(options.socket, 30_000);
43
+ try {
44
+ return await stopHostLocked(options);
45
+ }
46
+ finally {
47
+ await release();
48
+ }
49
+ }
50
+ async function stopHostLocked(options) {
32
51
  const paths = createHostDaemonPaths({
33
52
  socket: options.socket,
34
53
  ...(options.agentDir ? { agentDir: options.agentDir } : {}),
35
54
  });
55
+ const expected = options.expectedGeneration;
56
+ if (expected !== undefined) {
57
+ const refusal = generationRefusal(compareStopGeneration(await readHostRegistration(paths), expected));
58
+ if (refusal !== undefined)
59
+ return refusal;
60
+ }
36
61
  if (options.drain === true) {
37
62
  return drainHost(paths, {
38
63
  socket: options.socket,
@@ -41,9 +66,8 @@ export async function stopHost(options) {
41
66
  ...(options.timeoutMs !== undefined && { timeoutMs: options.timeoutMs }),
42
67
  });
43
68
  }
44
- const registered = await readHostRegistration(paths);
45
- const owner = await provenOwner(registered, options.socket);
46
- if (!owner)
69
+ const owner = expected === undefined ? await provenOwner(await readHostRegistration(paths), options.socket) : undefined;
70
+ if (expected === undefined && !owner)
47
71
  return { action: "refuse", reason: "unknown_owner" };
48
72
  const host = await probeProtocolInfo(options.socket, options.timeoutMs ?? 10_000);
49
73
  // A hard stop ends whatever the host is doing, including work that belongs to other clients:
@@ -55,19 +79,64 @@ export async function stopHost(options) {
55
79
  if (options.requireAnswer === true && (host === undefined || sessions === undefined)) {
56
80
  return { action: "refuse", reason: "sessions_live" };
57
81
  }
58
- await writeStopIntent(generationPaths(paths, owner.instanceId), {
82
+ const target = expected === undefined ? owner : await recheckedTarget(paths, options.socket, expected, host);
83
+ if (target === undefined)
84
+ return { action: "refuse", reason: "unknown_owner" };
85
+ if ("action" in target)
86
+ return target;
87
+ await writeStopIntent(generationPaths(paths, target.instanceId), {
59
88
  sender: { pid: process.pid, kind: "stop" },
60
- targetPid: owner.pid,
89
+ targetPid: target.pid,
61
90
  reason: "operator_stop",
62
91
  signal: "SIGTERM",
63
92
  at: new Date().toISOString(),
64
93
  });
65
- const delivered = signalGeneration(owner.pid, "SIGTERM");
94
+ const delivered = signalGeneration(target.pid, "SIGTERM");
66
95
  // A signal is not an exit: only the supervisor may release ownership after reaping its host.
67
96
  // Its circuit breaker deliberately leaves these records intact while the child might be alive.
68
97
  if (!delivered)
69
- await releaseGeneration(paths, { instanceId: owner.instanceId, pid: owner.pid });
70
- return { action: "stopped", pid: owner.pid };
98
+ await releaseGeneration(paths, { instanceId: target.instanceId, pid: target.pid });
99
+ return { action: "stopped", pid: target.pid };
100
+ }
101
+ function generationRefusal(match) {
102
+ if (match === "stale")
103
+ return { action: "refuse", reason: "stale_generation" };
104
+ if (match === "unprovable")
105
+ return { action: "refuse", reason: "unknown_owner" };
106
+ return undefined;
107
+ }
108
+ /**
109
+ * Refuses a binding the endpoint no longer matches, before any probe or census (desktop#3561 review): a
110
+ * caller learns its confirmation is stale rather than that the host it no longer names is busy.
111
+ */
112
+ export async function staleStopGeneration(socket, agentDir, expected) {
113
+ const paths = createHostDaemonPaths({ socket, ...(agentDir ? { agentDir } : {}) });
114
+ return generationRefusal(compareStopGeneration(await readHostRegistration(paths), expected));
115
+ }
116
+ /**
117
+ * The confirmed generation re-read after the probes, and the only pid/instance the stop may signal: the
118
+ * expected identity itself, never a registration read that was not compared with it.
119
+ */
120
+ async function recheckedTarget(paths, socket, expected, host) {
121
+ const current = await readHostRegistration(paths);
122
+ const match = compareStopGeneration(current, expected);
123
+ const refusal = generationRefusal(match);
124
+ if (refusal !== undefined)
125
+ return refusal;
126
+ if (match === "proven" && !(await provenOwner(current, socket)))
127
+ return undefined;
128
+ if (match === "bound_without_start_time") {
129
+ // No start time was recorded (Windows), so prove the same process another way before signalling: this
130
+ // endpoint, a live process that started before it wrote this registration (a later start is a reused pid),
131
+ // and, when the host answered, the confirmed instance.
132
+ if (current?.socket !== undefined && !sameEndpoint(current.socket, socket))
133
+ return undefined;
134
+ if (!(await startedBeforeRegistration(expected.pid, generationPaths(paths, expected.instanceId).pidFile)))
135
+ return undefined;
136
+ if (host?.instanceId !== undefined && host.instanceId !== expected.instanceId)
137
+ return { action: "refuse", reason: "stale_generation" };
138
+ }
139
+ return { pid: expected.pid, instanceId: expected.instanceId };
71
140
  }
72
141
  /**
73
142
  * Signals a generation whose owner was just proven, and reports whether the signal was delivered.
@@ -179,6 +179,11 @@ export declare class RpcClient {
179
179
  }>, "type" | "id" | "sessionId"> & {
180
180
  readonly sessionId?: string;
181
181
  }): Promise<RpcUnloadAnswer>;
182
+ /**
183
+ * Patch the context the attached session publishes (`set_session_context`); `null` deletes a key.
184
+ * Rejects with {@link RpcCommandError} carrying the host's code (`session_not_attached`, `invalid_session_context`).
185
+ */
186
+ setSessionContext(patch: Record<string, string | number | boolean | null>, sessionId?: string | undefined): Promise<void>;
182
187
  closeSession(sessionId?: string | undefined): Promise<void>;
183
188
  /**
184
189
  * Drop this connection's session attachments and wait until the host confirms they are gone (senpi#3238).
@@ -523,6 +528,10 @@ export declare class RpcClient {
523
528
  * Set the session display name.
524
529
  */
525
530
  setSessionName(name: string): Promise<void>;
531
+ /**
532
+ * Report that the user started typing a draft, so a first-activity prompt-cache prewarm can run (senpi#3859).
533
+ */
534
+ recordDraftActivity(): Promise<void>;
526
535
  importJsonl(inputPath: string, cwdOverride?: string): Promise<{
527
536
  cancelled: boolean;
528
537
  }>;
@@ -341,6 +341,15 @@ export class RpcClient {
341
341
  throw new Error("unload_session needs a session");
342
342
  return this.getData(await this.send({ ...options, type: "unload_session", sessionId }, false));
343
343
  }
344
+ /**
345
+ * Patch the context the attached session publishes (`set_session_context`); `null` deletes a key.
346
+ * Rejects with {@link RpcCommandError} carrying the host's code (`session_not_attached`, `invalid_session_context`).
347
+ */
348
+ async setSessionContext(patch, sessionId = this.sessionId) {
349
+ if (!sessionId)
350
+ throw new Error("set_session_context needs a session");
351
+ this.getData(await this.send({ type: "set_session_context", sessionId, patch }, false));
352
+ }
344
353
  async closeSession(sessionId = this.sessionId) {
345
354
  if (!sessionId)
346
355
  return;
@@ -807,6 +816,12 @@ export class RpcClient {
807
816
  async setSessionName(name) {
808
817
  await this.send({ type: "set_session_name", name });
809
818
  }
819
+ /**
820
+ * Report that the user started typing a draft, so a first-activity prompt-cache prewarm can run (senpi#3859).
821
+ */
822
+ async recordDraftActivity() {
823
+ await this.send({ type: "record_user_activity", kind: "draft" });
824
+ }
810
825
  async importJsonl(inputPath, cwdOverride) {
811
826
  const response = await this.send({ type: "import_jsonl", inputPath, cwdOverride });
812
827
  return this.getData(response);
@@ -372,6 +372,12 @@ type RpcSessionCommand = {
372
372
  id?: string;
373
373
  type: "set_session_name";
374
374
  name: string;
375
+ }
376
+ /** The user started typing a draft in this session's composer (senpi#3859). Only `draft` is accepted. */
377
+ | {
378
+ id?: string;
379
+ type: "record_user_activity";
380
+ kind: "draft";
375
381
  } | {
376
382
  id?: string;
377
383
  type: "import_jsonl";
@@ -572,6 +578,8 @@ export interface RpcAbortQuiesce {
572
578
  }
573
579
  /** Capability: `steer`/`follow_up` accept `whenIdle: "start"`. */
574
580
  export declare const STEER_WHEN_IDLE_CAPABILITY = "steer_when_idle";
581
+ /** Capability: `record_user_activity` reports composer draft activity (senpi#3859). */
582
+ export declare const USER_ACTIVITY_CAPABILITY = "user_activity";
575
583
  /** What `steer`/`follow_up` do on an idle session: queue (default) or start a turn. */
576
584
  export type RpcWhenIdle = "queue" | "start";
577
585
  /** What `dequeue_message` / `edit_queued_message` did; anything but removed/edited carries reason + action. */
@@ -1509,6 +1517,11 @@ export type RpcResponse = {
1509
1517
  type: "response";
1510
1518
  command: "set_session_name";
1511
1519
  success: true;
1520
+ } | {
1521
+ id?: string;
1522
+ type: "response";
1523
+ command: "record_user_activity";
1524
+ success: true;
1512
1525
  } | {
1513
1526
  id?: string;
1514
1527
  type: "response";
@@ -1593,6 +1606,11 @@ export type RpcResponse = {
1593
1606
  type: "response";
1594
1607
  command: "login_api_key";
1595
1608
  success: true;
1609
+ /** Present when the key replaced a login of the other kind; `backupPath` holds the previous file (senpi#3448). */
1610
+ data?: {
1611
+ replaced: "oauth" | "api_key";
1612
+ backupPath?: string;
1613
+ };
1596
1614
  } | {
1597
1615
  id?: string;
1598
1616
  type: "response";
@@ -105,4 +105,6 @@ export const RPC_ERROR_HOST_DRAINING = "host_draining";
105
105
  export const RPC_ERROR_HOST_STOPPING = "host_stopping";
106
106
  /** Capability: `steer`/`follow_up` accept `whenIdle: "start"`. */
107
107
  export const STEER_WHEN_IDLE_CAPABILITY = "steer_when_idle";
108
+ /** Capability: `record_user_activity` reports composer draft activity (senpi#3859). */
109
+ export const USER_ACTIVITY_CAPABILITY = "user_activity";
108
110
  //# sourceMappingURL=rpc-types.js.map
@@ -13,5 +13,5 @@ export interface RpcSessionBinding {
13
13
  * Creates the extension UI bridge and subscriptions within the entry's provider
14
14
  * scope. The classic handler remains the single source of command semantics.
15
15
  */
16
- export declare function createRpcSessionBinding(sessionId: string, entry: RpcSessionEntry, writer: SessionEventWriter, requestClose: () => void, options?: Pick<RpcConnectionOptions, "capabilities" | "clientInfo">): Promise<RpcSessionBinding>;
16
+ export declare function createRpcSessionBinding(sessionId: string, entry: RpcSessionEntry, writer: SessionEventWriter, requestClose: () => void, options?: Pick<RpcConnectionOptions, "capabilities" | "clientInfo" | "connectionRoles">): Promise<RpcSessionBinding>;
17
17
  //# sourceMappingURL=session-binding.d.ts.map
@@ -61,6 +61,7 @@ export declare class SessionCommandRouter {
61
61
  private readonly createBinding;
62
62
  private readonly connectionOptions;
63
63
  private readonly pendingCapabilities;
64
+ private readonly connectionRoles;
64
65
  private readonly finalizations;
65
66
  private readonly idleNow;
66
67
  private readonly idleEvictionMs;
@@ -2,6 +2,7 @@ import { VERSION } from "../../config.js";
2
2
  import { subscriptionLimits } from "../../core/subscription-limits/limit-store.js";
3
3
  import { detachConnection } from "./connection-detach.js";
4
4
  import { buildRpcSessionState } from "./connection-handler.js";
5
+ import { CONNECTION_ROLE_FIXED, ConnectionRoles, roleConnection } from "./connection-roles.js";
5
6
  import { answerEffectFromTranscript } from "./effect-commands.js";
6
7
  import { latestSessionEffect } from "./effect-ledger.js";
7
8
  import { multiSessionHostCapabilities } from "./host-capabilities.js";
@@ -49,6 +50,7 @@ export class SessionCommandRouter {
49
50
  /** Connections whose socket has already been detached. */
50
51
  this.releasedConnections = new Set();
51
52
  this.pendingCapabilities = new Map();
53
+ this.connectionRoles = new ConnectionRoles();
52
54
  this.finalizations = new Map();
53
55
  this.draining = false;
54
56
  this.handoffParks = 0;
@@ -320,6 +322,15 @@ export class SessionCommandRouter {
320
322
  }
321
323
  if (command.type === "set_client_info" && !command.sessionId) {
322
324
  const connection = this.writer.currentConnection();
325
+ if (command.capabilities !== undefined &&
326
+ !this.connectionRoles.declare(roleConnection(connection), command.capabilities))
327
+ return {
328
+ id: command.id,
329
+ type: "response",
330
+ command: "set_client_info",
331
+ success: false,
332
+ error: CONNECTION_ROLE_FIXED,
333
+ };
323
334
  if (connection !== undefined) {
324
335
  this.pendingCapabilities.set(connection, command.capabilities ?? []);
325
336
  this.writer.setConnectionCapabilities(connection, command.capabilities ?? []);
@@ -766,6 +777,7 @@ export class SessionCommandRouter {
766
777
  capabilities: owner !== undefined
767
778
  ? (this.pendingCapabilities.get(owner) ?? this.connectionOptions?.capabilities ?? [])
768
779
  : this.connectionOptions?.capabilities,
780
+ connectionRoles: this.connectionRoles,
769
781
  clientInfo: {
770
782
  setCapabilities: (connectionId, capabilities) => {
771
783
  if (connectionId !== undefined) {
@@ -872,6 +884,7 @@ export class SessionCommandRouter {
872
884
  async releaseConnection(connectionId) {
873
885
  this.releasedConnections.add(connectionId);
874
886
  this.writer.clearConnectionCapabilities(connectionId);
887
+ this.connectionRoles.release(connectionId);
875
888
  for (const sessionId of this.sessionsByConnection.get(connectionId)?.keys() ?? [])
876
889
  this.writer.detachConnectionFromSession(connectionId, sessionId);
877
890
  const opens = this.opensByConnection.get(connectionId);
@@ -43,7 +43,7 @@ export declare class SessionWorkerClient {
43
43
  /** Moves the worker's live session to another permission preset (a later `open_session.permissionPreset`). */
44
44
  setPermissionPreset(preset: string): Promise<void>;
45
45
  commit(): Promise<WorkerSnapshot>;
46
- bind(sessionId: string, writer: SessionEventWriter, requestClose: () => void, options: Pick<RpcConnectionOptions, "capabilities" | "clientInfo">): Promise<RpcSessionBinding>;
46
+ bind(sessionId: string, writer: SessionEventWriter, requestClose: () => void, options: Pick<RpcConnectionOptions, "capabilities" | "clientInfo" | "connectionRoles">): Promise<RpcSessionBinding>;
47
47
  get busy(): boolean;
48
48
  get handoffBusy(): boolean;
49
49
  subscribeSettled(listener: () => void): () => void;
@@ -3,6 +3,7 @@ import { Worker } from "node:worker_threads";
3
3
  import { isBunBinary } from "../../config.js";
4
4
  import { ownProcessStartedAtMs } from "../../core/extensions/builtin/terminal/process-identity.js";
5
5
  import { createWebViewBroker } from "../../core/webview/webview-broker.js";
6
+ import { roleConnection } from "./connection-roles.js";
6
7
  import { SessionWorkerRequests } from "./session-worker-requests.js";
7
8
  import { acknowledge, acknowledgeGrant } from "./session-worker-signals.js";
8
9
  const compiledWorkerEntry = typeof SENPI_RPC_SESSION_WORKER_ENTRY === "string"
@@ -95,10 +96,17 @@ export class SessionWorkerClient {
95
96
  sessionId,
96
97
  capabilities: options.capabilities ?? [],
97
98
  connection: writer.currentConnection(),
99
+ roles: options.connectionRoles?.rolesOf(roleConnection(writer.currentConnection())),
98
100
  });
99
101
  return {
100
102
  handle: async (command) => {
101
- await this.request({ type: "command", command, connection: writer.currentConnection() });
103
+ const connection = writer.currentConnection();
104
+ await this.request({
105
+ type: "command",
106
+ command,
107
+ connection,
108
+ roles: options.connectionRoles?.rolesOf(roleConnection(connection)),
109
+ });
102
110
  },
103
111
  cancelPendingExtensionUiRequests: () => this.post({ type: "cancel_ui" }),
104
112
  dispose: async () => {
@@ -208,6 +216,9 @@ export class SessionWorkerClient {
208
216
  return;
209
217
  }
210
218
  case "capabilities":
219
+ // The isolate already refused a changed role; a refusal here means the two registries drifted.
220
+ if (this.options.connectionRoles?.declare(roleConnection(message.connection), message.capabilities) === false)
221
+ process.stderr.write(`senpi rpc worker role drift: connection ${roleConnection(message.connection)} declared a changed role\n`);
211
222
  this.options.clientInfo?.setCapabilities(message.connection, message.capabilities);
212
223
  acknowledge(message.signal, true);
213
224
  return;
@@ -47,11 +47,14 @@ export type HostToSessionWorker = {
47
47
  sessionId: string;
48
48
  capabilities: readonly string[];
49
49
  connection?: string;
50
+ /** Roles the connection explicitly declared on the host (senpi#3811); the isolate adopts them. */
51
+ roles?: readonly string[];
50
52
  } | {
51
53
  type: "command";
52
54
  request: number;
53
55
  command: object;
54
56
  connection?: string;
57
+ roles?: readonly string[];
55
58
  } | {
56
59
  type: "prompt_surface";
57
60
  request: number;
@@ -12,6 +12,7 @@ import { registerWebViewBroker } from "../../core/webview/webview-broker.js";
12
12
  import { createCliRuntimeFactory } from "../../main.js";
13
13
  import { initTheme } from "../interactive/theme/theme.js";
14
14
  import { buildRpcSessionState } from "./connection-handler.js";
15
+ import { ConnectionRoles, roleConnection } from "./connection-roles.js";
15
16
  import { isHandoffBusy } from "./handoff-activity.js";
16
17
  import { createRpcSessionBinding } from "./session-binding.js";
17
18
  import { SessionEventWriter } from "./session-event-writer.js";
@@ -84,6 +85,8 @@ class WorkerEventWriter extends SessionEventWriter {
84
85
  }
85
86
  }
86
87
  const writer = new WorkerEventWriter();
88
+ // One session per isolate: the roles of the connections it serves, adopted from the host before each command.
89
+ const connectionRoles = new ConnectionRoles();
87
90
  let prepared;
88
91
  let registry;
89
92
  let entry;
@@ -150,9 +153,12 @@ async function handle(message) {
150
153
  case "bind": {
151
154
  if (!entry || binding)
152
155
  throw new Error("Invalid session binding");
156
+ if (message.roles !== undefined)
157
+ connectionRoles.adopt(roleConnection(message.connection), message.roles);
153
158
  const bindingEntry = entry;
154
159
  const createBinding = () => createRpcSessionBinding(message.sessionId, bindingEntry, writer, () => send({ type: "request_close" }), {
155
160
  capabilities: message.capabilities,
161
+ connectionRoles,
156
162
  clientInfo: {
157
163
  connectionId: () => writer.currentConnection(),
158
164
  setCapabilities: (connection, capabilities) => exchange((signal) => ({ type: "capabilities", connection, capabilities, signal }), "session_worker_capabilities_denied"),
@@ -177,6 +183,8 @@ async function handle(message) {
177
183
  ["abort", "abort_bash", "extension_ui_response", "extension_ui_progress"].includes(String(message.command.type));
178
184
  if (!binding || (closing && !privileged))
179
185
  throw new Error("session_closing");
186
+ if (message.roles !== undefined)
187
+ connectionRoles.adopt(roleConnection(message.connection), message.roles);
180
188
  const activeBinding = binding;
181
189
  await (message.connection === undefined
182
190
  ? activeBinding.handle(message.command)
@@ -212,6 +220,7 @@ async function handle(message) {
212
220
  unsubscribe?.();
213
221
  unsubscribeWake?.();
214
222
  await binding?.dispose();
223
+ connectionRoles.clear();
215
224
  if (entry?.runtime) {
216
225
  const current = entry;
217
226
  await runWithProviderScope(current.scope, async () => {