@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
@@ -1,5 +1,5 @@
1
1
  import { type OpenAcceptance } from "./compact-open-admission.ts";
2
- import { type HandoverParkedEntry } from "./host-handover-parked.ts";
2
+ import type { HandoverParkedEntry } from "./host-handover-parked.ts";
3
3
  import type { RpcCommand, RpcResponse } from "./rpc-types.ts";
4
4
  import type { RouterContext } from "./session-router-context.ts";
5
5
  export declare function openWithBarrier(host: RouterContext, command: Extract<RpcCommand, {
@@ -1,10 +1,10 @@
1
1
  import { buildRpcSessionState } from "./connection-handler.js";
2
2
  import { compactUpdatesEnabled } from "../../core/engine-socket-flag.js";
3
3
  import { startOpenTimer } from "../../core/open-timing.js";
4
+ import { openReplyPermissionMode } from "../../core/permission-mode.js";
4
5
  import { compactOpen } from "./compact-open-admission.js";
5
6
  import { CompactAdmissionError } from "./compact-admission-error.js";
6
7
  import { runAdmissionProducer } from "./compact-admission-producer.js";
7
- import { clearHandoverParkedSession } from "./host-handover-parked.js";
8
8
  import { parkedOpenCommand } from "./host-handover-parked-open.js";
9
9
  import { sessionHeldCheck } from "./session-held.js";
10
10
  import { forgetSessionOwnership, releaseOwnedSession, releaseOwnerAttachment } from "./session-router-attachments.js";
@@ -13,6 +13,7 @@ import { code, detail, error } from "./session-router-errors.js";
13
13
  import { sweepDrain } from "./session-router-occupancy.js";
14
14
  import { invalidSpawnedBy, validatedOpenProfile } from "./session-router-open-profile.js";
15
15
  import { relocationConnection } from "./session-router-relocation.js";
16
+ import { syncRetainedRecord } from "./session-router-residency.js";
16
17
  import { liveToolMediaScope, toolMediaPersister } from "./tool-media-store.js";
17
18
  export function openWithBarrier(host, command) {
18
19
  if (compactUpdatesEnabled() && command.admissionId !== undefined)
@@ -48,22 +49,6 @@ function openPhysicalWithBarrier(host, command, accept, signal) {
48
49
  // A late open's physical session is released by its admission (the accept/rollback path, 4272-admission-
49
50
  // abandonment), so the late response itself owns nothing to release here.
50
51
  const opened = signal === undefined ? start() : runAdmissionProducer(signal, start, async () => { });
51
- // A manual reopen-by-path supersedes the handover auto-reopen for that session: clear its
52
- // record entry, or a later generation would reopen a second copy (MEDIUM-1, senpi#3282 review).
53
- // Cleared only after the open SUCCEEDS (MEDIUM-A): if the open fails - a session_path_in_use
54
- // during a handover, or a bad profile - the entry must stay so a later generation can still
55
- // reopen the session. open() resolves `undefined` on success (the response is written through
56
- // the writer) and an RpcResponse only on failure, so success is `response === undefined`.
57
- if (command.sessionPath !== undefined && host.handoverParkedDir !== undefined) {
58
- const dir = host.handoverParkedDir;
59
- const sessionPath = command.sessionPath;
60
- void opened.then((response) => {
61
- if (response === undefined)
62
- void clearHandoverParkedSession(dir, sessionPath).catch(() => { });
63
- }, () => {
64
- // A rejection means the open failed before producing a response; the entry stays.
65
- });
66
- }
67
52
  return opened.finally(() => {
68
53
  resolveBarrier();
69
54
  opens.delete(barrier);
@@ -214,6 +199,9 @@ export async function open(host, command, owner, options) {
214
199
  releaseOwnerAttachment(host, owner, openedSession.sessionId);
215
200
  await releaseOwnedSession(host, openedSession.sessionId);
216
201
  }
202
+ // After the open SUCCEEDS (MEDIUM-A): a failed open keeps any record entry, so a later start
203
+ // still reopens that session.
204
+ await syncRetainedRecord(host, openedSession.sessionId, command.sessionPath);
217
205
  if (options?.accept) {
218
206
  return undefined;
219
207
  }
@@ -230,6 +218,7 @@ export async function open(host, command, owner, options) {
230
218
  data: {
231
219
  sessionId: opened.sessionId,
232
220
  state: replyState,
221
+ ...openReplyPermissionMode(replyState),
233
222
  ...(opened.attached ? { attached: true } : {}),
234
223
  ...(host.defaults.sessionPresence
235
224
  ? { presence: host.defaults.sessionPresence.view(replyState.sessionId) }
@@ -0,0 +1,24 @@
1
+ import type { RouterContext } from "./session-router-context.ts";
2
+ import type { RpcSessionEntry } from "./session-registry-types.ts";
3
+ /**
4
+ * Writes the record entry of a retained session under `sessionPath`, stamped with this host instance, and drops
5
+ * the entry it was recorded under before when the session has since moved to another transcript (senpi#6190).
6
+ * `resident` stamps the writing process: the session is open here. A park writes it unstamped.
7
+ */
8
+ export declare function recordRetainedSession(host: RouterContext, retained: RpcSessionEntry, sessionPath: string, resident: boolean): Promise<void>;
9
+ /**
10
+ * Brings the record in line with a session this host just opened or attached: a retained session is
11
+ * recorded as resident here, so a restart reopens it; a non-retained open by path supersedes any
12
+ * earlier park of that path, whose entry is cleared so no later start reopens a second copy
13
+ * (MEDIUM-1, senpi#3282 review). Runs only after the open succeeded, so a failed open keeps the entry.
14
+ */
15
+ export declare function syncRetainedRecord(host: RouterContext, sessionId: string, requestedPath: string | undefined): Promise<void>;
16
+ /**
17
+ * Called after every record a session publishes: a retained session whose transcript was switched (`new_session`,
18
+ * `switch_session`, a fork) moves its record entry to the new file at once, so a crash restores the transcript the
19
+ * session is on, never the one it left. A string compare until the path actually moves.
20
+ */
21
+ export declare function followRetainedRecord(host: RouterContext, sessionId: string): void;
22
+ /** An explicit close or reclaim ends the session for good: drop every key its record was written under. */
23
+ export declare function clearRetainedRecord(host: RouterContext, entry: RpcSessionEntry | undefined): Promise<void>;
24
+ //# sourceMappingURL=session-router-residency.d.ts.map
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The retained-session record a host keeps for the NEXT start of its endpoint: one per-session
3
+ * entry in the handover-parked record (`host-handover-parked.ts`), written when a session becomes
4
+ * retained-and-resident here, moved when the session switches to another transcript, and rewritten
5
+ * when it parks. A restart on the same socket and agent dir - after a handoff, a crash, or a kill
6
+ * mid-turn - reopens every entry a previous process wrote by its session file, the way a TUI resumes
7
+ * from its session file, so `list_sessions` (and the relay that mirrors it) lists the session again
8
+ * without anyone re-opening it by hand.
9
+ */
10
+ import { clearHandoverParkedSession, recordHandoverParkedSession } from "./host-handover-parked.js";
11
+ import { parkedEntryFromProfile } from "./host-handover-parked-open.js";
12
+ import { currentResidentWriter } from "./host-handover-resident-writer.js";
13
+ import { hostInstanceId } from "./protocol-identity.js";
14
+ const reportRecordFailure = (sessionId) => (cause) => {
15
+ process.stderr.write(`senpi rpc retained session record ${sessionId} failed: ${String(cause)}\n`);
16
+ };
17
+ /**
18
+ * Writes the record entry of a retained session under `sessionPath`, stamped with this host instance, and drops
19
+ * the entry it was recorded under before when the session has since moved to another transcript (senpi#6190).
20
+ * `resident` stamps the writing process: the session is open here. A park writes it unstamped.
21
+ */
22
+ export async function recordRetainedSession(host, retained, sessionPath, resident) {
23
+ const dir = host.handoverParkedDir;
24
+ if (dir === undefined)
25
+ return;
26
+ const previous = retained.recordedPath;
27
+ retained.recordedPath = sessionPath;
28
+ if (previous !== undefined && previous !== sessionPath)
29
+ await clearHandoverParkedSession(dir, previous);
30
+ const durableSessionId = retained.runtime?.session.sessionId ?? retained.durableSessionId;
31
+ await recordHandoverParkedSession(dir, {
32
+ ...parkedEntryFromProfile({
33
+ sessionPath,
34
+ contextOverlay: retained.contextOverlay,
35
+ ...(durableSessionId !== undefined ? { durableSessionId } : {}),
36
+ }, retained.profile, Object.keys(host.defaults.hostContext ?? {})),
37
+ parkedBy: host.defaults.hostContext?.host_instance ?? hostInstanceId(),
38
+ ...(resident ? { residentWriter: currentResidentWriter() } : {}),
39
+ });
40
+ }
41
+ /** The session path a live entry writes now; a replacement moves it before the registry's metadata catches up. */
42
+ const currentPath = (entry) => entry.runtime?.session.sessionFile ?? entry.sessionPath;
43
+ /**
44
+ * Brings the record in line with a session this host just opened or attached: a retained session is
45
+ * recorded as resident here, so a restart reopens it; a non-retained open by path supersedes any
46
+ * earlier park of that path, whose entry is cleared so no later start reopens a second copy
47
+ * (MEDIUM-1, senpi#3282 review). Runs only after the open succeeded, so a failed open keeps the entry.
48
+ */
49
+ export async function syncRetainedRecord(host, sessionId, requestedPath) {
50
+ const dir = host.handoverParkedDir;
51
+ if (dir === undefined)
52
+ return;
53
+ const entry = host.registry.peek(sessionId);
54
+ const sessionPath = entry === undefined ? undefined : currentPath(entry);
55
+ if (entry?.retainOnDisconnect === true && sessionPath !== undefined) {
56
+ await recordRetainedSession(host, entry, sessionPath, true).catch(reportRecordFailure(sessionId));
57
+ if (requestedPath === undefined || requestedPath === sessionPath)
58
+ return;
59
+ }
60
+ if (requestedPath !== undefined)
61
+ await clearHandoverParkedSession(dir, requestedPath).catch(() => { });
62
+ }
63
+ /**
64
+ * Called after every record a session publishes: a retained session whose transcript was switched (`new_session`,
65
+ * `switch_session`, a fork) moves its record entry to the new file at once, so a crash restores the transcript the
66
+ * session is on, never the one it left. A string compare until the path actually moves.
67
+ */
68
+ export function followRetainedRecord(host, sessionId) {
69
+ if (host.handoverParkedDir === undefined)
70
+ return;
71
+ const entry = host.registry.peek(sessionId);
72
+ if (entry?.retainOnDisconnect !== true || entry.recordedPath === undefined || entry.state !== "open")
73
+ return;
74
+ const sessionPath = currentPath(entry);
75
+ if (sessionPath === undefined || sessionPath === entry.recordedPath)
76
+ return;
77
+ void recordRetainedSession(host, entry, sessionPath, true).catch(reportRecordFailure(sessionId));
78
+ }
79
+ /** An explicit close or reclaim ends the session for good: drop every key its record was written under. */
80
+ export async function clearRetainedRecord(host, entry) {
81
+ const dir = host.handoverParkedDir;
82
+ if (dir === undefined || entry === undefined)
83
+ return;
84
+ const keys = new Set([entry.recordedPath, entry.sessionPath, entry.runtime?.session.sessionFile]);
85
+ entry.recordedPath = undefined;
86
+ for (const key of keys)
87
+ if (key !== undefined)
88
+ await clearHandoverParkedSession(dir, key).catch(() => { });
89
+ }
90
+ //# sourceMappingURL=session-router-residency.js.map
@@ -24,6 +24,7 @@ import { answerListSessions, answerProtocolInfo, answerSubscriptionLimits } from
24
24
  import { acknowledgePrompt, otherRequestsSettled } from "./session-router-observation.js";
25
25
  import { sweepDrain } from "./session-router-occupancy.js";
26
26
  import { openWithBarrier } from "./session-router-open.js";
27
+ import { syncRetainedRecord } from "./session-router-residency.js";
27
28
  import { answerSetSessionContext, answerSubscribeSessions, answerUnsubscribeSessions, notAttachedRefusal, } from "./session-status-commands.js";
28
29
  import { unloadSession } from "./session-unload.js";
29
30
  /**
@@ -245,7 +246,12 @@ export function setSessionContext(host, command) {
245
246
  return notAttachedRefusal(command);
246
247
  try {
247
248
  const entry = host.registry.getForCommand(command.sessionId, command.type);
248
- return answerSetSessionContext(command, entry, host.writer.status);
249
+ const answered = answerSetSessionContext(command, entry, host.writer.status);
250
+ // The record carries the overlay a restart reopens the session with (a terminal task_status
251
+ // is not reopened at all), so it follows each accepted patch.
252
+ if (answered.success)
253
+ void syncRetainedRecord(host, command.sessionId, undefined);
254
+ return answered;
249
255
  }
250
256
  catch (cause) {
251
257
  return error(command.id, command.type, code(cause), detail(cause));
@@ -57,7 +57,10 @@ export function deriveSessionStatus(entry, runtime, fanout, terminal, cause) {
57
57
  export function terminalOfLifecycle(record) {
58
58
  const reason = typeof record.reason === "string" ? record.reason : undefined;
59
59
  if (record.type === "session_parked")
60
- return { status: "unloaded", reason: reason === "unloaded" ? "unload_session" : "idle" };
60
+ return {
61
+ status: "unloaded",
62
+ reason: reason === "unloaded" ? "unload_session" : reason === "host_shutdown" ? "host_shutdown" : "idle",
63
+ };
61
64
  if (record.type !== "session_closed")
62
65
  return undefined;
63
66
  if (reason === "handoff_parked")
@@ -278,6 +278,8 @@ async function handle(message) {
278
278
  const bound = roleConnection(message.connection);
279
279
  if (message.delegation !== undefined && connectionRoles.retain(bound, message.delegation).kind === "conflict")
280
280
  throw new Error(DELEGATION_AUTHORITY_CONFLICT);
281
+ if (message.delegation !== undefined)
282
+ connectionRoles.noteCarriedDelegation(bound);
281
283
  if (message.roles !== undefined)
282
284
  connectionRoles.adopt(bound, message.roles, message.delegation?.incarnation);
283
285
  const bindingEntry = entry;
@@ -336,6 +338,8 @@ async function handle(message) {
336
338
  const resolved = message.delegation === undefined ? undefined : connectionRoles.retain(connection, message.delegation);
337
339
  if (resolved?.kind !== "conflict" && message.roles !== undefined)
338
340
  connectionRoles.adopt(connection, message.roles, message.delegation?.incarnation);
341
+ if (message.delegation !== undefined)
342
+ connectionRoles.noteCarriedDelegation(connection);
339
343
  if (message.origin !== undefined)
340
344
  connectionRoles.declareOrigin(connection, message.origin);
341
345
  const activeBinding = binding;
@@ -1,3 +1,4 @@
1
+ import { clientNamedPreset, withClientPreset } from "../../core/permission-mode.js";
1
2
  import { outsideCommand } from "./command-authority.js";
2
3
  import { refreshesSessionActivity } from "./session-command-activity.js";
3
4
  import { publishedSessionContext } from "./session-context-patch.js";
@@ -163,7 +164,9 @@ export class WorkerSessionRegistry {
163
164
  return undefined;
164
165
  }
165
166
  async attach(owner, path, options, requested = { cwd: "" }) {
166
- const { promptSurface, browserEngine, permissionPreset } = requested;
167
+ const { promptSurface, browserEngine } = requested;
168
+ // A host-default preset folded into a preset-less attach is not a client choice (see session-registry-attach.ts).
169
+ const permissionPreset = clientNamedPreset(requested);
167
170
  const entry = this.entries.get(owner);
168
171
  if (entry?.state !== "open" || !entry.worker?.bindingReady || entry.worker.snapshot?.sessionPath !== path)
169
172
  throw new RpcSessionRegistryError("session_path_in_use");
@@ -178,7 +181,7 @@ export class WorkerSessionRegistry {
178
181
  }
179
182
  // Sent even when the record already names it: only the worker sees its live session (senpi#2842).
180
183
  if (permissionPreset !== undefined) {
181
- entry.profile = frozenProfile({ ...entry.profile, permissionPreset });
184
+ entry.profile = frozenProfile(withClientPreset(entry.profile, permissionPreset));
182
185
  await entry.worker.setPermissionPreset(permissionPreset);
183
186
  }
184
187
  const result = this.openResult(owner, entry);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "rpcClientRequests": "{\"type\":\"get_protocol_info\",\"id\":\"rpc-auth-protocol\",\"observe\":true,\"auth\":{\"client_nonce\":\"<public-nonce>\"}}\n{\"type\":\"set_client_info\",\"width\":100,\"capabilities\":[],\"id\":\"req_1\"}\n{\"type\":\"open_session\",\"cwd\":\"<root>/cwd\",\"sessionPath\":\"<root>/cwd/session.jsonl\",\"id\":\"req_2\"}\n{\"type\":\"get_entries\",\"sessionId\":\"rpc-1\",\"id\":\"req_3\"}\n{\"type\":\"close_session\",\"sessionId\":\"rpc-1\",\"id\":\"req_4\"}\n",
3
- "rpcClientFrames": "{\"id\":\"rpc-auth-protocol\",\"type\":\"response\",\"command\":\"get_protocol_info\",\"success\":true,\"data\":{\"protocolVersion\":1,\"serverVersion\":\"<server-version>\",\"capabilities\":[\"<capabilities>\"],\"mode\":\"multi\",\"instanceId\":\"<uuid-1>\",\"generation\":0,\"engineVersion\":\"<server-version>\",\"engineOrdinal\":\"<engine-ordinal>\",\"launch_profile\":{\"profile_id\":\"ca7a49435f02009fdda5dac49d29408dcfeb1ab5374353784c5a9def467f5be1\",\"core\":{\"extensions\":[],\"multi_session\":false,\"session_runtime\":\"worker\"}},\"agentDir\":\"<root>/agent\",\"method_classes\":{\"version\":1,\"digest\":\"3860136b2a1b07c20373ed361df07c045bd1e84fce001917e271ca0e4b903e63\",\"classes\":{\"get_protocol_info\":\"observe\",\"get_subscription_limits\":\"observe\",\"list_sessions\":\"observe\",\"list_hands\":\"observe\",\"get_state\":\"observe\",\"get_snapshot\":\"observe\",\"get_entries\":\"observe\",\"get_messages\":\"observe\",\"get_tree\":\"observe\",\"get_fork_messages\":\"observe\",\"get_last_assistant_text\":\"observe\",\"get_session_stats\":\"observe\",\"get_effect_status\":\"dynamic\",\"get_pending_inputs\":\"observe\",\"get_follow_up_messages\":\"observe\",\"get_steering_messages\":\"observe\",\"get_commands\":\"observe\",\"get_loaded_surfaces\":\"observe\",\"get_project_trust\":\"observe\",\"get_available_models\":\"observe\",\"get_available_thinking_levels\":\"observe\",\"get_model_thinking_level\":\"observe\",\"get_fast_mode\":\"observe\",\"get_media\":\"observe\",\"memory_report\":\"observe\",\"eval_cells.list\":\"observe\",\"eval_cells.peek\":\"observe\",\"prompt\":\"dynamic\",\"steer\":\"dynamic\",\"follow_up\":\"dynamic\",\"abort\":\"drive\",\"interrupt\":\"drive\",\"abort_retry\":\"drive\",\"abort_compaction\":\"drive\",\"abort_branch_summary\":\"drive\",\"compact\":\"drive\",\"clear_queue\":\"drive\",\"dequeue_message\":\"drive\",\"edit_queued_message\":\"drive\",\"hold_input\":\"drive\",\"withdraw_held_input\":\"drive\",\"append_user_message\":\"drive\",\"edit_user_message\":\"drive\",\"continue_from_leaf\":\"drive\",\"navigate_tree\":\"drive\",\"set_label\":\"drive\",\"set_session_name\":\"drive\",\"set_thinking_level\":\"dynamic\",\"set_model\":\"dynamic\",\"set_auto_compaction\":\"drive\",\"set_retry_fallback\":\"drive\",\"pause_for_run\":\"drive\",\"resume_for_run\":\"drive\",\"wake\":\"drive\",\"release_tool_barrier\":\"drive\",\"request_self_settle\":\"drive\",\"eval_cells.stop\":\"drive\",\"append_session_entry\":\"dynamic\",\"extension_ui_response\":\"answer\",\"extension_ui_progress\":\"answer\",\"new_session\":\"dynamic\",\"open_session\":\"dynamic\",\"resume_session\":\"thread\",\"fork\":\"thread\",\"clone\":\"thread\",\"close_session\":\"thread\",\"unload_session\":\"thread\",\"set_session_context\":\"thread\",\"attach_session\":\"thread\",\"select_hand\":\"device_admin\",\"bash\":\"device_admin\",\"abort_bash\":\"device_admin\",\"record_bash_result\":\"device_admin\",\"cleanup_bash_output\":\"device_admin\",\"extension_request\":\"device_admin\",\"reload\":\"device_admin\",\"check_reload_veto\":\"device_admin\",\"resolve_path\":\"device_admin\",\"login_start\":\"device_admin\",\"login_cancel\":\"device_admin\",\"login_api_key\":\"device_admin\",\"logout\":\"device_admin\",\"verify_api_key\":\"device_admin\",\"get_auth_providers\":\"device_admin\",\"get_provider_accounts\":\"device_admin\",\"account_order\":\"device_admin\",\"account_pin\":\"device_admin\",\"account_remove\":\"device_admin\",\"account_reset\":\"device_admin\",\"test_provider_account\":\"device_admin\",\"set_favorite_models\":\"device_admin\",\"set_scoped_models\":\"device_admin\",\"cycle_model\":\"device_admin\",\"cycle_thinking_level\":\"device_admin\",\"set_fast_mode\":\"device_admin\",\"set_steering_mode\":\"device_admin\",\"set_follow_up_mode\":\"device_admin\",\"set_auto_retry\":\"device_admin\",\"set_project_trust\":\"device_admin\",\"get_project_settings\":\"device_admin\",\"set_project_setting\":\"device_admin\",\"edit_assistant_message\":\"device_admin\",\"send_custom_message\":\"device_admin\",\"switch_session\":\"device_admin\",\"import_jsonl\":\"device_admin\",\"export_html\":\"device_admin\",\"export_jsonl\":\"device_admin\",\"set_client_info\":\"connection_local\",\"subscribe_sessions\":\"connection_local\",\"unsubscribe_sessions\":\"connection_local\",\"record_user_activity\":\"connection_local\",\"detach_connection\":\"connection_local\",\"release_session\":\"connection_local\",\"warm\":\"connection_local\",\"host_response\":\"connection_local\",\"release_attachment\":\"connection_local\",\"reclaim_session\":\"connection_local\",\"move_session\":\"connection_local\",\"get_recovery_page\":\"connection_local\",\"release_recovery\":\"connection_local\",\"cancel_attempt\":\"connection_local\",\"reset_stream\":\"connection_local\"},\"dynamic_versions\":{\"get_effect_status\":1,\"prompt\":1,\"steer\":1,\"follow_up\":1,\"set_thinking_level\":1,\"set_model\":1,\"append_session_entry\":1,\"new_session\":1,\"open_session\":2}},\"memory_pressure\":false,\"path_identity_resolver\":{\"state\":\"not_started\",\"detail\":null}}}\n{\"id\":\"req_1\",\"type\":\"response\",\"command\":\"set_client_info\",\"success\":true}\n{\"type\":\"queued\",\"for_request\":\"req_2\",\"position\":1,\"in_flight\":0}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-2>\",\"method\":\"setWidget\",\"widgetKey\":\"todo-sidebar\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-3>\",\"method\":\"setStatus\",\"statusKey\":\"anthropic-web-search\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-4>\",\"method\":\"setWidget\",\"widgetKey\":\"anthropic-web-search\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-5>\",\"method\":\"setStatus\",\"statusKey\":\"openai-web-search\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-6>\",\"method\":\"setWidget\",\"widgetKey\":\"openai-web-search\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-7>\",\"method\":\"setStatus\",\"statusKey\":\"monitors\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-8>\",\"method\":\"setStatus\",\"statusKey\":\"pi-websearch\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-9>\",\"method\":\"setWidget\",\"widgetKey\":\"pi-websearch\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-10>\",\"method\":\"setStatus\",\"statusKey\":\"pi-webfetch\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-11>\",\"method\":\"setWidget\",\"widgetKey\":\"pi-webfetch\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"entry_appended\",\"entry\":{\"type\":\"custom\",\"customType\":\"pi-rules.scan\",\"data\":{\"cwd\":\"<root>/cwd\",\"reason\":\"startup\"},\"startupBookkeeping\":true,\"id\":\"<entry-1>\",\"parentId\":\"<entry-2>\",\"timestamp\":\"2026-10-29T00:00:00.000Z\"},\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-12>\",\"method\":\"setStatus\",\"statusKey\":\"todo-waiting\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-13>\",\"method\":\"setStatus\",\"statusKey\":\"goal\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"session_opened\",\"sessionId\":\"rpc-1\",\"durableSessionId\":\"<uuid-14>\",\"sessionPath\":\"<root>/cwd/session.jsonl\",\"kind\":\"interactive\",\"cwd\":\"<root>/cwd\"}\n{\"id\":\"req_2\",\"type\":\"response\",\"command\":\"open_session\",\"success\":true,\"data\":{\"sessionId\":\"rpc-1\",\"state\":{\"answeredUiRequests\":[],\"paused\":false,\"pausedNoticeCount\":0,\"holds\":[],\"lifecycle\":{\"state\":\"done\",\"seq\":0,\"epoch\":0,\"epochOrigin\":\"automatic\",\"humanInputRevision\":0,\"selfSettleEligible\":true,\"restoring\":false,\"persisted\":false,\"runPaused\":false,\"since\":\"2026-10-29T00:00:00.000Z\",\"role\":\"root\",\"rootSessionId\":\"<uuid-14>\",\"activityRevision\":{\"instanceId\":\"<uuid-15>\",\"revision\":0},\"waitingOn\":{\"children\":0,\"background\":[],\"human\":[]},\"holds\":[],\"observedAt\":\"2026-10-29T00:00:00.000Z\"},\"pendingQuestions\":[],\"pendingDialogs\":[],\"model\":{\"id\":\"faux-a\",\"name\":\"faux-a\",\"api\":\"faux:fixture\",\"provider\":\"faux\",\"baseUrl\":\"http://localhost:0\",\"reasoning\":false,\"input\":[\"text\",\"image\"],\"cost\":{\"input\":0,\"output\":0,\"cacheRead\":0,\"cacheWrite\":0},\"contextWindow\":128000,\"maxTokens\":16384},\"pendingModelSwitch\":null,\"thinkingLevel\":\"off\",\"thinkingLevelSource\":\"default\",\"sessionModelThinkingLevels\":{\"faux/faux-a\":\"off\"},\"fastMode\":false,\"isStreaming\":false,\"pausedHeldInputCount\":0,\"runPaused\":false,\"pause\":{\"pauseSeq\":0,\"records\":[]},\"isCompacting\":false,\"retryAttempt\":0,\"isBashRunning\":false,\"steeringMode\":\"all\",\"followUpMode\":\"one-at-a-time\",\"sessionFile\":\"<root>/cwd/session.jsonl\",\"sessionId\":\"<uuid-14>\",\"cwd\":\"<root>/cwd\",\"projectTrusted\":false,\"entries\":[{\"type\":\"model_change\",\"id\":\"<entry-3>\",\"parentId\":null,\"timestamp\":\"2026-10-29T00:00:00.000Z\",\"provider\":\"faux\",\"modelId\":\"faux-a\"},{\"type\":\"thinking_level_change\",\"id\":\"<entry-2>\",\"parentId\":\"<entry-3>\",\"timestamp\":\"2026-10-29T00:00:00.000Z\",\"thinkingLevel\":\"off\",\"provider\":\"faux\",\"modelId\":\"faux-a\"},{\"type\":\"custom\",\"customType\":\"pi-rules.scan\",\"data\":{\"cwd\":\"<root>/cwd\",\"reason\":\"startup\"},\"startupBookkeeping\":true,\"id\":\"<entry-1>\",\"parentId\":\"<entry-2>\",\"timestamp\":\"2026-10-29T00:00:00.000Z\"}],\"steering\":[],\"followUp\":[],\"ordered\":[],\"autoCompactionEnabled\":true,\"messageCount\":0,\"pendingMessageCount\":0,\"usageTotals\":{\"input\":0,\"output\":0,\"cacheRead\":0,\"cacheWrite\":0,\"cost\":0},\"contextUsage\":{\"tokens\":0,\"contextWindow\":128000,\"percent\":0},\"favoriteModels\":[],\"scopedModels\":[]}},\"sessionId\":\"rpc-1\"}\n{\"id\":\"req_3\",\"type\":\"response\",\"command\":\"get_entries\",\"success\":true,\"data\":{\"entries\":[{\"type\":\"model_change\",\"id\":\"<entry-3>\",\"parentId\":null,\"timestamp\":\"2026-10-29T00:00:00.000Z\",\"provider\":\"faux\",\"modelId\":\"faux-a\"},{\"type\":\"thinking_level_change\",\"id\":\"<entry-2>\",\"parentId\":\"<entry-3>\",\"timestamp\":\"2026-10-29T00:00:00.000Z\",\"thinkingLevel\":\"off\",\"provider\":\"faux\",\"modelId\":\"faux-a\"},{\"type\":\"custom\",\"customType\":\"pi-rules.scan\",\"data\":{\"cwd\":\"<root>/cwd\",\"reason\":\"startup\"},\"startupBookkeeping\":true,\"id\":\"<entry-1>\",\"parentId\":\"<entry-2>\",\"timestamp\":\"2026-10-29T00:00:00.000Z\"}],\"leafId\":\"<entry-1>\"},\"sessionId\":\"rpc-1\"}\n{\"type\":\"session_closed\",\"sessionId\":\"rpc-1\",\"reason\":\"client_close\"}\n{\"id\":\"req_4\",\"type\":\"response\",\"command\":\"close_session\",\"success\":true,\"data\":{},\"sessionId\":\"rpc-1\"}\n",
3
+ "rpcClientFrames": "{\"id\":\"rpc-auth-protocol\",\"type\":\"response\",\"command\":\"get_protocol_info\",\"success\":true,\"data\":{\"protocolVersion\":1,\"serverVersion\":\"<server-version>\",\"capabilities\":[\"<capabilities>\"],\"mode\":\"multi\",\"instanceId\":\"<uuid-1>\",\"generation\":0,\"engineVersion\":\"<server-version>\",\"engineOrdinal\":\"<engine-ordinal>\",\"launch_profile\":{\"profile_id\":\"ca7a49435f02009fdda5dac49d29408dcfeb1ab5374353784c5a9def467f5be1\",\"core\":{\"extensions\":[],\"multi_session\":false,\"session_runtime\":\"worker\"}},\"agentDir\":\"<root>/agent\",\"method_classes\":{\"version\":1,\"digest\":\"67e7dac5622360f9e5fc471764f5a69c43c3abcc22bed55859761cbb53ede37c\",\"classes\":{\"get_protocol_info\":\"observe\",\"get_subscription_limits\":\"observe\",\"list_sessions\":\"observe\",\"list_hands\":\"observe\",\"get_state\":\"observe\",\"get_snapshot\":\"observe\",\"get_entries\":\"observe\",\"get_messages\":\"observe\",\"get_tree\":\"observe\",\"get_fork_messages\":\"observe\",\"get_last_assistant_text\":\"observe\",\"get_session_stats\":\"observe\",\"get_effect_status\":\"dynamic\",\"get_pending_inputs\":\"observe\",\"get_follow_up_messages\":\"observe\",\"get_steering_messages\":\"observe\",\"get_commands\":\"observe\",\"get_loaded_surfaces\":\"observe\",\"get_project_trust\":\"observe\",\"get_available_models\":\"observe\",\"get_available_thinking_levels\":\"observe\",\"get_model_thinking_level\":\"observe\",\"get_fast_mode\":\"observe\",\"get_media\":\"observe\",\"memory_report\":\"observe\",\"eval_cells.list\":\"observe\",\"eval_cells.peek\":\"observe\",\"prompt\":\"dynamic\",\"steer\":\"dynamic\",\"follow_up\":\"dynamic\",\"abort\":\"drive\",\"interrupt\":\"drive\",\"abort_retry\":\"drive\",\"abort_compaction\":\"drive\",\"abort_branch_summary\":\"drive\",\"compact\":\"drive\",\"clear_queue\":\"drive\",\"dequeue_message\":\"drive\",\"edit_queued_message\":\"drive\",\"hold_input\":\"drive\",\"withdraw_held_input\":\"drive\",\"append_user_message\":\"drive\",\"edit_user_message\":\"drive\",\"continue_from_leaf\":\"drive\",\"navigate_tree\":\"drive\",\"set_label\":\"drive\",\"set_session_name\":\"drive\",\"set_thinking_level\":\"dynamic\",\"set_model\":\"dynamic\",\"set_auto_compaction\":\"drive\",\"set_permission_preset\":\"device_admin\",\"set_retry_fallback\":\"drive\",\"pause_for_run\":\"drive\",\"resume_for_run\":\"drive\",\"wake\":\"drive\",\"release_tool_barrier\":\"drive\",\"request_self_settle\":\"drive\",\"eval_cells.stop\":\"drive\",\"append_session_entry\":\"dynamic\",\"extension_ui_response\":\"answer\",\"extension_ui_progress\":\"answer\",\"new_session\":\"dynamic\",\"open_session\":\"dynamic\",\"resume_session\":\"thread\",\"fork\":\"thread\",\"clone\":\"thread\",\"close_session\":\"thread\",\"unload_session\":\"thread\",\"set_session_context\":\"thread\",\"attach_session\":\"thread\",\"select_hand\":\"device_admin\",\"bash\":\"device_admin\",\"abort_bash\":\"device_admin\",\"record_bash_result\":\"device_admin\",\"cleanup_bash_output\":\"device_admin\",\"extension_request\":\"device_admin\",\"reload\":\"device_admin\",\"check_reload_veto\":\"device_admin\",\"resolve_path\":\"device_admin\",\"login_start\":\"device_admin\",\"login_cancel\":\"device_admin\",\"login_api_key\":\"device_admin\",\"logout\":\"device_admin\",\"verify_api_key\":\"device_admin\",\"get_auth_providers\":\"device_admin\",\"get_provider_accounts\":\"device_admin\",\"account_order\":\"device_admin\",\"account_pin\":\"device_admin\",\"account_remove\":\"device_admin\",\"account_reset\":\"device_admin\",\"test_provider_account\":\"device_admin\",\"set_favorite_models\":\"device_admin\",\"set_scoped_models\":\"device_admin\",\"cycle_model\":\"device_admin\",\"cycle_thinking_level\":\"device_admin\",\"set_fast_mode\":\"device_admin\",\"set_steering_mode\":\"device_admin\",\"set_follow_up_mode\":\"device_admin\",\"set_auto_retry\":\"device_admin\",\"set_project_trust\":\"device_admin\",\"get_project_settings\":\"device_admin\",\"set_project_setting\":\"device_admin\",\"edit_assistant_message\":\"device_admin\",\"send_custom_message\":\"device_admin\",\"switch_session\":\"device_admin\",\"import_jsonl\":\"device_admin\",\"export_html\":\"device_admin\",\"export_jsonl\":\"device_admin\",\"set_client_info\":\"connection_local\",\"subscribe_sessions\":\"connection_local\",\"unsubscribe_sessions\":\"connection_local\",\"record_user_activity\":\"connection_local\",\"detach_connection\":\"connection_local\",\"release_session\":\"connection_local\",\"warm\":\"connection_local\",\"host_response\":\"connection_local\",\"release_attachment\":\"connection_local\",\"reclaim_session\":\"connection_local\",\"move_session\":\"connection_local\",\"get_recovery_page\":\"connection_local\",\"release_recovery\":\"connection_local\",\"cancel_attempt\":\"connection_local\",\"reset_stream\":\"connection_local\"},\"dynamic_versions\":{\"get_effect_status\":1,\"prompt\":1,\"steer\":1,\"follow_up\":1,\"set_thinking_level\":1,\"set_model\":1,\"append_session_entry\":1,\"new_session\":1,\"open_session\":2}},\"memory_pressure\":false,\"path_identity_resolver\":{\"state\":\"not_started\",\"detail\":null}}}\n{\"id\":\"req_1\",\"type\":\"response\",\"command\":\"set_client_info\",\"success\":true}\n{\"type\":\"queued\",\"for_request\":\"req_2\",\"position\":1,\"in_flight\":0}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-2>\",\"method\":\"setWidget\",\"widgetKey\":\"todo-sidebar\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-3>\",\"method\":\"setStatus\",\"statusKey\":\"anthropic-web-search\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-4>\",\"method\":\"setWidget\",\"widgetKey\":\"anthropic-web-search\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-5>\",\"method\":\"setStatus\",\"statusKey\":\"openai-web-search\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-6>\",\"method\":\"setWidget\",\"widgetKey\":\"openai-web-search\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-7>\",\"method\":\"setStatus\",\"statusKey\":\"monitors\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-8>\",\"method\":\"setStatus\",\"statusKey\":\"pi-websearch\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-9>\",\"method\":\"setWidget\",\"widgetKey\":\"pi-websearch\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-10>\",\"method\":\"setStatus\",\"statusKey\":\"pi-webfetch\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-11>\",\"method\":\"setWidget\",\"widgetKey\":\"pi-webfetch\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"entry_appended\",\"entry\":{\"type\":\"custom\",\"customType\":\"pi-rules.scan\",\"data\":{\"cwd\":\"<root>/cwd\",\"reason\":\"startup\"},\"startupBookkeeping\":true,\"id\":\"<entry-1>\",\"parentId\":\"<entry-2>\",\"timestamp\":\"2026-10-29T00:00:00.000Z\"},\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-12>\",\"method\":\"setStatus\",\"statusKey\":\"todo-waiting\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-13>\",\"method\":\"setStatus\",\"statusKey\":\"goal\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"session_opened\",\"sessionId\":\"rpc-1\",\"durableSessionId\":\"<uuid-14>\",\"sessionPath\":\"<root>/cwd/session.jsonl\",\"kind\":\"interactive\",\"cwd\":\"<root>/cwd\"}\n{\"id\":\"req_2\",\"type\":\"response\",\"command\":\"open_session\",\"success\":true,\"data\":{\"sessionId\":\"rpc-1\",\"state\":{\"answeredUiRequests\":[],\"paused\":false,\"pausedNoticeCount\":0,\"holds\":[],\"lifecycle\":{\"state\":\"done\",\"seq\":0,\"epoch\":0,\"epochOrigin\":\"automatic\",\"humanInputRevision\":0,\"selfSettleEligible\":true,\"restoring\":false,\"persisted\":false,\"runPaused\":false,\"since\":\"2026-10-29T00:00:00.000Z\",\"role\":\"root\",\"rootSessionId\":\"<uuid-14>\",\"activityRevision\":{\"instanceId\":\"<uuid-15>\",\"revision\":0},\"waitingOn\":{\"children\":0,\"background\":[],\"human\":[]},\"holds\":[],\"observedAt\":\"2026-10-29T00:00:00.000Z\"},\"pendingQuestions\":[],\"pendingDialogs\":[],\"permissionPreset\":\"full-access\",\"permissionPresetSource\":\"host_default\",\"permissionPresetStatus\":\"applied\",\"effectivePermissionPreset\":\"full-access\",\"model\":{\"id\":\"faux-a\",\"name\":\"faux-a\",\"api\":\"faux:fixture\",\"provider\":\"faux\",\"baseUrl\":\"http://localhost:0\",\"reasoning\":false,\"input\":[\"text\",\"image\"],\"cost\":{\"input\":0,\"output\":0,\"cacheRead\":0,\"cacheWrite\":0},\"contextWindow\":128000,\"maxTokens\":16384},\"pendingModelSwitch\":null,\"thinkingLevel\":\"off\",\"thinkingLevelSource\":\"default\",\"sessionModelThinkingLevels\":{\"faux/faux-a\":\"off\"},\"fastMode\":false,\"isStreaming\":false,\"pausedHeldInputCount\":0,\"runPaused\":false,\"pause\":{\"pauseSeq\":0,\"records\":[]},\"isCompacting\":false,\"retryAttempt\":0,\"isBashRunning\":false,\"steeringMode\":\"all\",\"followUpMode\":\"one-at-a-time\",\"sessionFile\":\"<root>/cwd/session.jsonl\",\"sessionId\":\"<uuid-14>\",\"cwd\":\"<root>/cwd\",\"projectTrusted\":false,\"entries\":[{\"type\":\"model_change\",\"id\":\"<entry-3>\",\"parentId\":null,\"timestamp\":\"2026-10-29T00:00:00.000Z\",\"provider\":\"faux\",\"modelId\":\"faux-a\"},{\"type\":\"thinking_level_change\",\"id\":\"<entry-2>\",\"parentId\":\"<entry-3>\",\"timestamp\":\"2026-10-29T00:00:00.000Z\",\"thinkingLevel\":\"off\",\"provider\":\"faux\",\"modelId\":\"faux-a\"},{\"type\":\"custom\",\"customType\":\"pi-rules.scan\",\"data\":{\"cwd\":\"<root>/cwd\",\"reason\":\"startup\"},\"startupBookkeeping\":true,\"id\":\"<entry-1>\",\"parentId\":\"<entry-2>\",\"timestamp\":\"2026-10-29T00:00:00.000Z\"}],\"steering\":[],\"followUp\":[],\"ordered\":[],\"autoCompactionEnabled\":true,\"messageCount\":0,\"pendingMessageCount\":0,\"usageTotals\":{\"input\":0,\"output\":0,\"cacheRead\":0,\"cacheWrite\":0,\"cost\":0},\"contextUsage\":{\"tokens\":0,\"contextWindow\":128000,\"percent\":0},\"favoriteModels\":[],\"scopedModels\":[]},\"permissionPreset\":\"full-access\",\"permissionPresetSource\":\"host_default\",\"permissionPresetStatus\":\"applied\"},\"sessionId\":\"rpc-1\"}\n{\"id\":\"req_3\",\"type\":\"response\",\"command\":\"get_entries\",\"success\":true,\"data\":{\"entries\":[{\"type\":\"model_change\",\"id\":\"<entry-3>\",\"parentId\":null,\"timestamp\":\"2026-10-29T00:00:00.000Z\",\"provider\":\"faux\",\"modelId\":\"faux-a\"},{\"type\":\"thinking_level_change\",\"id\":\"<entry-2>\",\"parentId\":\"<entry-3>\",\"timestamp\":\"2026-10-29T00:00:00.000Z\",\"thinkingLevel\":\"off\",\"provider\":\"faux\",\"modelId\":\"faux-a\"},{\"type\":\"custom\",\"customType\":\"pi-rules.scan\",\"data\":{\"cwd\":\"<root>/cwd\",\"reason\":\"startup\"},\"startupBookkeeping\":true,\"id\":\"<entry-1>\",\"parentId\":\"<entry-2>\",\"timestamp\":\"2026-10-29T00:00:00.000Z\"}],\"leafId\":\"<entry-1>\"},\"sessionId\":\"rpc-1\"}\n{\"type\":\"session_closed\",\"sessionId\":\"rpc-1\",\"reason\":\"client_close\"}\n{\"id\":\"req_4\",\"type\":\"response\",\"command\":\"close_session\",\"success\":true,\"data\":{},\"sessionId\":\"rpc-1\"}\n",
4
4
  "subscribeRequests": "{\"type\":\"subscribe_sessions\",\"mirror\":\"all\",\"id\":\"subscriber-1\"}\n{\"type\":\"unsubscribe_sessions\",\"subscriptionId\":\"sub-1\",\"id\":\"subscriber-2\"}\n",
5
5
  "subscribeFrames": "{\"id\":\"subscriber-1\",\"type\":\"response\",\"command\":\"subscribe_sessions\",\"success\":true,\"data\":{\"subscriptionId\":\"sub-1\",\"snapshot\":[],\"seq\":0,\"hostGeneration\":\"0\",\"hostInstanceId\":\"<uuid-1>\"}}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-2>\",\"method\":\"setWidget\",\"widgetKey\":\"todo-sidebar\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-3>\",\"method\":\"setStatus\",\"statusKey\":\"anthropic-web-search\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-4>\",\"method\":\"setWidget\",\"widgetKey\":\"anthropic-web-search\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-5>\",\"method\":\"setStatus\",\"statusKey\":\"openai-web-search\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-6>\",\"method\":\"setWidget\",\"widgetKey\":\"openai-web-search\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-7>\",\"method\":\"setStatus\",\"statusKey\":\"monitors\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-8>\",\"method\":\"setStatus\",\"statusKey\":\"pi-websearch\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-9>\",\"method\":\"setWidget\",\"widgetKey\":\"pi-websearch\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-10>\",\"method\":\"setStatus\",\"statusKey\":\"pi-webfetch\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-11>\",\"method\":\"setWidget\",\"widgetKey\":\"pi-webfetch\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"entry_appended\",\"entry\":{\"type\":\"custom\",\"customType\":\"pi-rules.scan\",\"data\":{\"cwd\":\"<root>/cwd\",\"reason\":\"startup\"},\"startupBookkeeping\":true,\"id\":\"<entry-1>\",\"parentId\":\"<entry-2>\",\"timestamp\":\"2026-10-29T00:00:00.000Z\"},\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-12>\",\"method\":\"setStatus\",\"statusKey\":\"todo-waiting\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-13>\",\"method\":\"setStatus\",\"statusKey\":\"goal\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"session_opened\",\"sessionId\":\"rpc-1\",\"durableSessionId\":\"<uuid-14>\",\"sessionPath\":\"<root>/cwd/session.jsonl\",\"kind\":\"interactive\",\"cwd\":\"<root>/cwd\"}\n{\"type\":\"session_status\",\"subscriptionId\":\"sub-1\",\"seq\":1,\"sessionId\":\"rpc-1\",\"status\":\"idle\",\"kind\":\"interactive\",\"cwd\":\"<root>/cwd\",\"attachments\":1,\"durableSessionId\":\"<uuid-14>\",\"sessionPath\":\"<root>/cwd/session.jsonl\",\"model\":{\"provider\":\"faux\",\"id\":\"faux-a\"},\"thinkingLevel\":\"off\",\"pendingQueue\":{\"steering\":0,\"followUp\":0},\"lastChange\":{\"at\":\"2026-10-29T00:00:00.000Z\",\"cause\":\"open\",\"by\":\"client\"}}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-15>\",\"method\":\"setStatus\",\"statusKey\":\"anthropic-web-search\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-16>\",\"method\":\"setWidget\",\"widgetKey\":\"anthropic-web-search\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-17>\",\"method\":\"setStatus\",\"statusKey\":\"openai-web-search\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-18>\",\"method\":\"setWidget\",\"widgetKey\":\"openai-web-search\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-19>\",\"method\":\"setStatus\",\"statusKey\":\"monitors\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-20>\",\"method\":\"setStatus\",\"statusKey\":\"pi-websearch\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-21>\",\"method\":\"setWidget\",\"widgetKey\":\"pi-websearch\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-22>\",\"method\":\"setStatus\",\"statusKey\":\"pi-webfetch\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-23>\",\"method\":\"setWidget\",\"widgetKey\":\"pi-webfetch\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"extension_ui_request\",\"id\":\"<uuid-24>\",\"method\":\"setStatus\",\"statusKey\":\"loop\",\"sessionId\":\"rpc-1\"}\n{\"type\":\"session_closed\",\"sessionId\":\"rpc-1\",\"reason\":\"client_close\"}\n{\"type\":\"session_status\",\"subscriptionId\":\"sub-1\",\"seq\":2,\"sessionId\":\"rpc-1\",\"status\":\"closed\",\"reason\":\"client_close\",\"kind\":\"interactive\",\"cwd\":\"<root>/cwd\",\"attachments\":1,\"durableSessionId\":\"<uuid-14>\",\"sessionPath\":\"<root>/cwd/session.jsonl\",\"model\":{\"provider\":\"faux\",\"id\":\"faux-a\"},\"thinkingLevel\":\"off\",\"pendingQueue\":{\"steering\":0,\"followUp\":0},\"lastChange\":{\"at\":\"2026-10-29T00:00:00.000Z\",\"cause\":\"close\",\"by\":\"client\"}}\n{\"id\":\"subscriber-2\",\"type\":\"response\",\"command\":\"unsubscribe_sessions\",\"success\":true}\n"
6
6
  }
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "methodClassesVersion": 1,
3
- "methodClassesDigest": "3860136b2a1b07c20373ed361df07c045bd1e84fce001917e271ca0e4b903e63",
3
+ "methodClassesDigest": "67e7dac5622360f9e5fc471764f5a69c43c3abcc22bed55859761cbb53ede37c",
4
4
  "sampleId": "00000000-0000-7000-8000-000000000000",
5
5
  "samplePathRoot": "/nonexistent/senpi-rpc-inbound-sample",
6
- "methodCount": 122,
6
+ "methodCount": 123,
7
7
  "methods": [
8
8
  {
9
9
  "method": "abort",
@@ -1065,6 +1065,16 @@
1065
1065
  "modelId": "rpc-sample-model"
1066
1066
  }
1067
1067
  },
1068
+ {
1069
+ "method": "set_permission_preset",
1070
+ "class": "device_admin",
1071
+ "sampleClass": "device_admin",
1072
+ "connectionLocal": false,
1073
+ "sample": {
1074
+ "type": "set_permission_preset",
1075
+ "permissionPreset": "ask"
1076
+ }
1077
+ },
1068
1078
  {
1069
1079
  "method": "set_project_setting",
1070
1080
  "class": "device_admin",
@@ -885,6 +885,18 @@ pi.on("ui_prompt_end", async (event, ctx) => {
885
885
  });
886
886
  ```
887
887
 
888
+ #### Permission mode and session-wide dialogs
889
+
890
+ `pi.getPermissionPreset()` returns the preset the permission system enforces for this session right now, or `undefined` when no permission system runs for it or its setup failed.
891
+
892
+ When a client moves a live session to another preset (`set_permission_preset`, or an attach naming one), the session announces it on the `pi.events` bus. A session's initial preset is not announced; read it with `pi.getPermissionPreset()`.
893
+
894
+ | Channel | Payload | Meaning |
895
+ |---------|---------|---------|
896
+ | `permission_preset_changed` | `{ permissionPreset, permissionPresetSource, permissionPresetStatus, effectivePermissionPreset }` | The session's approval mode changed; the payload is the same mode `get_state` reports. |
897
+
898
+ `pi.sessionDialog.select / confirm / input / editor` raise a dialog addressed to every client attached to the session, not only to the connection whose command is running. Use it for a prompt raised after that command returned, such as a background child's permission request a task runner forwards. The first answer wins; a later answer to the same dialog is refused with `already_answered`. In the terminal UI it shows like any other dialog. The same addressing is available on `ctx.ui` dialogs with the `audience: "session"` option.
899
+
888
900
  #### Pending-question bus events
889
901
 
890
902
  These additive events use `pi.events.on(...)`, not `pi.on(...)`:
package/docs/rpc.md CHANGED
@@ -1286,7 +1286,7 @@ from them that the host itself went away (shutdown) or handed its endpoint to a
1286
1286
  | `event_queue_failed` | `true` | The socket's event queue failed; follows its `overflow` record when it has one | `overflow`, `stall` or `transport_error` |
1287
1287
  | `event_lease_refused` | `true` | An event cannot be leased; follows its `event_lease_refused` record | `lease_limit` or `storage_unavailable` |
1288
1288
  | `compact_endpoint_unsupported` | `false` | A compact mirror of a session served only on the legacy wire; follows its `compact_refused` record | none |
1289
- | `host_shutdown` | `true` | The host is shutting down; follows its sessions' `session_closed` records (`reason: "host_shutdown"`) | none |
1289
+ | `host_shutdown` | `true` | The host is shutting down; follows its sessions' `session_closed` records (`reason: "host_shutdown"`), or `session_parked { reason: "host_shutdown" }` for a retained session on a socket host | none |
1290
1290
  | `host_handoff` | `true` | The host drains for a newer generation; follows its sessions' handoff park records | none |
1291
1291
 
1292
1292
  #### interactive_user
@@ -2514,7 +2514,8 @@ releases the caller's own attachment and closes the session only when no other c
2514
2514
 
2515
2515
  Retention limits (what `open`'s `retain_on_disconnect: true` does NOT survive): an explicit `close`, the host's own
2516
2516
  idle-eviction window (`SENPI_RPC_SESSION_IDLE_EVICTION_MS`, default 30 minutes, after which the session is parked -
2517
- any `senpi host session` verb given its path or durable id resumes it), or the host process exiting. A session that has not
2517
+ any `senpi host session` verb given its path or durable id resumes it). The host process exiting does not end it: the next
2518
+ host on the same endpoint reopens it (see "Retained sessions across a host restart"). A session that has not
2518
2519
  received a message yet may have no JSONL file on disk even though `open` already reported a `sessionPath`.
2519
2520
 
2520
2521
  A minimal orchestration run:
@@ -2801,8 +2802,8 @@ viewer opened to the next one (senpi#5808). So every `open_session` on a delegat
2801
2802
  stays `open` with `attachments: 0` and attachable from any connection, exactly like a retained session (see "Retained
2802
2803
  sessions"). The host decides this when it reads the line, so a connection that ends while its open is still running
2803
2804
  leaves that session retained too. Like any retained session it is still closed by an explicit `close_session`, parked
2804
- by the idle-eviction window, and recorded at park for the next host generation, which reopens it on startup, so a
2805
- parked session survives into the next generation. `attach_session` never turns retention on: attaching over a delegated
2805
+ by the idle-eviction window, and recorded (at open, and again at park) for the next host on the endpoint, which reopens
2806
+ it on startup, so it survives into the next generation and across a host restart. `attach_session` never turns retention on: attaching over a delegated
2806
2807
  connection to a session that is not retained leaves it closing with its last attachment. One consequence to know: a
2807
2808
  delegated `open_session` by `sessionPath` (which needs `device_admin`) that lands on a session already live and opened by
2808
2809
  someone else joins it as an attach that asks for retention, so it turns retention on for that live session and every
@@ -3297,8 +3298,9 @@ What the host does enforce are lifecycle windows, and they only ever return memo
3297
3298
  command's `unknown_session`. A GENERATION HANDOFF also parks attached sessions, using the distinct activity
3298
3299
  predicate below, and emits `session_closed { sessionId, reason: "handoff_parked", sessionPath }`, so a client can tell "the host handed
3299
3300
  over, reopen by path" from "this session ended". An explicit `close_session` is `reason: "client_close"`; the host
3300
- process exiting is `reason: "host_shutdown"` (even for a retained session - the process is going away, so this is not
3301
- a park). `reason` is optional on the wire; a client that does not know a
3301
+ process exiting is `reason: "host_shutdown"`, except for a retained session on a socket host, which PARKS instead
3302
+ (`session_parked { reason: "host_shutdown" }`) because the next host on the endpoint reopens it (see "Retained sessions
3303
+ across a host restart"). `reason` is optional on the wire; a client that does not know a
3302
3304
  value, or receives a record with no `reason` field, treats the record exactly as a reason-less one. "Session-owned
3303
3305
  work" is the complete activity contract, not just a streaming turn: an agent run, a running bash command,
3304
3306
  background terminal jobs and any other published wake source (terminal monitors, loop-guard holds), compaction,
@@ -3334,17 +3336,45 @@ What the host does enforce are lifecycle windows, and they only ever return memo
3334
3336
  in-flight turn to settlement, keeps its path reservation and worker slot, and stays in `list_sessions`. A later
3335
3337
  `open_session` with that `sessionPath` — from any connection — attaches to the same routing handle and returns
3336
3338
  `attached: true`. Retention never outranks an explicit teardown: a `close_session` from an attached connection still
3337
- reaches zero attachments and closes the session, host shutdown closes it, and the idle-eviction window above still
3339
+ reaches zero attachments and closes the session, a socket host's shutdown parks it (see below), and the idle-eviction
3340
+ window above still
3338
3341
  parks it (or the completed-detached-worker rule above parks it sooner) — announced as
3339
3342
  `session_parked { sessionId, sessionPath }`, after which the file reopens by path like any
3340
3343
  evicted session, and the parked session counts as gone for the empty-host exit below. The park is also recorded
3341
3344
  for the NEXT generation, which reopens it on startup; the host that parked it never reopens it on its own, so a
3342
- client reopening the session right after `session_parked` is the only open of that path (senpi#3485). It is also bounded by the empty-host exit
3343
- and the supervisor's idle-exit window below: retention survives a client, not the host. Any attach may turn retention
3345
+ client reopening the session right after `session_parked` is the only open of that path (senpi#3485). Retention survives
3346
+ the host process too (next item). Any attach may turn retention
3344
3347
  on for a live session; no attach turns it off for clients that already rely on it. Omitting the flag is byte-identical
3345
3348
  to the previous behavior — the session is closed with its last connection — except on a delegated connection
3346
3349
  (`declare_delegation`), where every `open_session` is retained (see "Delegated connections"; senpi#5808). Probe `retain_on_disconnect` in
3347
3350
  `get_protocol_info` capabilities before relying on it: an older host silently ignores the field.
3351
+ - **Retained sessions across a host restart**: every retained session with a session path is recorded in the
3352
+ endpoint's daemon directory (`handover-parked/`, one entry per session path, the same record a handoff park writes)
3353
+ as soon as its open succeeds; the entry follows the session's `set_session_context` patches and its park, and a
3354
+ transcript switch (`new_session`, `switch_session`, a fork) moves it to the new file, so only the transcript the
3355
+ session is on comes back. An explicit `close_session` or a reclaim removes every key the session was recorded
3356
+ under. While the session is open the entry names the process that holds it; a park drops that, and a host never
3357
+ reopens an entry whose holding process is still alive. So during a generation handoff the successor does not open
3358
+ a session its draining predecessor still has loaded, idle and detached ones included: it reopens the predecessor's
3359
+ park once the drain writes it (or the entry, if the predecessor dies without parking). A host
3360
+ that starts on the same socket and agent dir - after a handoff, a clean stop, a crash, or a kill in the middle of a
3361
+ turn - reopens, in the background right after it binds, every entry an earlier process wrote, replaying the
3362
+ recorded launch profile as `open_session { sessionPath, durableSessionId, retain_on_disconnect: true, ... }`: the same
3363
+ open-by-path a TUI resume runs on its session file. The session is `open` in `list_sessions` again with the same
3364
+ `durableSessionId`, `sessionPath` and transcript, announced by a `session_opened` record, and no client has to
3365
+ re-open it. A clean stop (SIGTERM, idle or empty exit) parks each retained session first: attached clients receive
3366
+ `session_parked { sessionId, sessionPath, reason: "host_shutdown" }`, never `session_closed`, and a turn running at
3367
+ the stop ends with `agent_end { aborted: true, abortSource: "system", abortCause: { kind: "host_shutdown" } }`, never
3368
+ a user abort. A killed host sends nothing. Either way the turn the stopped process was running is not resumed: the
3369
+ transcript holds what was persisted before the stop, as after a TUI crash. The routing handle (`sessionId`, `rpc-N`) is new, since handles are unique per process;
3370
+ a client holding the old one gets `session_not_found` and re-resolves the session from `list_sessions` (or
3371
+ `session_opened`) by `durableSessionId`. A relay that mirrors `list_sessions` (relayd keeps no session store of its
3372
+ own) therefore sees the session again unchanged. Not reopened: an entry whose session file is gone (a session that
3373
+ never received a message has none), an explicitly closed or reclaimed session (its entry is removed), and a session
3374
+ whose context `task_status` is terminal (`completed`, `cancelled`, `error`, `failed`, `lost`, `interrupted`). There is
3375
+ no separate resume-by-durable-id request: `open_session { sessionPath }` already resumes a known transcript, but a
3376
+ remote client learns sessions only from `list_sessions`, so a session the restarted host did not list could not be
3377
+ resumed by one at all.
3348
3378
  - **Empty-host exit**: when the registry holds zero sessions AND no client is connected, continuously for
3349
3379
  `SENPI_RPC_HOST_EMPTY_EXIT_MS` (default 15 minutes), the host exits through its clean shutdown path (flush, socket
3350
3380
  removal), for stdio and `--listen` hosts alike. A connected client counts as occupancy even with no session open,
@@ -4740,11 +4770,26 @@ Response:
4740
4770
  "autoCompactionEnabled": true,
4741
4771
  "messageCount": 5,
4742
4772
  "pendingMessageCount": 0,
4743
- "pendingDialogs": []
4773
+ "pendingDialogs": [],
4774
+ "permissionPreset": "ask",
4775
+ "permissionPresetSource": "client",
4776
+ "permissionPresetStatus": "applied",
4777
+ "effectivePermissionPreset": "ask"
4744
4778
  }
4745
4779
  }
4746
4780
  ```
4747
4781
 
4782
+ `permissionPreset`, `permissionPresetSource`, `permissionPresetStatus` and `effectivePermissionPreset` report the approval mode the session enforces. `permissionPresetSource` is `client` when a client named a valid preset for this session (`open_session.permissionPreset`, an attach naming one, or `set_permission_preset`) and `host_default` when the host's own default applies (its `--permission-preset` flag, then project and global settings, then `full-access`). `permissionPresetStatus` is `invalid` when the named preset or the host's setting cannot be parsed: the open is still accepted, but every gated tool call is refused, and an invalid preset never counts as one the client set. `effectivePermissionPreset` is the preset the next permission decision uses, `null` while the status is `invalid`. A client confirms a mode it asked for only when `permissionPreset` equals it and `permissionPresetStatus` is `applied`. The `open_session` reply carries the first three fields in `data` as well as in `data.state`.
4783
+
4784
+ A preset a client named is recorded in the session file as a `permission_mode` entry. The entry is a record only: a later open that names no preset still uses the host's own default, and the launch flag always wins.
4785
+
4786
+ Each gated decision picks its preset in this order:
4787
+
4788
+ 1. A preset a client named for the session (`open_session.permissionPreset`, an attach naming one, or `set_permission_preset`) wins.
4789
+ 2. Work a remote device started asks: a turn started by a `prompt`, `follow_up` or `steer` (`whenIdle: "start"`) from a delegated connection, and engine-started work (extension messages, goal and loop turns, `continue_from_leaf`, `wake`, restored input) on a session a remote device has driven. A session counts as remote-driven once a delegated connection sends it any record whose method class is `drive` or `device_admin`; the engine's method-class table decides, so every run starter is covered. That includes records that start no turn, such as `abort`, `set_session_name` or `clear_queue`, and the mark lasts for the session's lifetime on purpose: telling which drive records can shape later engine-started work would need a second hand-kept list beside the table, and a wrong guess there would run remote-influenced work without asking, so the engine errs toward asking. When the operator named no `--permission-preset`, it uses the preset a client recorded in the session file instead. An embedder's host-level preset is the host default, not a launch flag. This rule is decided before `spawnedBy`: a parent named in `open_session`, real or not, never takes a remote device's work off it.
4790
+ 3. A child session (`spawnedBy`) follows its live parent's current mode, including later changes to it. A child whose parent is not live in this process asks, unless the operator named a preset for it with `--permission-preset`.
4791
+ 4. Everything else uses the host's own default. A local connection's turn keeps the host default even on a session a remote device has driven, and a `steer` never changes the origin of the turn it joins.
4792
+
4748
4793
  `currentTurnId` is the id of the running turn, present only while one runs; a steer or follow-up names it as `expectedTurnId` (on `prompt`, `steer` or `follow_up`).
4749
4794
 
4750
4795
  The `model` field is a full [Model](#model) object or `null`. `pendingModelSwitch` is always present: `null` means no held switch, while an object with `provider` and `id` identifies a switch that applies after the next compaction. The `sessionName` field is the display name set via `set_session_name`, or omitted if not set.
@@ -5198,6 +5243,21 @@ Response:
5198
5243
  {"type": "response", "command": "set_auto_compaction", "success": true}
5199
5244
  ```
5200
5245
 
5246
+ #### set_permission_preset
5247
+
5248
+ Move this session to another approval mode. It works on a classic `--mode rpc` connection, which refuses `open_session`, and on a multi-session host with the `sessionId` envelope, so a client on another device can change a live session's mode without knowing its session file. It is classed `device_admin`: it changes what the agent may do without asking. Hosts that take it advertise `set_permission_preset_v1`.
5249
+
5250
+ ```json
5251
+ {"type": "set_permission_preset", "permissionPreset": "ask"}
5252
+ ```
5253
+
5254
+ Response:
5255
+ ```json
5256
+ {"type": "response", "command": "set_permission_preset", "success": true, "data": {"permissionPreset": "ask", "permissionPresetSource": "client", "permissionPresetStatus": "applied"}}
5257
+ ```
5258
+
5259
+ The new preset applies from the next permission decision, and the session file records it like a preset named at open. An unknown preset is refused with `errorCode` `invalid_launch_profile` and changes nothing, so the session keeps the mode it had. Extensions hear a change on the `permission_preset_changed` event and read the enforced preset with `pi.getPermissionPreset()`.
5260
+
5201
5261
  ### Retry
5202
5262
 
5203
5263
  #### set_auto_retry
@@ -6259,7 +6319,7 @@ When a multi-session host ends a routing handle it may name why:
6259
6319
  | `client_close` | An attached client sent `close_session` |
6260
6320
  | `idle_evicted` | The idle sweep ended a session that was not retained |
6261
6321
  | `session_dir_removed` | The session's transcript directory was deleted and nothing can reopen it: the idle sweep ended it because no client held it (whatever the idle window), or a generation handoff drain ended it instead of parking it. No `sessionPath` |
6262
- | `host_shutdown` | The host process is exiting (SIGTERM, idle-exit, empty-host). A retained session is closed, not parked |
6322
+ | `host_shutdown` | The host process is exiting (SIGTERM, idle-exit, empty-host). A retained session on a socket host is parked instead (`session_parked { reason: "host_shutdown" }`) |
6263
6323
  | `replaced` | The routing handle ended because the live session behind it was replaced |
6264
6324
  | `handoff_parked` | A generation handoff drained this host; reopen with `open_session { sessionPath }` |
6265
6325
  | `released` | `release_session` handed the session to a runtime outside this host. Carries `sessionPath`, the file that runtime now writes: do NOT reopen it here |
@@ -6450,7 +6510,7 @@ cannot replace the ID on a queued start event. Older engines omit this additive
6450
6510
 
6451
6511
  ### agent_end
6452
6512
 
6453
- Emitted when one low-level agent run completes. Contains all messages generated during this run. If `willRetry` is true, an automatic retry will follow. A run stopped because its session was replaced also carries `abortCause: { kind: "session_replaced", reason, targetSessionFile?, targetSessionId? }` (see `session_replaced`); extensions' own `agent_end` does not carry it, they learn of the replacement from `session_shutdown`.
6513
+ Emitted when one low-level agent run completes. Contains all messages generated during this run. If `willRetry` is true, an automatic retry will follow. A run stopped because its session was replaced also carries `abortCause: { kind: "session_replaced", reason, targetSessionFile?, targetSessionId? }` (see `session_replaced`); a run the host's own stop cut short ends with `aborted: true`, `abortSource: "system"` and `abortCause: { kind: "host_shutdown" }`, never `abortSource: "user"` (senpi#6190); extensions' own `agent_end` does not carry it, they learn of the replacement from `session_shutdown`.
6454
6514
 
6455
6515
  ```json
6456
6516
  {