@code-yeongyu/senpi 2026.11.6-2 → 2026.11.6-3

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 (165) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/bundle/chunks/{bun-extension-importer-RCGPQTI2.js → bun-extension-importer-GQ6IKEXD.js} +2 -2
  3. package/dist/bundle/chunks/{chunk-QN2BHP3M.js → chunk-44BJPS5I.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-C7OKUC7L.js → chunk-4BATXPB7.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-RQ6CHXQ2.js → chunk-5LKB732P.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-X5ZVYNOL.js → chunk-6CEDL5S2.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-HLM46XEB.js → chunk-7DRWPZ3N.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-7PSBD5KS.js → chunk-BASS47UD.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-ECW7VBMB.js → chunk-CFEHJ4VU.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-TIPKAMSS.js → chunk-EO2VPZ2K.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-LNPAT22L.js → chunk-F5JLDCYD.js} +8 -7
  12. package/dist/bundle/chunks/{chunk-GSKCWXXG.js → chunk-JMQOLOC2.js} +1 -1
  13. package/dist/bundle/chunks/chunk-L7ZIS3MC.js +106 -0
  14. package/dist/bundle/chunks/{chunk-47PPZKPO.js → chunk-LM6DOWQH.js} +4 -4
  15. package/dist/bundle/chunks/chunk-LPYBKMPT.js +2 -0
  16. package/dist/bundle/chunks/{chunk-GNSMNSL7.js → chunk-LUZVGPQ7.js} +2 -2
  17. package/dist/bundle/chunks/{chunk-7III44PV.js → chunk-MJTCZUXS.js} +1 -1
  18. package/dist/bundle/chunks/{chunk-2FDW2LQL.js → chunk-SB6MXNO5.js} +1 -1
  19. package/dist/bundle/chunks/chunk-UODSYQ2D.js +2 -0
  20. package/dist/bundle/chunks/{chunk-D5MRJWUK.js → chunk-V2QAYGOK.js} +216 -224
  21. package/dist/bundle/chunks/{chunk-WGRKM3N6.js → chunk-X53PD4ZC.js} +1 -1
  22. package/dist/bundle/chunks/{cli-main-T74YJGIF.js → cli-main-RWHDZKTQ.js} +1 -1
  23. package/dist/bundle/chunks/{dispatch-BAK2YZKD.js → dispatch-VDIYSXQN.js} +1 -1
  24. package/dist/bundle/chunks/{hand-entry-AYRUGE2A.js → hand-entry-G5LYF62Q.js} +1 -1
  25. package/dist/bundle/chunks/{host-command-YRXZE4TV.js → host-command-I2JZ5JM7.js} +1 -1
  26. package/dist/bundle/chunks/{host-session-command-N7IN7ZDV.js → host-session-command-Y52NFPP2.js} +1 -1
  27. package/dist/bundle/chunks/interactive-mode-PA7DGVS6.js +2 -0
  28. package/dist/bundle/chunks/main-OBL6375C.js +2 -0
  29. package/dist/bundle/chunks/{multi-session-host-XSUJDHFZ.js → multi-session-host-NM6I5BWZ.js} +20 -17
  30. package/dist/bundle/chunks/{package-manager-cli-GLUNPQJU.js → package-manager-cli-GNRQ5MVR.js} +1 -1
  31. package/dist/bundle/chunks/{role-RRFFHZF5.js → role-F56BS2ZQ.js} +1 -1
  32. package/dist/bundle/chunks/rpc-mode-G4KIP7XS.js +2 -0
  33. package/dist/bundle/chunks/{run-attached-tui-W236SEOR.js → run-attached-tui-XTGPAGYM.js} +1 -1
  34. package/dist/bundle/chunks/{runtime-session-OBXYK4PI.js → runtime-session-VO3X4OE3.js} +1 -1
  35. package/dist/bundle/chunks/{schedule-command-B2EJTBOE.js → schedule-command-L4M3TSQ3.js} +1 -1
  36. package/dist/bundle/chunks/{session-control-endpoint-YIMLHGLG.js → session-control-endpoint-V46WMCDF.js} +1 -1
  37. package/dist/bundle/chunks/{session-picker-E6ZONUJB.js → session-picker-AG6KSR5A.js} +1 -1
  38. package/dist/bundle/chunks/session-worker.js +87 -83
  39. package/dist/bundle/cli.js +2 -2
  40. package/dist/bundle/index.js +1 -1
  41. package/dist/bundle/rpc-entry.js +1 -1
  42. package/dist/bundle/runtime-manifest.json +1 -1
  43. package/dist/core/agent-session-runtime.d.ts +2 -0
  44. package/dist/core/agent-session-runtime.js +9 -2
  45. package/dist/core/agent-session.d.ts +24 -3
  46. package/dist/core/agent-session.js +94 -28
  47. package/dist/core/credential-pool/rotation-slots.d.ts +3 -3
  48. package/dist/core/credential-pool/state-store.d.ts +1 -1
  49. package/dist/core/egress/hpke.d.ts +63 -0
  50. package/dist/core/egress/hpke.js +188 -0
  51. package/dist/core/egress/placement-claim.d.ts +69 -0
  52. package/dist/core/egress/placement-claim.js +115 -0
  53. package/dist/core/egress/pool-state.d.ts +18 -0
  54. package/dist/core/egress/pool-state.js +5 -0
  55. package/dist/core/egress/sealed-format.d.ts +101 -0
  56. package/dist/core/egress/sealed-format.js +174 -0
  57. package/dist/core/egress/wire.d.ts +226 -0
  58. package/dist/core/egress/wire.js +47 -0
  59. package/dist/core/extensions/builtin/ask-user/escape-residue.d.ts +50 -0
  60. package/dist/core/extensions/builtin/ask-user/escape-residue.js +113 -0
  61. package/dist/core/extensions/builtin/ask-user/extension.js +7 -1
  62. package/dist/core/extensions/builtin/ask-user/resume.js +2 -1
  63. package/dist/core/extensions/builtin/ask-user/tool.d.ts +3 -0
  64. package/dist/core/extensions/builtin/ask-user/tool.js +8 -0
  65. package/dist/core/extensions/builtin/goal/index.js +46 -0
  66. package/dist/core/extensions/builtin/goal/stop-pause.d.ts +1 -0
  67. package/dist/core/extensions/builtin/goal/stop-pause.js +4 -2
  68. package/dist/core/extensions/builtin/permission-system/index.js +10 -4
  69. package/dist/core/extensions/builtin/permission-system/preset-registry.d.ts +2 -0
  70. package/dist/core/extensions/builtin/permission-system/preset-registry.js +11 -0
  71. package/dist/core/extensions/builtin/permission-system/settings.d.ts +2 -0
  72. package/dist/core/extensions/builtin/permission-system/settings.js +10 -1
  73. package/dist/core/extensions/builtin/terminal/tools/bash.d.ts +1 -1
  74. package/dist/core/extensions/builtin/terminal/tools/bash.js +38 -9
  75. package/dist/core/extensions/builtin/terminal/tools/context.d.ts +3 -0
  76. package/dist/core/extensions/bun-extension-importer.js +4 -3
  77. package/dist/core/extensions/bun-extension-load-turns.d.ts +4 -0
  78. package/dist/core/extensions/bun-extension-load-turns.js +45 -0
  79. package/dist/core/extensions/bun-extension-registry.d.ts +1 -0
  80. package/dist/core/extensions/bun-extension-registry.js +19 -1
  81. package/dist/core/extensions/loader.js +5 -0
  82. package/dist/core/extensions/session-dialog-api.d.ts +5 -0
  83. package/dist/core/extensions/session-dialog-api.js +13 -0
  84. package/dist/core/extensions/types.d.ts +14 -1
  85. package/dist/core/permission-decision.d.ts +22 -0
  86. package/dist/core/permission-decision.js +53 -0
  87. package/dist/core/permission-mode.d.ts +56 -0
  88. package/dist/core/permission-mode.js +133 -0
  89. package/dist/core/retry-fallback/controller-types.d.ts +6 -0
  90. package/dist/core/retry-fallback/controller.d.ts +8 -0
  91. package/dist/core/retry-fallback/controller.js +31 -3
  92. package/dist/core/retry-fallback/unusable.d.ts +23 -1
  93. package/dist/core/retry-fallback/unusable.js +80 -1
  94. package/dist/core/retry-fallback/usage-limit.d.ts +1 -1
  95. package/dist/core/retry-fallback/usage-limit.js +6 -3
  96. package/dist/core/sdk.js +2 -0
  97. package/dist/core/session-log.js +1 -1
  98. package/dist/core/stall-warning.d.ts +41 -0
  99. package/dist/core/stall-warning.js +72 -0
  100. package/dist/core/turn-origin.d.ts +27 -0
  101. package/dist/core/turn-origin.js +54 -0
  102. package/dist/modes/rpc/client-input-handler.d.ts +2 -0
  103. package/dist/modes/rpc/client-input-handler.js +2 -0
  104. package/dist/modes/rpc/connection-handler.js +16 -5
  105. package/dist/modes/rpc/connection-roles.d.ts +6 -0
  106. package/dist/modes/rpc/connection-roles.js +13 -0
  107. package/dist/modes/rpc/delegated-drive-origin.d.ts +9 -0
  108. package/dist/modes/rpc/delegated-drive-origin.js +16 -0
  109. package/dist/modes/rpc/effect-commands.d.ts +6 -0
  110. package/dist/modes/rpc/host-bare-endpoint-registration.d.ts +7 -0
  111. package/dist/modes/rpc/host-bare-endpoint-registration.js +35 -0
  112. package/dist/modes/rpc/host-capabilities.d.ts +2 -0
  113. package/dist/modes/rpc/host-capabilities.js +3 -0
  114. package/dist/modes/rpc/host-handover-parked.d.ts +11 -0
  115. package/dist/modes/rpc/host-handover-parked.js +17 -0
  116. package/dist/modes/rpc/host-handover-reopen.js +18 -6
  117. package/dist/modes/rpc/host-handover-resident-writer.d.ts +16 -0
  118. package/dist/modes/rpc/host-handover-resident-writer.js +57 -0
  119. package/dist/modes/rpc/host-warm.d.ts +16 -1
  120. package/dist/modes/rpc/host-warm.js +33 -6
  121. package/dist/modes/rpc/method-classes-schema-session.d.ts +9 -0
  122. package/dist/modes/rpc/method-classes-schema-session.js +1 -0
  123. package/dist/modes/rpc/method-classes-schema.d.ts +9 -0
  124. package/dist/modes/rpc/method-classes.d.ts +1 -0
  125. package/dist/modes/rpc/method-classes.js +2 -0
  126. package/dist/modes/rpc/multi-session-host.js +6 -1
  127. package/dist/modes/rpc/permission-preset-command.d.ts +6 -0
  128. package/dist/modes/rpc/permission-preset-command.js +27 -0
  129. package/dist/modes/rpc/rpc-mutation-response-types.d.ts +7 -0
  130. package/dist/modes/rpc/rpc-session-command-types.d.ts +4 -0
  131. package/dist/modes/rpc/rpc-session-event-types.d.ts +5 -2
  132. package/dist/modes/rpc/rpc-session-state-types.d.ts +2 -1
  133. package/dist/modes/rpc/rpc-session-state.js +2 -0
  134. package/dist/modes/rpc/session-attachment-commands.js +3 -3
  135. package/dist/modes/rpc/session-event-writer-connections.js +1 -0
  136. package/dist/modes/rpc/session-event-writer.d.ts +1 -1
  137. package/dist/modes/rpc/session-registry-attach.d.ts +0 -9
  138. package/dist/modes/rpc/session-registry-attach.js +16 -3
  139. package/dist/modes/rpc/session-registry-types.d.ts +2 -0
  140. package/dist/modes/rpc/session-registry.d.ts +2 -0
  141. package/dist/modes/rpc/session-registry.js +16 -3
  142. package/dist/modes/rpc/session-router-close.js +43 -6
  143. package/dist/modes/rpc/session-router-context.js +2 -0
  144. package/dist/modes/rpc/session-router-occupancy.js +2 -11
  145. package/dist/modes/rpc/session-router-open-profile.js +4 -0
  146. package/dist/modes/rpc/session-router-open.d.ts +1 -1
  147. package/dist/modes/rpc/session-router-open.js +6 -17
  148. package/dist/modes/rpc/session-router-residency.d.ts +24 -0
  149. package/dist/modes/rpc/session-router-residency.js +90 -0
  150. package/dist/modes/rpc/session-router-routing.js +7 -1
  151. package/dist/modes/rpc/session-status.js +4 -1
  152. package/dist/modes/rpc/session-worker.js +4 -0
  153. package/dist/modes/rpc/worker-session-registry.js +5 -2
  154. package/docs/engine-socket/fixtures/admission-routes/h-legacy.json +1 -1
  155. package/docs/engine-socket/fixtures/rpc-inbound-samples.json +12 -2
  156. package/docs/extensions.md +12 -0
  157. package/docs/rpc.md +72 -12
  158. package/native/socket-secret/prebuilds/win32-arm64/senpi_socket_secret.win32-arm64.node +0 -0
  159. package/native/socket-secret/prebuilds/win32-x64/senpi_socket_secret.win32-x64.node +0 -0
  160. package/package.json +6 -6
  161. package/dist/bundle/chunks/chunk-C7HEEBFH.js +0 -2
  162. package/dist/bundle/chunks/chunk-XCMII2VQ.js +0 -98
  163. package/dist/bundle/chunks/interactive-mode-B5SQDLH7.js +0 -2
  164. package/dist/bundle/chunks/main-UQFVFCNY.js +0 -2
  165. package/dist/bundle/chunks/rpc-mode-JYN3E7P7.js +0 -2
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Deadline-bounded warnings for waits that have no deadline of their own (senpi#6189).
3
+ *
4
+ * A wait that never settles leaves no trace: no timer fires, no result is written, and the
5
+ * session just idles. These helpers only observe such a wait. They log once when it outlives
6
+ * its threshold and never resolve, reject, abort or delay anything themselves.
7
+ */
8
+ /** A wait still pending after this long is logged once. A bash spawn's own shorter timeout lowers it. */
9
+ export const STALL_WARNING_MS = 30_000;
10
+ /** The warning threshold for a bash spawn whose own `timeout` (in seconds) may be shorter than {@link STALL_WARNING_MS}. */
11
+ export function stallThresholdMs(timeoutSeconds) {
12
+ if (typeof timeoutSeconds !== "number" || !Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) {
13
+ return STALL_WARNING_MS;
14
+ }
15
+ return Math.max(1, Math.min(STALL_WARNING_MS, Math.trunc(timeoutSeconds * 1000)));
16
+ }
17
+ export function queueEntryLabel(event) {
18
+ const toolCallId = event.toolCallId;
19
+ return typeof toolCallId === "string" ? `${event.type}:${toolCallId}` : event.type;
20
+ }
21
+ /**
22
+ * Call `onStall` once if `stop` has not run within `thresholdMs`. The caller keeps its own `await`, so the
23
+ * watched wait's outcome and ordering are unchanged. The timer is unref'd: it never keeps the process alive.
24
+ */
25
+ export function watchForStall(thresholdMs, onStall) {
26
+ const startedAt = Date.now();
27
+ const timer = setTimeout(() => onStall(Date.now() - startedAt), thresholdMs);
28
+ timer.unref?.();
29
+ return { stop: () => clearTimeout(timer) };
30
+ }
31
+ /** The unsettled entries of a promise chain, oldest first, so a stalled wait on the chain can name what it waits behind. */
32
+ export class PendingQueueLedger {
33
+ constructor() {
34
+ this.nextId = 0;
35
+ this.entries = new Map();
36
+ }
37
+ /**
38
+ * Record `run` as an entry from now until it settles, and return it wrapped for the chain. The wrapper returns
39
+ * exactly what `run` returns or throws, and releases the entry from that result. It never attaches a handler to
40
+ * the chain itself, so a rejection on the chain is exactly as visible (or unhandled) as before.
41
+ */
42
+ track(label, run) {
43
+ const id = this.nextId++;
44
+ this.entries.set(id, { label, enqueuedAt: Date.now() });
45
+ const release = () => {
46
+ this.entries.delete(id);
47
+ };
48
+ return (...args) => {
49
+ let result;
50
+ try {
51
+ result = run(...args);
52
+ }
53
+ catch (error) {
54
+ release();
55
+ throw error;
56
+ }
57
+ // The chain adopts `result`, which marks it handled anyway; this handler changes nobody's view of it.
58
+ if (result instanceof Promise)
59
+ void result.then(release, release);
60
+ else
61
+ release();
62
+ return result;
63
+ };
64
+ }
65
+ snapshot() {
66
+ const head = this.entries.values().next().value;
67
+ if (head === undefined)
68
+ return { queueDepth: 0 };
69
+ return { queueDepth: this.entries.size, queueHead: head.label, ageMs: Date.now() - head.enqueuedAt };
70
+ }
71
+ }
72
+ //# sourceMappingURL=stall-warning.js.map
@@ -0,0 +1,27 @@
1
+ import type { SessionManager } from "./session-manager.ts";
2
+ /**
3
+ * Who started a turn, for the delegated-default permission rule (remote approval-mode work):
4
+ * - `local`: a local connection's or the TUI's own input;
5
+ * - `delegated`: input that arrived on a delegated (remote device) connection;
6
+ * - `engine`: a turn no client record started (an extension's message, a goal or loop turn, restored input).
7
+ * A steer joins the running turn (`join`) and never changes its origin.
8
+ */
9
+ export type TurnOrigin = "local" | "delegated" | "engine";
10
+ export type InputTurnMark = TurnOrigin | "join";
11
+ export type ConnectionOrigin = "local" | "delegated";
12
+ type OriginSessionManager = Pick<SessionManager, "getEntries" | "appendCustomEntry">;
13
+ /** The mark an input gets where it is created: explicit for client records, `local` for the TUI, else `engine`. */
14
+ export declare function inputTurnMark(input: {
15
+ readonly mode?: "steer" | "followUp";
16
+ readonly connectionOrigin?: ConnectionOrigin;
17
+ readonly source?: string;
18
+ }): InputTurnMark;
19
+ export declare function markInputTurnOrigin(message: object, mark: InputTurnMark): void;
20
+ /** A run starts as engine work; the user message that opens a turn sets its origin, a steer keeps it. */
21
+ export declare function noteRunStart(sessionManager: object): void;
22
+ export declare function noteUserMessageStart(sessionManager: object, message: object): void;
23
+ export declare function recordDelegatedOrigin(sessionManager: OriginSessionManager): void;
24
+ /** Whether the deciding turn counts as delegated-origin: a remote device started it, or nobody did on a remote-driven session. */
25
+ export declare function decidingTurnIsDelegated(sessionManager: Pick<SessionManager, "getEntries">): boolean;
26
+ export {};
27
+ //# sourceMappingURL=turn-origin.d.ts.map
@@ -0,0 +1,54 @@
1
+ const DELEGATED_ORIGIN_ENTRY = "delegated_origin";
2
+ const inputMarks = new WeakMap();
3
+ const currentOrigins = new WeakMap();
4
+ const delegatedSessions = new WeakSet();
5
+ /** The mark an input gets where it is created: explicit for client records, `local` for the TUI, else `engine`. */
6
+ export function inputTurnMark(input) {
7
+ if (input.mode === "steer")
8
+ return "join";
9
+ if (input.connectionOrigin !== undefined)
10
+ return input.connectionOrigin;
11
+ return input.source === undefined || input.source === "interactive" ? "local" : "engine";
12
+ }
13
+ export function markInputTurnOrigin(message, mark) {
14
+ inputMarks.set(message, mark);
15
+ }
16
+ /** A run starts as engine work; the user message that opens a turn sets its origin, a steer keeps it. */
17
+ export function noteRunStart(sessionManager) {
18
+ currentOrigins.set(sessionManager, "engine");
19
+ }
20
+ export function noteUserMessageStart(sessionManager, message) {
21
+ const mark = inputMarks.get(message);
22
+ if (mark === "join")
23
+ return;
24
+ currentOrigins.set(sessionManager, mark ?? "engine");
25
+ }
26
+ export function recordDelegatedOrigin(sessionManager) {
27
+ if (delegatedOriginOf(sessionManager))
28
+ return;
29
+ delegatedSessions.add(sessionManager);
30
+ sessionManager.appendCustomEntry(DELEGATED_ORIGIN_ENTRY, { delegatedOrigin: true }, { startupBookkeeping: true });
31
+ }
32
+ // How many entries a session's scan has already found free of the marker: each gated decision reads only new entries.
33
+ const scannedEntries = new WeakMap();
34
+ function delegatedOriginOf(sessionManager) {
35
+ if (delegatedSessions.has(sessionManager))
36
+ return true;
37
+ const entries = sessionManager.getEntries();
38
+ const persisted = entries
39
+ .slice(scannedEntries.get(sessionManager) ?? 0)
40
+ .some((entry) => entry.type === "custom" && entry.customType === DELEGATED_ORIGIN_ENTRY);
41
+ if (persisted)
42
+ delegatedSessions.add(sessionManager);
43
+ else
44
+ scannedEntries.set(sessionManager, entries.length);
45
+ return persisted;
46
+ }
47
+ /** Whether the deciding turn counts as delegated-origin: a remote device started it, or nobody did on a remote-driven session. */
48
+ export function decidingTurnIsDelegated(sessionManager) {
49
+ const origin = currentOrigins.get(sessionManager) ?? "engine";
50
+ if (origin === "delegated")
51
+ return true;
52
+ return origin === "engine" && delegatedOriginOf(sessionManager);
53
+ }
54
+ //# sourceMappingURL=turn-origin.js.map
@@ -1,4 +1,5 @@
1
1
  import type { AgentSession } from "../../core/agent-session.ts";
2
+ import type { ConnectionOrigin } from "../../core/turn-origin.ts";
2
3
  import { type RpcClientInput } from "./client-admission-record.ts";
3
4
  type InputOutput = {
4
5
  readonly output: (record: object) => void;
@@ -7,6 +8,7 @@ type InputOutput = {
7
8
  /** The sending connection's declared role; a connection that never declared `interactive_user` is an agent's. */
8
9
  type InputClient = {
9
10
  readonly interactiveUser: boolean;
11
+ readonly connectionOrigin?: ConnectionOrigin;
10
12
  };
11
13
  /** The same admission path serves classic stdio, shared sessions, and worker sessions. */
12
14
  export declare function handleClientInput(session: AgentSession, command: RpcClientInput, io: InputOutput, client?: InputClient): Promise<void>;
@@ -72,6 +72,7 @@ export async function handleClientInput(session, command, io, client = { interac
72
72
  ...options,
73
73
  source: "rpc",
74
74
  interactiveUser: client.interactiveUser,
75
+ ...(client.connectionOrigin === undefined ? {} : { connectionOrigin: client.connectionOrigin }),
75
76
  ...chain,
76
77
  onQueuedInput: hooks.queued,
77
78
  promptDisposition: (next) => {
@@ -136,6 +137,7 @@ export async function handleClientInput(session, command, io, client = { interac
136
137
  expectedTurnId: command.expectedTurnId,
137
138
  source: "rpc",
138
139
  interactiveUser: client.interactiveUser,
140
+ ...(client.connectionOrigin === undefined ? {} : { connectionOrigin: client.connectionOrigin }),
139
141
  ...chain,
140
142
  onQueuedInput: hooks.queued,
141
143
  };
@@ -51,12 +51,15 @@ import { ConnectionQuestionBridge, degradeQuestion, sessionQuestionBridges } fro
51
51
  import { remoteOriginDeclaration, remoteOriginField } from "./remote-origin.js";
52
52
  import { advertisedCompactCapabilities, sessionDeclaration } from "./compact-negotiation.js";
53
53
  import { commandAuthority } from "./command-authority.js";
54
+ import { noteDelegatedRecord } from "./delegated-drive-origin.js";
54
55
  import { ConnectionRoles, DELEGATION_INCARNATION_STALE, roleConnection } from "./connection-roles.js";
55
56
  import { ACCOUNT_RESET_CAPABILITY, AUTH_KEY_VERIFY_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, buildCustomUnsupportedRequest, CONTINUE_FROM_LEAF_CAPABILITY, DEFAULT_CUSTOM_EXTENSION_LABEL, EVAL_CELLS_CAPABILITY, EXTENSION_EVENTS_CAPABILITY, ENTRIES_TAIL_CAPABILITY, INTERACTIVE_USER_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, PROVIDER_ACCOUNT_TEST_CAPABILITY, QUESTION_CAPABILITY, QUEUED_MESSAGE_REVISION_CAPABILITY, RESUME_SESSION_CAPABILITY, RETRY_FALLBACK_COMMAND_CAPABILITY, SESSION_HELD_CAPABILITY, TOOL_BARRIER_CAPABILITY, TASK_PARENT_CAPABILITY, } from "./custom-capability.js";
56
57
  import { runAbort, runGetEffectStatus, runQueuedInputChange, runSetThinkingLevel, } from "./effect-commands.js";
57
58
  import { EffectLedger } from "./effect-ledger.js";
58
59
  import { EFFECT_LEDGER_CAPABILITY } from "./effect-record.js";
59
60
  import { handleModelCommand } from "./handlers-model.js";
61
+ import { SET_PERMISSION_PRESET_CAPABILITY } from "./host-capabilities.js";
62
+ import { runSetPermissionPreset } from "./permission-preset-command.js";
60
63
  import { createRpcEventOutputBuffer } from "./event-output-buffer.js";
61
64
  import { extensionUiProgressReply, refusedExtensionUiResponse } from "./extension-ui-response.js";
62
65
  import { answerExtensionUiResponse } from "./handlers-answer.js";
@@ -86,11 +89,13 @@ import { answerHostResponse, isHostResponse, relocationChannel, } from "./sessio
86
89
  export { buildRpcSessionState } from "./rpc-session-state.js";
87
90
  /** The tool call a dialog is about, as wire fields (absent when the dialog is not a permission request). */
88
91
  function dialogCall(opts) {
92
+ const audience = opts?.audience === "session" ? { audience: "session" } : {};
89
93
  if (opts?.toolCallId === undefined)
90
- return {};
94
+ return audience;
91
95
  return {
92
96
  toolCallId: opts.toolCallId,
93
97
  ...(opts.parentToolCallId === undefined ? {} : { parentToolCallId: opts.parentToolCallId }),
98
+ ...audience,
94
99
  };
95
100
  }
96
101
  function loadedExtensionName(path) {
@@ -201,6 +206,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
201
206
  const connectionRoles = options.connectionRoles ?? new ConnectionRoles();
202
207
  const currentConnectionId = () => roleConnection(options.clientInfo?.connectionId());
203
208
  const hasRole = (role) => connectionRoles.has(currentConnectionId(), role);
209
+ const connectionOrigin = () => connectionRoles.isDelegated(currentConnectionId()) ? "delegated" : "local";
204
210
  const routingSessionId = options.sessionId;
205
211
  // True only while THIS connection's own command drives a replacement; the issuer
206
212
  // already learns the new identity from its command response.
@@ -449,7 +455,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
449
455
  ? questions.ask(request, opts)
450
456
  : degradeQuestion(createExtensionUIContext(), request, opts),
451
457
  select: (title, options, opts) => createDialogPromise(opts, undefined, { method: "select", title, options, timeout: opts?.timeout, ...dialogCall(opts) }, (r) => ("cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined)),
452
- confirm: (title, message, opts) => createDialogPromise(opts, false, { method: "confirm", title, message, timeout: opts?.timeout }, (r) => "cancelled" in r && r.cancelled ? false : "confirmed" in r ? r.confirmed : false),
458
+ confirm: (title, message, opts) => createDialogPromise(opts, false, { method: "confirm", title, message, timeout: opts?.timeout, ...dialogCall(opts) }, (r) => ("cancelled" in r && r.cancelled ? false : "confirmed" in r ? r.confirmed : false)),
453
459
  input: (title, placeholder, opts) => opts?.secret
454
460
  ? secretInputs.input(title, placeholder, opts)
455
461
  : createDialogPromise(opts, undefined, { method: "input", title, placeholder, timeout: opts?.timeout, ...dialogCall(opts) }, (r) => ("cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined)),
@@ -564,7 +570,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
564
570
  // Host should track editor state locally if needed
565
571
  return "";
566
572
  },
567
- async editor(title, prefill) {
573
+ async editor(title, prefill, opts) {
568
574
  const id = crypto.randomUUID();
569
575
  const frame = {
570
576
  type: "extension_ui_request",
@@ -572,6 +578,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
572
578
  method: "editor",
573
579
  title,
574
580
  prefill,
581
+ ...dialogCall(opts),
575
582
  ...remoteOriginField(raiserOrigin()),
576
583
  };
577
584
  return new Promise((resolve, reject) => {
@@ -1000,6 +1007,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
1000
1007
  // Handle a single command
1001
1008
  const handleCommand = async (command, requestContext) => {
1002
1009
  const id = command.id;
1010
+ noteDelegatedRecord(command, connectionOrigin(), session.sessionManager);
1003
1011
  switch (command.type) {
1004
1012
  case "get_protocol_info":
1005
1013
  return {
@@ -1033,6 +1041,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
1033
1041
  SESSION_MODEL_SCOPE_CAPABILITY,
1034
1042
  RESUME_SESSION_CAPABILITY,
1035
1043
  ENTRIES_TAIL_CAPABILITY,
1044
+ SET_PERMISSION_PRESET_CAPABILITY,
1036
1045
  ...(options.retryFallbackCommand ? [RETRY_FALLBACK_COMMAND_CAPABILITY] : []),
1037
1046
  ...(options.toolRuntime ? toolRuntimeCapabilities(options.toolRuntime) : []),
1038
1047
  // Client flags cannot advertise a host guard this classic binding does not run, nor a tool runtime.
@@ -1055,7 +1064,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
1055
1064
  if (holdAutomaticInput(session, command, hasRole(INTERACTIVE_USER_CAPABILITY)))
1056
1065
  return success(id, "prompt", { disposition: "handled", heldAsNotice: true });
1057
1066
  turnRequested = true;
1058
- const admission = handleClientInput(session, command, { output, promptCalls }, { interactiveUser: hasRole(INTERACTIVE_USER_CAPABILITY) });
1067
+ const admission = handleClientInput(session, command, { output, promptCalls }, { interactiveUser: hasRole(INTERACTIVE_USER_CAPABILITY), connectionOrigin: connectionOrigin() });
1059
1068
  promptCalls.add(admission);
1060
1069
  void admission.finally(() => promptCalls.delete(admission));
1061
1070
  return undefined;
@@ -1125,7 +1134,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
1125
1134
  if (holdAutomaticInput(session, command, hasRole(INTERACTIVE_USER_CAPABILITY)))
1126
1135
  return success(id, command.type, { disposition: "handled", heldAsNotice: true });
1127
1136
  turnRequested = true;
1128
- await handleClientInput(session, command, { output, promptCalls }, { interactiveUser: hasRole(INTERACTIVE_USER_CAPABILITY) });
1137
+ await handleClientInput(session, command, { output, promptCalls }, { interactiveUser: hasRole(INTERACTIVE_USER_CAPABILITY), connectionOrigin: connectionOrigin() });
1129
1138
  return undefined;
1130
1139
  }
1131
1140
  case "abort":
@@ -1289,6 +1298,8 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
1289
1298
  // =================================================================
1290
1299
  case "set_thinking_level":
1291
1300
  return runSetThinkingLevel(effectContext(), command);
1301
+ case "set_permission_preset":
1302
+ return runSetPermissionPreset(runtimeHost, command);
1292
1303
  case "cycle_thinking_level": {
1293
1304
  const level = session.cycleThinkingLevel();
1294
1305
  if (!level) {
@@ -34,6 +34,8 @@ export declare class ConnectionRoles {
34
34
  private readonly origins;
35
35
  /** A worker isolate's first-seen delegation per connection (`retain`), kept for the worker's life. */
36
36
  private readonly retained;
37
+ /** A worker isolate's connections whose bind or command carried a delegation (a legacy one is never retained). */
38
+ private readonly carried;
37
39
  private source;
38
40
  /** The host reads each connection's live record from its writer's delegations. */
39
41
  useDelegations(source: (connectionId: string) => DelegatedConnectionView | undefined): void;
@@ -59,11 +61,15 @@ export declare class ConnectionRoles {
59
61
  declareOrigin(connectionId: string, origin: RpcRemoteOrigin): void;
60
62
  /** The remote device this connection speaks for: a delegated connection's principal, else its declared origin. */
61
63
  originOf(connectionId: string | undefined): RpcRemoteOrigin | undefined;
64
+ /** Whether the connection is delegated (a remote device's): on the host, retained by a worker, or carried to one. */
65
+ isDelegated(connectionId: string | undefined): boolean;
62
66
  has(connectionId: string | undefined, role: string): boolean;
63
67
  /** A logical detach: drops claims of earlier incarnations of a delegated socket, and everything of a local one. */
64
68
  release(connectionId: string): void;
65
69
  /** The socket is gone: nothing of it is kept. */
66
70
  forget(connectionId: string): void;
71
+ /** Worker isolate: a bind or command for `connectionId` carried a delegation, in either mode. */
72
+ noteCarriedDelegation(connectionId: string): void;
67
73
  /** Forgets every connection, for a worker isolate whose session ends. */
68
74
  clear(): void;
69
75
  }
@@ -35,6 +35,8 @@ export class ConnectionRoles {
35
35
  this.origins = new Map();
36
36
  /** A worker isolate's first-seen delegation per connection (`retain`), kept for the worker's life. */
37
37
  this.retained = new Map();
38
+ /** A worker isolate's connections whose bind or command carried a delegation (a legacy one is never retained). */
39
+ this.carried = new Set();
38
40
  }
39
41
  /** The host reads each connection's live record from its writer's delegations. */
40
42
  useDelegations(source) {
@@ -125,6 +127,11 @@ export class ConnectionRoles {
125
127
  ? { deviceId: principal.deviceId }
126
128
  : { deviceId: principal.deviceId, deviceName: principal.deviceName };
127
129
  }
130
+ /** Whether the connection is delegated (a remote device's): on the host, retained by a worker, or carried to one. */
131
+ isDelegated(connectionId) {
132
+ const key = roleConnection(connectionId);
133
+ return this.viewOf(key) !== undefined || this.carried.has(key);
134
+ }
128
135
  has(connectionId, role) {
129
136
  const key = roleConnection(connectionId);
130
137
  const store = commandAuthority.getStore();
@@ -155,12 +162,18 @@ export class ConnectionRoles {
155
162
  this.claims.delete(connectionId);
156
163
  this.origins.delete(connectionId);
157
164
  this.retained.delete(connectionId);
165
+ this.carried.delete(connectionId);
166
+ }
167
+ /** Worker isolate: a bind or command for `connectionId` carried a delegation, in either mode. */
168
+ noteCarriedDelegation(connectionId) {
169
+ this.carried.add(connectionId);
158
170
  }
159
171
  /** Forgets every connection, for a worker isolate whose session ends. */
160
172
  clear() {
161
173
  this.claims.clear();
162
174
  this.origins.clear();
163
175
  this.retained.clear();
176
+ this.carried.clear();
164
177
  }
165
178
  }
166
179
  //# sourceMappingURL=connection-roles.js.map
@@ -0,0 +1,9 @@
1
+ import type { SessionManager } from "../../core/session-manager.ts";
2
+ import { type ConnectionOrigin } from "../../core/turn-origin.ts";
3
+ /**
4
+ * Marks a session as remote-driven when a delegated connection sends it any record that can act on its work, decided
5
+ * by the engine's own method-class table (`drive`, and `device_admin`, which grants more), never by a hand-kept list of
6
+ * commands, so a new run starter (`continue_from_leaf`, `wake`, `resume_for_run`, ...) cannot slip past the rule.
7
+ */
8
+ export declare function noteDelegatedRecord(command: object, origin: ConnectionOrigin, sessionManager: Pick<SessionManager, "getEntries" | "appendCustomEntry">): void;
9
+ //# sourceMappingURL=delegated-drive-origin.d.ts.map
@@ -0,0 +1,16 @@
1
+ import { recordDelegatedOrigin } from "../../core/turn-origin.js";
2
+ import { classifyRpcMethod } from "./method-classes.js";
3
+ const ACTING_CLASSES = new Set(["drive", "device_admin"]);
4
+ /**
5
+ * Marks a session as remote-driven when a delegated connection sends it any record that can act on its work, decided
6
+ * by the engine's own method-class table (`drive`, and `device_admin`, which grants more), never by a hand-kept list of
7
+ * commands, so a new run starter (`continue_from_leaf`, `wake`, `resume_for_run`, ...) cannot slip past the rule.
8
+ */
9
+ export function noteDelegatedRecord(command, origin, sessionManager) {
10
+ if (origin !== "delegated")
11
+ return;
12
+ const methodClass = classifyRpcMethod(command);
13
+ if (methodClass !== undefined && ACTING_CLASSES.has(methodClass))
14
+ recordDelegatedOrigin(sessionManager);
15
+ }
16
+ //# sourceMappingURL=delegated-drive-origin.js.map
@@ -430,6 +430,12 @@ export declare function runSetThinkingLevel(ctx: EffectCommandContext, command:
430
430
  type: "response";
431
431
  command: "set_auto_compaction";
432
432
  success: true;
433
+ } | {
434
+ id?: string;
435
+ type: "response";
436
+ command: "set_permission_preset";
437
+ success: true;
438
+ data: import("../../core/permission-mode.ts").PermissionModeReply;
433
439
  } | {
434
440
  id?: string;
435
441
  type: "response";
@@ -0,0 +1,7 @@
1
+ export interface BareEndpointRegistration {
2
+ readonly agentDir: string;
3
+ readonly socket: string;
4
+ readonly log: (message: string) => void;
5
+ }
6
+ export declare function bindRegisteredEndpoint<T>(registration: BareEndpointRegistration, bind: () => Promise<T>): Promise<T>;
7
+ //# sourceMappingURL=host-bare-endpoint-registration.d.ts.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The registry record of a socket host that binds its own public socket (`--listen`, `--headless`). No
3
+ * `host ensure` runs for such a host, so nothing else writes its `endpoint.json`, and `host gc` removes the
4
+ * record with the rest of a dead endpoint's directory: after one outage the restarted host would serve
5
+ * while every registry reader (`host status --all`, a relay device service) finds nothing (senpi#6190).
6
+ *
7
+ * So the host writes the record itself before it serves, and binds inside the endpoint's ensure lock, the
8
+ * lock `host gc` judges and removes under: a gc that judged the socket dead has finished its removal
9
+ * before this write, and one that starts later finds the socket bound and keeps the endpoint. A supervised
10
+ * host never comes here - its ensure wrote the record and holds that lock while it starts the host.
11
+ */
12
+ import { createDaemonDirectories, createHostDaemonPaths } from "./host-daemon-paths.js";
13
+ import { acquireHostEnsureLock } from "./host-ensure-lock.js";
14
+ /** Longer than a gc's hold of one endpoint (its 2 s lock wait, the evidence probes, the removal). */
15
+ const REGISTRATION_LOCK_WAIT_MS = 10_000;
16
+ export async function bindRegisteredEndpoint(registration, bind) {
17
+ // An abstract socket has no path, so it has no daemon directory either.
18
+ if (registration.socket.startsWith("\0"))
19
+ return bind();
20
+ const release = await acquireHostEnsureLock(registration.socket, REGISTRATION_LOCK_WAIT_MS).catch((cause) => {
21
+ registration.log(`endpoint ensure lock unavailable, registering without it: ${messageOf(cause)}`);
22
+ return undefined;
23
+ });
24
+ try {
25
+ await createDaemonDirectories(createHostDaemonPaths({ socket: registration.socket, agentDir: registration.agentDir })).catch((cause) => registration.log(`endpoint registry record not written: ${messageOf(cause)}`));
26
+ return await bind();
27
+ }
28
+ finally {
29
+ await release?.();
30
+ }
31
+ }
32
+ function messageOf(cause) {
33
+ return cause instanceof Error ? cause.message : String(cause);
34
+ }
35
+ //# sourceMappingURL=host-bare-endpoint-registration.js.map
@@ -5,6 +5,8 @@ export declare const UNLOAD_SESSION_CAPABILITY = "unload_session";
5
5
  export declare const SUBSCRIPTION_LIMITS_CAPABILITY = "subscription_limits";
6
6
  /** `resolve_path`: which file a message's path means, the engine's one rule (senpi#3415). */
7
7
  export declare const RESOLVE_PATH_CAPABILITY = "resolve_path";
8
+ /** `set_permission_preset`: a live session's approval mode can be set and is echoed back (classic and multi-session). */
9
+ export declare const SET_PERMISSION_PRESET_CAPABILITY = "set_permission_preset_v1";
8
10
  /**
9
11
  * The capabilities a multi-session host advertises in `get_protocol_info`, in their wire order. `warm` is added only
10
12
  * when the runtime is in-process, `tool_runtime:<kind>` names where heavy tool work runs, and `negotiated` carries
@@ -13,6 +13,8 @@ export const UNLOAD_SESSION_CAPABILITY = "unload_session";
13
13
  export const SUBSCRIPTION_LIMITS_CAPABILITY = "subscription_limits";
14
14
  /** `resolve_path`: which file a message's path means, the engine's one rule (senpi#3415). */
15
15
  export const RESOLVE_PATH_CAPABILITY = "resolve_path";
16
+ /** `set_permission_preset`: a live session's approval mode can be set and is echoed back (classic and multi-session). */
17
+ export const SET_PERMISSION_PRESET_CAPABILITY = "set_permission_preset_v1";
16
18
  /**
17
19
  * The capabilities a multi-session host advertises in `get_protocol_info`, in their wire order. `warm` is added only
18
20
  * when the runtime is in-process, `tool_runtime:<kind>` names where heavy tool work runs, and `negotiated` carries
@@ -56,6 +58,7 @@ export function multiSessionHostCapabilities(options) {
56
58
  // `get_state`/`get_entries` marked `observe` never attach, and history reads page backward too (senpi#4267).
57
59
  OBSERVE_READS_CAPABILITY,
58
60
  ENTRIES_TAIL_CAPABILITY,
61
+ SET_PERMISSION_PRESET_CAPABILITY,
59
62
  // Host capabilities, not client opt-ins: only a multi-session host owns the
60
63
  // attachment refcount `open_session.retain_on_disconnect` detaches from, the
61
64
  // per-session launch profile `context`/`auto_title` travel on, and the session
@@ -4,6 +4,7 @@ import type { PromptSurface } from "../../core/dynamic-prompt/types.ts";
4
4
  import type { SessionContext, SessionKind } from "../../core/extensions/types.ts";
5
5
  import { type SessionEnv } from "./session-caller-env.ts";
6
6
  import { type SessionContextOverlay } from "./session-context-patch.ts";
7
+ import { type ResidentWriter } from "./host-handover-resident-writer.ts";
7
8
  export interface HandoverParkedEntry {
8
9
  readonly sessionPath: string;
9
10
  readonly durableSessionId?: string;
@@ -41,9 +42,19 @@ export interface HandoverParkedEntry {
41
42
  * Absent on entries an older generation wrote, which every reader treats as a predecessor's park.
42
43
  */
43
44
  readonly parkedBy?: string;
45
+ /**
46
+ * Present while the session is open on its host (senpi#6190); a park drops it. A successor reopens a stamped
47
+ * entry only once that writer process is gone, so it never opens a session its live predecessor still holds.
48
+ */
49
+ readonly residentWriter?: ResidentWriter;
44
50
  }
45
51
  export declare function recordHandoverParkedSession(daemonDir: string, entry: HandoverParkedEntry): Promise<void>;
46
52
  export declare function readHandoverParkedSessions(daemonDir: string): Promise<readonly HandoverParkedEntry[]>;
47
53
  export declare function clearHandoverParkedSession(daemonDir: string, sessionPath: string): Promise<void>;
54
+ /**
55
+ * Clears an inherited entry after its reopen, unless the reopen re-recorded it as resident on
56
+ * `holder` (a retained session is recorded by the host it is open on, so the next start reopens it).
57
+ */
58
+ export declare function clearInheritedParkedSession(daemonDir: string, sessionPath: string, holder: string | undefined): Promise<void>;
48
59
  export declare function dropHandoverParkedSessionIfFileGone(daemonDir: string, sessionPath: string): Promise<void>;
49
60
  //# sourceMappingURL=host-handover-parked.d.ts.map
@@ -21,6 +21,7 @@ import { parsePermissionPresetName } from "../../core/extensions/builtin/permiss
21
21
  import { sessionAutoTitleError, sessionBrowserEngineError, sessionContextError, sessionKindError, sessionPromptSurfaceError, sessionRetryFallbackError, } from "./rpc-input-validation.js";
22
22
  import { sessionEnvShapeError } from "./session-caller-env.js";
23
23
  import { applyContextPatch } from "./session-context-patch.js";
24
+ import { isResidentWriter } from "./host-handover-resident-writer.js";
24
25
  const recordDir = (daemonDir) => join(daemonDir, "handover-parked");
25
26
  const entryFile = (daemonDir, sessionPath) => join(recordDir(daemonDir), `${createHash("sha256").update(sessionPath, "utf8").digest("hex").slice(0, 16)}.json`);
26
27
  let mutationChain = Promise.resolve();
@@ -38,6 +39,7 @@ function isEntry(value) {
38
39
  (record.durableSessionId === undefined || typeof record.durableSessionId === "string") &&
39
40
  (record.cwd === undefined || typeof record.cwd === "string") &&
40
41
  (record.parkedBy === undefined || typeof record.parkedBy === "string") &&
42
+ (record.residentWriter === undefined || isResidentWriter(record.residentWriter)) &&
41
43
  (record.thinkingLevel === undefined || typeof record.thinkingLevel === "string") &&
42
44
  (record.permissionPreset === undefined ||
43
45
  (typeof record.permissionPreset === "string" &&
@@ -100,6 +102,21 @@ export async function readHandoverParkedSessions(daemonDir) {
100
102
  export function clearHandoverParkedSession(daemonDir, sessionPath) {
101
103
  return enqueueMutation(() => rm(entryFile(daemonDir, sessionPath), { force: true }));
102
104
  }
105
+ /**
106
+ * Clears an inherited entry after its reopen, unless the reopen re-recorded it as resident on
107
+ * `holder` (a retained session is recorded by the host it is open on, so the next start reopens it).
108
+ */
109
+ export function clearInheritedParkedSession(daemonDir, sessionPath, holder) {
110
+ return enqueueMutation(async () => {
111
+ const file = entryFile(daemonDir, sessionPath);
112
+ if (holder !== undefined) {
113
+ const recorded = await readFile(file, "utf8").then((text) => JSON.parse(text), () => undefined);
114
+ if (isEntry(recorded) && recorded.parkedBy === holder)
115
+ return;
116
+ }
117
+ await rm(file, { force: true });
118
+ });
119
+ }
103
120
  export function dropHandoverParkedSessionIfFileGone(daemonDir, sessionPath) {
104
121
  if (existsSync(sessionPath))
105
122
  return Promise.resolve();
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { createHostDaemonPaths } from "./host-daemon-paths.js";
3
- import { clearHandoverParkedSession, dropHandoverParkedSessionIfFileGone, readHandoverParkedSessions, } from "./host-handover-parked.js";
3
+ import { clearHandoverParkedSession, clearInheritedParkedSession, dropHandoverParkedSessionIfFileGone, readHandoverParkedSessions, } from "./host-handover-parked.js";
4
+ import { residentWriterProbe } from "./host-handover-resident-writer.js";
4
5
  import { RpcSessionRegistryError } from "./session-registry.js";
5
6
  // The watch budget must outlast a heavily loaded drain: a predecessor whose event loop is blocked
6
7
  // (parallel hosts, a busy turn settling) can take tens of seconds to write its park after this
@@ -37,6 +38,7 @@ export async function reopenHandoverParkedSessions(open, agentDir, socket, holde
37
38
  const deadline = Date.now() + HANDOVER_PARKED_REOPEN_BUDGET_MS;
38
39
  const reopenAttempts = new Map();
39
40
  const heldPolls = new Map();
41
+ const writerAlive = residentWriterProbe();
40
42
  let emptyStreak = 0;
41
43
  let sawAnyPark = false;
42
44
  for (;;) {
@@ -46,12 +48,12 @@ export async function reopenHandoverParkedSessions(open, agentDir, socket, holde
46
48
  // it by path with the same durable id.
47
49
  if (holder?.shouldReopen !== undefined && !holder.shouldReopen())
48
50
  return;
49
- const parked = await inheritedParks(daemonDir, holder);
51
+ const parked = await inheritedParks(daemonDir, holder, writerAlive);
50
52
  for (const entry of parked) {
51
53
  sawAnyPark = true;
52
54
  await reopenOne(daemonDir, open, entry, reopenAttempts, heldPolls, holder);
53
55
  }
54
- const remaining = (await inheritedParks(daemonDir, holder)).length;
56
+ const remaining = (await inheritedParks(daemonDir, holder, writerAlive)).length;
55
57
  if (holder?.shouldReopen !== undefined && !holder.shouldReopen())
56
58
  return;
57
59
  emptyStreak = remaining === 0 ? emptyStreak + 1 : 0;
@@ -67,9 +69,19 @@ export async function reopenHandoverParkedSessions(open, agentDir, socket, holde
67
69
  * parks retained sessions into the same record) is for its successor: reopening it here would put a
68
70
  * second open of the path in flight against the client reopening it (senpi#3485).
69
71
  */
70
- async function inheritedParks(daemonDir, holder) {
72
+ async function inheritedParks(daemonDir, holder, writerAlive) {
71
73
  const parked = await readHandoverParkedSessions(daemonDir);
72
- return parked.filter((entry) => entry.parkedBy === undefined || entry.parkedBy !== holder?.hostInstance);
74
+ const inherited = [];
75
+ for (const entry of parked) {
76
+ if (entry.parkedBy !== undefined && entry.parkedBy === holder?.hostInstance)
77
+ continue;
78
+ // A session still resident in its live writer (a handoff's draining predecessor) is that process's to park,
79
+ // and its unstamped park is what this host reopens; only a writer that is gone leaves it here to restore.
80
+ if (entry.residentWriter !== undefined && (await writerAlive(entry.residentWriter)))
81
+ continue;
82
+ inherited.push(entry);
83
+ }
84
+ return inherited;
73
85
  }
74
86
  async function reopenOne(daemonDir, open, entry, attempts, heldPolls, holder) {
75
87
  const taskStatus = entry.contextOverlay && Object.hasOwn(entry.contextOverlay, "task_status")
@@ -93,7 +105,7 @@ async function reopenOne(daemonDir, open, entry, attempts, heldPolls, holder) {
93
105
  // reopens it; clearing it here would lose the session when the drain closes it (senpi#3282).
94
106
  if (holder?.shouldReopen !== undefined && !holder.shouldReopen())
95
107
  return;
96
- await clearHandoverParkedSession(daemonDir, entry.sessionPath);
108
+ await clearInheritedParkedSession(daemonDir, entry.sessionPath, holder?.hostInstance);
97
109
  }
98
110
  catch (cause) {
99
111
  // A `session_path_in_use` refusal means the predecessor still holds the path's cross-generation
@@ -0,0 +1,16 @@
1
+ import { type WriterProbes } from "../../core/session-writer-claims.ts";
2
+ export interface ResidentWriter {
3
+ readonly host: string;
4
+ readonly pid: number;
5
+ readonly bootAtMs: number;
6
+ readonly processStartedAtMs: number;
7
+ }
8
+ export declare function currentResidentWriter(probes?: WriterProbes): ResidentWriter;
9
+ export declare function isResidentWriter(value: unknown): value is ResidentWriter;
10
+ /**
11
+ * A liveness probe for one reopen watch, which polls: a writer found gone stays gone, and a live pid's start time
12
+ * (a process spawn on some platforms) is read once. A writer on another host cannot be probed from here, so it is
13
+ * never displaced.
14
+ */
15
+ export declare function residentWriterProbe(probes?: WriterProbes): (writer: ResidentWriter) => Promise<boolean>;
16
+ //# sourceMappingURL=host-handover-resident-writer.d.ts.map