@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,57 @@
1
+ /**
2
+ * The process that holds a retained session resident (senpi#6190). A host records each retained session it has
3
+ * open, stamped with this writer, so the next host on the endpoint can restore it after a crash; while that
4
+ * writer is still alive (a generation handoff's draining predecessor) the session is not its successor's to
5
+ * reopen - the predecessor parks it, and the unstamped park is what the successor consumes. Liveness follows the
6
+ * session writer claims: same host, same boot, live pid whose start time matches.
7
+ */
8
+ import { BOOT_INSTANT_TOLERANCE_MS, sameBoot } from "../../core/extensions/builtin/terminal/process-identity.js";
9
+ import { realWriterProbes } from "../../core/session-writer-claims.js";
10
+ export function currentResidentWriter(probes = realWriterProbes) {
11
+ const self = probes.self();
12
+ return {
13
+ host: probes.host(),
14
+ pid: self.pid,
15
+ bootAtMs: probes.bootAtMs(),
16
+ processStartedAtMs: self.processStartedAtMs,
17
+ };
18
+ }
19
+ export function isResidentWriter(value) {
20
+ if (typeof value !== "object" || value === null || Array.isArray(value))
21
+ return false;
22
+ const record = value;
23
+ return (typeof record.host === "string" &&
24
+ Number.isInteger(record.pid) &&
25
+ typeof record.bootAtMs === "number" &&
26
+ typeof record.processStartedAtMs === "number");
27
+ }
28
+ /**
29
+ * A liveness probe for one reopen watch, which polls: a writer found gone stays gone, and a live pid's start time
30
+ * (a process spawn on some platforms) is read once. A writer on another host cannot be probed from here, so it is
31
+ * never displaced.
32
+ */
33
+ export function residentWriterProbe(probes = realWriterProbes) {
34
+ const gone = new Set();
35
+ const verified = new Set();
36
+ return async (writer) => {
37
+ const key = `${writer.host}:${writer.pid}:${writer.processStartedAtMs}`;
38
+ if (gone.has(key))
39
+ return false;
40
+ if (writer.host !== probes.host())
41
+ return true;
42
+ if (!sameBoot(probes.bootAtMs(), writer.bootAtMs) || !probes.pidAlive(writer.pid)) {
43
+ gone.add(key);
44
+ return false;
45
+ }
46
+ if (verified.has(key))
47
+ return true;
48
+ const startedAtMs = await probes.readProcessStartMs(writer.pid);
49
+ if (startedAtMs !== undefined && Math.abs(startedAtMs - writer.processStartedAtMs) > BOOT_INSTANT_TOLERANCE_MS) {
50
+ gone.add(key);
51
+ return false;
52
+ }
53
+ verified.add(key);
54
+ return true;
55
+ };
56
+ }
57
+ //# sourceMappingURL=host-handover-resident-writer.js.map
@@ -30,6 +30,11 @@ export declare class HostWarmer {
30
30
  private readonly load;
31
31
  constructor(load: (profile: HostWarmProfile) => Promise<void>);
32
32
  run(profile: HostWarmProfile): Promise<HostWarmState>;
33
+ /**
34
+ * Settles once every load of `cwd` in flight NOW has settled; `undefined` when none is. A snapshot: a warm
35
+ * that starts later, or one of another cwd, is not part of it.
36
+ */
37
+ inFlight(cwd: string): Promise<void> | undefined;
33
38
  private loadOnce;
34
39
  }
35
40
  /**
@@ -40,7 +45,17 @@ export declare class HostWarmer {
40
45
  export declare function createRegistryWarm(prepare: NonNullable<PreparableRuntimeFactory["prepare"]>, host: {
41
46
  agentDir: string;
42
47
  mcpRegistry?: HostMcpRegistry;
43
- }): HostWarm;
48
+ }): {
49
+ readonly warm: HostWarm;
50
+ readonly inFlight: (cwd: string) => Promise<void> | undefined;
51
+ };
52
+ /** How an open's wait for the warms of its cwd ended. */
53
+ export type WarmWaitOutcome = "settled" | "warm_wait_deadline";
54
+ /**
55
+ * Waits for `warming` at most `boundMs`. A warm whose extension factory never settles (there is no factory
56
+ * timeout) must not stall an open, so past the bound the open proceeds and the reason is reported.
57
+ */
58
+ export declare function waitForWarms(warming: Promise<void>, boundMs: number, cwd: string): Promise<WarmWaitOutcome>;
44
59
  type WarmCommand = Extract<RpcCommand, {
45
60
  type: "warm";
46
61
  }>;
@@ -34,14 +34,24 @@ export class HostWarmer {
34
34
  const key = profileKey(profile);
35
35
  if (this.warm.has(key))
36
36
  return "already_warm";
37
- let pending = this.loading.get(key);
38
- if (pending === undefined) {
39
- pending = this.loadOnce(key, profile);
40
- this.loading.set(key, pending);
37
+ let load = this.loading.get(key);
38
+ if (load === undefined) {
39
+ load = { cwd: profile.cwd, pending: this.loadOnce(key, profile) };
40
+ this.loading.set(key, load);
41
41
  }
42
- await pending;
42
+ await load.pending;
43
43
  return "warmed";
44
44
  }
45
+ /**
46
+ * Settles once every load of `cwd` in flight NOW has settled; `undefined` when none is. A snapshot: a warm
47
+ * that starts later, or one of another cwd, is not part of it.
48
+ */
49
+ inFlight(cwd) {
50
+ const pending = [...this.loading.values()].filter((load) => load.cwd === cwd).map((load) => load.pending);
51
+ if (pending.length === 0)
52
+ return undefined;
53
+ return Promise.allSettled(pending).then(() => undefined);
54
+ }
45
55
  async loadOnce(key, profile) {
46
56
  try {
47
57
  await this.load(profile);
@@ -85,7 +95,24 @@ export function createRegistryWarm(prepare, host) {
85
95
  scope.close();
86
96
  }
87
97
  });
88
- return (profile) => warmer.run(profile);
98
+ return { warm: (profile) => warmer.run(profile), inFlight: (cwd) => warmer.inFlight(cwd) };
99
+ }
100
+ /**
101
+ * Waits for `warming` at most `boundMs`. A warm whose extension factory never settles (there is no factory
102
+ * timeout) must not stall an open, so past the bound the open proceeds and the reason is reported.
103
+ */
104
+ export async function waitForWarms(warming, boundMs, cwd) {
105
+ let deadline;
106
+ const outcome = await Promise.race([
107
+ warming.then(() => "settled"),
108
+ new Promise((resolve) => {
109
+ deadline = setTimeout(() => resolve("warm_wait_deadline"), boundMs);
110
+ }),
111
+ ]);
112
+ clearTimeout(deadline);
113
+ if (outcome === "warm_wait_deadline")
114
+ process.stderr.write(`senpi rpc open: warm_wait_deadline: a warm of ${cwd} still loading after ${boundMs} ms; opening anyway\n`);
115
+ return outcome;
89
116
  }
90
117
  function refusal(command, code) {
91
118
  return { id: command.id, type: "response", command: "warm", success: false, error: code };
@@ -478,6 +478,15 @@ export declare const SESSION_RECORD_SCHEMAS: {
478
478
  } & {
479
479
  readonly enabled: Type.TBoolean;
480
480
  }>;
481
+ set_permission_preset: Type.TObject<{
482
+ id: Type.TOptional<Type.TString>;
483
+ admissionId: Type.TOptional<Type.TString>;
484
+ type: Type.TLiteral<"set_permission_preset">;
485
+ } & {
486
+ readonly sessionId: Type.TOptional<Type.TString>;
487
+ } & {
488
+ readonly permissionPreset: Type.TString;
489
+ }>;
481
490
  set_auto_retry: Type.TObject<{
482
491
  id: Type.TOptional<Type.TString>;
483
492
  admissionId: Type.TOptional<Type.TString>;
@@ -131,6 +131,7 @@ const STATE_AND_MODEL_SCHEMAS = {
131
131
  set_follow_up_mode: session("set_follow_up_mode", { mode: literals("all", "one-at-a-time") }),
132
132
  compact: session("compact", { customInstructions: OptStr }),
133
133
  set_auto_compaction: session("set_auto_compaction", { enabled: Bool }),
134
+ set_permission_preset: session("set_permission_preset", { permissionPreset: Id }),
134
135
  set_auto_retry: session("set_auto_retry", { enabled: Bool }),
135
136
  abort_retry: session("abort_retry", {}),
136
137
  set_retry_fallback: session("set_retry_fallback", { retryFallback: RetryFallback }),
@@ -852,6 +852,15 @@ export declare const RPC_INBOUND_SCHEMAS: {
852
852
  } & {
853
853
  readonly enabled: import("typebox").TBoolean;
854
854
  }>;
855
+ readonly set_permission_preset: import("typebox").TObject<{
856
+ id: import("typebox").TOptional<import("typebox").TString>;
857
+ admissionId: import("typebox").TOptional<import("typebox").TString>;
858
+ type: import("typebox").TLiteral<"set_permission_preset">;
859
+ } & {
860
+ readonly sessionId: import("typebox").TOptional<import("typebox").TString>;
861
+ } & {
862
+ readonly permissionPreset: import("typebox").TString;
863
+ }>;
855
864
  readonly set_auto_retry: import("typebox").TObject<{
856
865
  id: import("typebox").TOptional<import("typebox").TString>;
857
866
  admissionId: import("typebox").TOptional<import("typebox").TString>;
@@ -140,6 +140,7 @@ export declare const RPC_METHOD_CLASS: {
140
140
  readonly version: 1;
141
141
  };
142
142
  readonly set_auto_compaction: "drive";
143
+ readonly set_permission_preset: "device_admin";
143
144
  readonly set_retry_fallback: "drive";
144
145
  readonly pause_for_run: "drive";
145
146
  readonly resume_for_run: "drive";
@@ -187,6 +187,8 @@ export const RPC_METHOD_CLASS = {
187
187
  set_model: { classify: setModelClass, version: 1 },
188
188
  // a session override; set_steering_mode, set_follow_up_mode and set_auto_retry write global settings (device_admin)
189
189
  set_auto_compaction: "drive",
190
+ // like a preset-naming open_session: it changes what the agent may do without asking
191
+ set_permission_preset: "device_admin",
190
192
  set_retry_fallback: "drive",
191
193
  pause_for_run: "drive",
192
194
  resume_for_run: "drive",
@@ -13,6 +13,7 @@ import { answerDelegation, relayExecRefusal } from "./delegated-answer.js";
13
13
  import { parseClientCapabilities } from "./custom-capability.js";
14
14
  import { ClientOccupancy } from "./host-client-occupancy.js";
15
15
  import { HostCoreGate } from "./host-core-gate.js";
16
+ import { bindRegisteredEndpoint } from "./host-bare-endpoint-registration.js";
16
17
  import { createHostDaemonPaths } from "./host-daemon-paths.js";
17
18
  import { ENDPOINT_OWNER_FENCE_CAPABILITY, GENERATION_HANDOFF_CAPABILITY } from "./host-decision.js";
18
19
  import { HandoffAnnouncement } from "./host-handoff-announcement.js";
@@ -549,7 +550,11 @@ async function runSocketHost(options, socketPath) {
549
550
  });
550
551
  // Everything the internal listener needs is initialized: the runtime milestone of a coordinated start (senpi#4602).
551
552
  reportStartupMilestone("host_runtime");
552
- boundIdentity = await listen(server, socketPath, secret);
553
+ const bind = () => listen(server, socketPath, secret);
554
+ // A host binding its own public socket keeps its registry record; a supervised one's ensure keeps it.
555
+ boundIdentity = await (hostWatchdogBinding() === undefined
556
+ ? bindRegisteredEndpoint({ agentDir: options.agentDir, socket: socketPath, log: hostLog }, bind)
557
+ : bind());
553
558
  // A key record that cannot be published safely ends this host: held connections are refused, never served keyless.
554
559
  await transport.publish(() => shutdown(1));
555
560
  if (supervisorPublicOwnerFile) {
@@ -0,0 +1,6 @@
1
+ import type { AgentSessionRuntime } from "../../core/agent-session-runtime.ts";
2
+ import { type RpcCommand, type RpcResponse } from "./rpc-types.ts";
3
+ export declare function runSetPermissionPreset(runtime: Pick<AgentSessionRuntime, "session" | "setPermissionPreset">, command: Extract<RpcCommand, {
4
+ type: "set_permission_preset";
5
+ }>): RpcResponse;
6
+ //# sourceMappingURL=permission-preset-command.d.ts.map
@@ -0,0 +1,27 @@
1
+ import { parsePermissionPresetName } from "../../core/extensions/builtin/permission-system/cli.js";
2
+ import { permissionModeOf, permissionModeReply } from "../../core/permission-mode.js";
3
+ import { RPC_ERROR_INVALID_LAUNCH_PROFILE } from "./rpc-types.js";
4
+ // The live setter a classic `--mode rpc` session has no other way to receive (it refuses `open_session`), and the
5
+ // path-free one for a session on another device. An unknown preset is refused before anything changes, so the
6
+ // session keeps the mode it had; a valid one is applied through the runtime the way an attach naming it applies it.
7
+ export function runSetPermissionPreset(runtime, command) {
8
+ if (parsePermissionPresetName(command.permissionPreset) === undefined) {
9
+ return {
10
+ id: command.id,
11
+ type: "response",
12
+ command: "set_permission_preset",
13
+ success: false,
14
+ error: `${RPC_ERROR_INVALID_LAUNCH_PROFILE}: unknown permissionPreset "${command.permissionPreset}"`,
15
+ errorCode: RPC_ERROR_INVALID_LAUNCH_PROFILE,
16
+ };
17
+ }
18
+ runtime.setPermissionPreset(command.permissionPreset);
19
+ return {
20
+ id: command.id,
21
+ type: "response",
22
+ command: "set_permission_preset",
23
+ success: true,
24
+ data: permissionModeReply(permissionModeOf(runtime.session)),
25
+ };
26
+ }
27
+ //# sourceMappingURL=permission-preset-command.js.map
@@ -6,6 +6,7 @@ import type { ClientMessageIdentity } from "../../core/client-message-identity.t
6
6
  import type { CompactionResult } from "../../core/compaction/index.ts";
7
7
  import type { ServiceTier } from "../../core/extensions/builtin/service-tier.ts";
8
8
  import type { SessionControlAdmission } from "../../core/extensions/types.ts";
9
+ import type { PermissionModeReply } from "../../core/permission-mode.ts";
9
10
  import type { ApiKeyVerification } from "../../core/verify-api-key.ts";
10
11
  import type { ClientMessageAdmission } from "./client-admission-record.ts";
11
12
  import type { RpcProjectSettings, RpcProjectTrust } from "./project-workspace-commands.ts";
@@ -234,6 +235,12 @@ export type RpcMutationResponse = {
234
235
  type: "response";
235
236
  command: "set_auto_compaction";
236
237
  success: true;
238
+ } | {
239
+ id?: string;
240
+ type: "response";
241
+ command: "set_permission_preset";
242
+ success: true;
243
+ data: PermissionModeReply;
237
244
  } | {
238
245
  id?: string;
239
246
  type: "response";
@@ -293,6 +293,10 @@ export type RpcSessionCommand = {
293
293
  id?: string;
294
294
  type: "set_auto_compaction";
295
295
  enabled: boolean;
296
+ } | {
297
+ id?: string;
298
+ type: "set_permission_preset";
299
+ permissionPreset: string;
296
300
  } | {
297
301
  id?: string;
298
302
  type: "set_auto_retry";
@@ -109,8 +109,11 @@ export type RpcSessionParkedEvent = {
109
109
  sessionId: string;
110
110
  /** Session file to reopen this session by. */
111
111
  sessionPath: string;
112
- /** `idle`: the idle sweep parked it; `unloaded`: `unload_session` did. Absent from older hosts. */
113
- reason?: "idle" | "unloaded";
112
+ /**
113
+ * `idle`: the idle sweep parked it; `unloaded`: `unload_session` did; `host_shutdown`: the host stopped with the session
114
+ * retained, and the next host on the endpoint reopens it. Absent from older hosts.
115
+ */
116
+ reason?: "idle" | "unloaded" | "host_shutdown";
114
117
  };
115
118
  /**
116
119
  * Sent only to the connections a headless engine's quiet takeover let go (senpi#5732): this connection no longer holds
@@ -3,6 +3,7 @@ import type { Model, ProviderDiagnostic, ThinkingSelection } from "@earendil-wor
3
3
  import type { AgentAbortSource } from "../../core/agent-abort-provenance.ts";
4
4
  import type { QueuedInput } from "../../core/agent-session.ts";
5
5
  import type { HoldKind } from "../../core/engine-turn-hold.ts";
6
+ import type { PermissionMode } from "../../core/permission-mode.ts";
6
7
  import type { ProviderOrigin } from "../../core/provider-origin.ts";
7
8
  import type { ServiceTier } from "../../core/extensions/builtin/service-tier.ts";
8
9
  import type { ContextUsage } from "../../core/extensions/types.ts";
@@ -105,7 +106,7 @@ export interface RpcSessionModelEntry {
105
106
  thinkingSelection?: ThinkingSelection;
106
107
  serviceTier?: ServiceTier;
107
108
  }
108
- export interface RpcSessionState {
109
+ export interface RpcSessionState extends Partial<PermissionMode> {
109
110
  /** The running turn's id; absent when no turn is running. A steer or follow-up names it as `expectedTurnId` (senpi#5776). */
110
111
  currentTurnId?: string;
111
112
  /** Latest 64 UI-answer receipts in this session generation, oldest first. */
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { existsSync } from "node:fs";
6
6
  import { sanitizeProviderDiagnostic } from "@earendil-works/pi-ai";
7
+ import { permissionModeOf } from "../../core/permission-mode.js";
7
8
  import { ProjectTrustStore } from "../../core/trust-manager.js";
8
9
  import { clientFacingIdentity } from "./client-admission-record.js";
9
10
  import { sessionQuestionBridges } from "./connection-question-bridge.js";
@@ -38,6 +39,7 @@ export function buildRpcSessionState(session, lastAbortSource) {
38
39
  lifecycle: observeLifecycle(session.lifecycle),
39
40
  pendingQuestions: sessionQuestionBridges.get(session)?.pendingQuestions(),
40
41
  pendingDialogs: sessionDialogRequests.get(session)?.snapshot() ?? [],
42
+ ...permissionModeOf(session),
41
43
  model: session.model,
42
44
  pendingModelSwitch: session.pendingModelSwitch
43
45
  ? {
@@ -1,6 +1,6 @@
1
1
  import { takeOver } from "./attach-takeover.js";
2
2
  import { compactOpen } from "./compact-open-admission.js";
3
- import { clearHandoverParkedSession } from "./host-handover-parked.js";
3
+ import { clearRetainedRecord } from "./session-router-residency.js";
4
4
  import { hostInstanceId } from "./protocol-identity.js";
5
5
  import { canonicalSessionPath } from "./session-path-key.js";
6
6
  import { recordAttachment, releaseDisconnectAttachment, releaseOwnerAttachment } from "./session-router-attachments.js";
@@ -116,8 +116,8 @@ export async function reclaimSession(host, command) {
116
116
  if (host.sessionsByConnection.get(owner)?.size === 0)
117
117
  host.sessionsByConnection.delete(owner);
118
118
  connectionLeft(host, row.sessionId, owner);
119
- if (host.handoverParkedDir !== undefined && entry.sessionPath !== undefined)
120
- await clearHandoverParkedSession(host.handoverParkedDir, entry.sessionPath).catch(() => { });
119
+ if (host.handoverParkedDir !== undefined)
120
+ await clearRetainedRecord(host, entry);
121
121
  if (claim.finalizer) {
122
122
  const binding = host.bindings.get(row.sessionId);
123
123
  binding?.cancelPendingExtensionUiRequests?.();
@@ -23,6 +23,7 @@ export class SessionEventWriterConnections extends SessionEventWriterLanes {
23
23
  record.type === "host_request" ||
24
24
  record.type === "relocation_due" ||
25
25
  (record.type === "extension_ui_request" &&
26
+ record.audience !== "session" &&
26
27
  ["select", "confirm", "input", "editor"].includes(String(record.method))));
27
28
  }
28
29
  isSessionSealed(sessionId) {
@@ -51,7 +51,7 @@ export declare class SessionEventWriter extends SessionEventWriterDestinations {
51
51
  * session's reaches every connection. No client asked for this teardown, so there is
52
52
  * no close response to answer.
53
53
  */
54
- parkSession(sessionId: string, sessionPath: string, reason: "idle" | "unloaded"): void;
54
+ parkSession(sessionId: string, sessionPath: string, reason: "idle" | "unloaded" | "host_shutdown"): void;
55
55
  /**
56
56
  * Seal a session `release_session` handed to a runtime outside this host and publish
57
57
  * `session_closed { reason: "released", sessionPath }`. Unlike a park the file must NOT be reopened
@@ -1,12 +1,3 @@
1
- /**
2
- * Attach-on-open: a live session outlives individual client attachments, so a resume (or a second
3
- * surface) for an already-hosted path joins the existing runtime instead of failing. An entry
4
- * still opening holds the reservation, and an explicit open for that path waits on the entry's
5
- * openCompletion (bounded on the waiter only, default 45s) and attaches once it opens; on that
6
- * wait expiring, only the one waiting reopen fails with a retryable session_path_in_use
7
- * {reason: "open_in_progress", retry_after_ms} naming an attached current owner, and the in-flight
8
- * open keeps running untouched.
9
- */
10
1
  import type { SessionPathReservations } from "./host-reservations.ts";
11
2
  import { type OpenRpcSession, type RpcSessionEntry, type RpcSessionLaunchProfile, type RpcSessionOpenOptions } from "./session-registry-types.ts";
12
3
  export declare function attachToOpenSession(entries: ReadonlyMap<string, RpcSessionEntry>, sessionPath: string, profile: RpcSessionLaunchProfile, options: RpcSessionOpenOptions | undefined, pathReservations: SessionPathReservations | undefined, now: number): Promise<OpenRpcSession>;
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Attach-on-open: a live session outlives individual client attachments, so a resume (or a second
3
+ * surface) for an already-hosted path joins the existing runtime instead of failing. An entry
4
+ * still opening holds the reservation, and an explicit open for that path waits on the entry's
5
+ * openCompletion (bounded on the waiter only, default 45s) and attaches once it opens; on that
6
+ * wait expiring, only the one waiting reopen fails with a retryable session_path_in_use
7
+ * {reason: "open_in_progress", retry_after_ms} naming an attached current owner, and the in-flight
8
+ * open keeps running untouched.
9
+ */
10
+ import { clientNamedPreset, withClientPreset } from "../../core/permission-mode.js";
1
11
  import { hostInstanceId } from "./protocol-identity.js";
2
12
  import { frozenProfile, RpcSessionRegistryError, } from "./session-registry-types.js";
3
13
  /** How long ONE explicit reopen waits on an in-flight open before giving up (the open keeps going). */
@@ -104,9 +114,12 @@ export async function attachToOpenSession(entries, sessionPath, profile, options
104
114
  }
105
115
  // The preset is applied even when the record already names it: the record is not the live session,
106
116
  // so a resent attach also repairs a session that drifted from it (senpi#2842).
107
- if (profile.permissionPreset !== undefined) {
108
- entry.profile = frozenProfile({ ...entry.profile, permissionPreset: profile.permissionPreset });
109
- entry.runtime?.setPermissionPreset(profile.permissionPreset);
117
+ // Only a preset the client named moves the session: an embedder's host default folded into a preset-less attach is
118
+ // not a choice, and applying it would turn the host default into a client-named mode (remote approval-mode rule).
119
+ const namedPreset = clientNamedPreset(profile);
120
+ if (namedPreset !== undefined) {
121
+ entry.profile = frozenProfile(withClientPreset(entry.profile, namedPreset));
122
+ entry.runtime?.setPermissionPreset(namedPreset);
110
123
  }
111
124
  entry.lastCommandAt = now;
112
125
  if (wasParked) {
@@ -57,6 +57,8 @@ export interface RpcSessionEntry {
57
57
  * Requested per `open_session`; an attach may turn it on, never off.
58
58
  */
59
59
  retainOnDisconnect?: boolean;
60
+ /** The session path this host's handover-parked record entry is keyed by; it follows a transcript switch. */
61
+ recordedPath?: string;
60
62
  /** Timestamp of the last routed command / observed activity; drives idle eviction. */
61
63
  lastCommandAt: number;
62
64
  /**
@@ -14,6 +14,7 @@ export declare class RpcSessionRegistry {
14
14
  readonly closeGraceMs: number;
15
15
  /** Present only when the runtime factory can build a session's services alone (`host-warm.ts`). */
16
16
  readonly warm?: HostWarm;
17
+ private readonly warmsInFlight?;
17
18
  /** The agent directory this host keeps its sessions (and session-move folders) under. */
18
19
  get agentDir(): string;
19
20
  constructor(options: RpcSessionRegistryOptions);
@@ -22,6 +23,7 @@ export declare class RpcSessionRegistry {
22
23
  /** Daemon-family identities come from the endpoint's claims and generation registry. */
23
24
  holderPids(observedStarts?: ReadonlyMap<number, number | undefined>): Promise<readonly number[]>;
24
25
  openSession(requested: RpcSessionLaunchProfile, options?: RpcSessionOpenOptions): Promise<OpenRpcSession>;
26
+ private openNow;
25
27
  /**
26
28
  * Read-only lookup with no state transitions or attachment accounting.
27
29
  * Exists so lifecycle decisions (e.g. deferring a dropped connection's
@@ -1,6 +1,6 @@
1
1
  import { isAbsolute } from "node:path";
2
2
  import { assertValidSessionId } from "../../core/session-manager.js";
3
- import { createRegistryWarm } from "./host-warm.js";
3
+ import { createRegistryWarm, waitForWarms } from "./host-warm.js";
4
4
  import { refreshesSessionActivity } from "./session-command-activity.js";
5
5
  import { publishedSessionContext } from "./session-context-patch.js";
6
6
  import { attachLiveSession, markPathAttached } from "./session-live-attachment.js";
@@ -20,11 +20,14 @@ export class RpcSessionRegistry {
20
20
  this.reservations = new Set();
21
21
  this.pendingClaims = new Map();
22
22
  this.nextHandle = 0;
23
- if (options.createRuntime.prepare)
24
- this.warm = createRegistryWarm(options.createRuntime.prepare, {
23
+ if (options.createRuntime.prepare) {
24
+ const warming = createRegistryWarm(options.createRuntime.prepare, {
25
25
  agentDir: options.agentDir,
26
26
  ...(options.mcpRegistry !== undefined ? { mcpRegistry: options.mcpRegistry } : {}),
27
27
  });
28
+ this.warm = warming.warm;
29
+ this.warmsInFlight = warming.inFlight;
30
+ }
28
31
  this.options =
29
32
  options.mcpRegistry === undefined
30
33
  ? options
@@ -70,6 +73,16 @@ export class RpcSessionRegistry {
70
73
  return this.options.pathReservations?.holderPids?.(observedStarts) ?? Promise.resolve([]);
71
74
  }
72
75
  openSession(requested, options) {
76
+ // A warm in flight (the boot load of senpi#6147 included) loads on this loop and yields it between modules
77
+ // (senpi#6199): an open builds its runtime after the warms of ITS cwd that are loading when it arrives, so
78
+ // its factories never run beside that load. Other cwds and later warms are not waited on, and the wait is
79
+ // bounded by `closeGraceMs` like the closing-reservation wait.
80
+ const warming = this.warmsInFlight?.(requested.cwd);
81
+ if (warming === undefined)
82
+ return this.openNow(requested, options);
83
+ return waitForWarms(warming, this.closeGraceMs, requested.cwd).then(() => this.openNow(requested, options));
84
+ }
85
+ openNow(requested, options) {
73
86
  return openRegistrySession({
74
87
  entries: this.entries,
75
88
  reservations: this.reservations,
@@ -1,8 +1,30 @@
1
- import { clearHandoverParkedSession } from "./host-handover-parked.js";
2
1
  import { RPC_ERROR_UNKNOWN_SESSION } from "./rpc-types.js";
3
2
  import { forgetSessionOwnership, releaseOwnerAttachment } from "./session-router-attachments.js";
4
3
  import { code, error } from "./session-router-errors.js";
5
4
  import { stopSweep } from "./session-router-occupancy.js";
5
+ import { clearRetainedRecord, recordRetainedSession } from "./session-router-residency.js";
6
+ /** How long a stop lets a cut turn settle before its teardown proceeds anyway: the default close grace. */
7
+ const HOST_SHUTDOWN_ABORT_BOUND_MS = 10_000;
8
+ /** Ends the running turn as a host shutdown (senpi#6190), bounded so a run its abort cannot release never holds the stop. */
9
+ async function abortForHostShutdownWithin(sessionId, entry) {
10
+ // Async wrapper: a session that throws synchronously still only fails its own abort, never the whole stop.
11
+ const aborting = (async () => {
12
+ await entry?.runtime?.session.abortForHostShutdown();
13
+ })().catch((cause) => {
14
+ process.stderr.write(`senpi rpc host shutdown abort ${sessionId} failed: ${String(cause)}\n`);
15
+ });
16
+ let timer;
17
+ const bound = new Promise((resolve) => {
18
+ timer = setTimeout(resolve, HOST_SHUTDOWN_ABORT_BOUND_MS);
19
+ timer.unref?.();
20
+ });
21
+ try {
22
+ await Promise.race([aborting, bound]);
23
+ }
24
+ finally {
25
+ clearTimeout(timer);
26
+ }
27
+ }
6
28
  /**
7
29
  * Releases one attachment (or every attachment when draining) and reports
8
30
  * whether this caller became the finalizer. The join token is installed inside
@@ -66,10 +88,10 @@ export async function close(host, command) {
66
88
  releaseOwnerAttachment(host, owner, command.sessionId);
67
89
  if (claim.finalizer) {
68
90
  // An explicit close ends the session for good: drop any handover-parked record entry, or
69
- // the session would come back on the next host start (MEDIUM-1, senpi#3282 review).
70
- const entry = host.registry.peek(command.sessionId);
71
- if (host.handoverParkedDir !== undefined && entry?.sessionPath !== undefined)
72
- await clearHandoverParkedSession(host.handoverParkedDir, entry.sessionPath).catch(() => { });
91
+ // the session would come back on the next host start (MEDIUM-1, senpi#3282 review). Awaited only when a record
92
+ // exists, so a host without one starts the teardown in the same tick as the claim.
93
+ if (host.handoverParkedDir !== undefined)
94
+ await clearRetainedRecord(host, host.registry.peek(command.sessionId));
73
95
  await finalizeClose(host, command.sessionId, host.bindings.get(command.sessionId), () => reply.complete(true, "client_close"));
74
96
  }
75
97
  else {
@@ -189,11 +211,26 @@ export async function dispose(host) {
189
211
  if (host.admissions)
190
212
  await host.admissions.dispose();
191
213
  await Promise.all([...host.bindings.entries()].map(async ([sessionId, binding]) => {
214
+ const entry = host.registry.peek(sessionId);
215
+ // A retained session outlives this process: it parks under the transcript it is on now (its record stays
216
+ // for the next host on the endpoint, which reopens it) instead of closing, so attached clients do not read
217
+ // it as ended.
218
+ const parkedPath = host.handoverParkedDir !== undefined && entry?.retainOnDisconnect === true
219
+ ? (entry.runtime?.session.sessionFile ?? entry.sessionPath)
220
+ : undefined;
221
+ // Claimed before any await, like close(): the stop fences the session in the tick it starts.
192
222
  const claim = tryClaimClose(host, sessionId, { drainAttachments: true });
193
223
  if (!claim)
194
224
  return;
195
225
  if (claim.finalizer) {
196
- await finalizeClose(host, sessionId, binding, () => host.writer.closeSession(sessionId, { type: "response", command: "close_session", success: true, data: {} }, "host_shutdown"));
226
+ await abortForHostShutdownWithin(sessionId, entry);
227
+ if (entry !== undefined && parkedPath !== undefined)
228
+ await recordRetainedSession(host, entry, parkedPath, false).catch((cause) => {
229
+ process.stderr.write(`senpi rpc host shutdown park record ${sessionId} failed: ${String(cause)}\n`);
230
+ });
231
+ await finalizeClose(host, sessionId, binding, () => parkedPath !== undefined
232
+ ? host.writer.parkSession(sessionId, parkedPath, "host_shutdown")
233
+ : host.writer.closeSession(sessionId, { type: "response", command: "close_session", success: true, data: {} }, "host_shutdown"));
197
234
  }
198
235
  else
199
236
  await host.finalizations.get(sessionId)?.promise;
@@ -10,6 +10,7 @@ import { SessionAttachOrder } from "./session-attach-order.js";
10
10
  import { tearDownReleased } from "./session-router-close.js";
11
11
  import { routingIdOf } from "./session-router-errors.js";
12
12
  import { sweepIdleSessions } from "./session-router-occupancy.js";
13
+ import { followRetainedRecord } from "./session-router-residency.js";
13
14
  import { registryStatusSource } from "./session-status-source.js";
14
15
  export class RouterContext {
15
16
  constructor(registry, writer, defaults, createBinding = createRpcSessionBinding, connectionOptions = {}, idle) {
@@ -74,6 +75,7 @@ export class RouterContext {
74
75
  : { effectId: latest.effectId, kind: latest.kind, state: latest.state };
75
76
  });
76
77
  writer.afterRecord = (sessionId) => {
78
+ followRetainedRecord(this, sessionId);
77
79
  const session = this.registry.peek(sessionId)?.runtime?.session;
78
80
  if (session === undefined)
79
81
  return;
@@ -1,10 +1,8 @@
1
- import { recordHandoverParkedSession } from "./host-handover-parked.js";
2
- import { parkedEntryFromProfile } from "./host-handover-parked-open.js";
3
- import { hostInstanceId } from "./protocol-identity.js";
4
1
  import { readDrainVerdicts } from "./session-drain.js";
5
2
  import { holdUnattendedTurns } from "./session-drain-hold.js";
6
3
  import { forgetSessionOwnership } from "./session-router-attachments.js";
7
4
  import { finalizeClose, tryClaimClose } from "./session-router-close.js";
5
+ import { recordRetainedSession } from "./session-router-residency.js";
8
6
  import { selectSweepEvictions } from "./session-sweep.js";
9
7
  const DRAIN_SWEEP_MS = 50;
10
8
  /**
@@ -128,14 +126,7 @@ export async function evictIdleSession(host, sessionId, reason) {
128
126
  parkedPath !== undefined &&
129
127
  retained?.retainOnDisconnect === true &&
130
128
  retained.sessionPath !== undefined) {
131
- await recordHandoverParkedSession(host.handoverParkedDir, {
132
- ...parkedEntryFromProfile({
133
- sessionPath: retained.sessionPath,
134
- contextOverlay: retained.contextOverlay,
135
- ...(retained.durableSessionId !== undefined ? { durableSessionId: retained.durableSessionId } : {}),
136
- }, retained.profile, Object.keys(host.defaults.hostContext ?? {})),
137
- parkedBy: host.defaults.hostContext?.host_instance ?? hostInstanceId(),
138
- }).catch((cause) => {
129
+ await recordRetainedSession(host, retained, retained.sessionPath, false).catch((cause) => {
139
130
  process.stderr.write(`senpi rpc handover park record ${sessionId} failed: ${String(cause)}\n`);
140
131
  });
141
132
  }
@@ -70,6 +70,10 @@ export function validatedOpenProfile(host, command, options) {
70
70
  : { host_socket: host.defaults.hostContext.host_socket }),
71
71
  },
72
72
  permissionPreset: command.permissionPreset ?? defaults?.permissionPreset,
73
+ // An embedder's host-level preset is the host default, never a client's choice (remote approval-mode rule).
74
+ ...(command.permissionPreset === undefined && defaults?.permissionPreset !== undefined
75
+ ? { permissionPresetSource: "host_default" }
76
+ : {}),
73
77
  creationModel: command.provider && command.modelId
74
78
  ? { provider: command.provider, modelId: command.modelId }
75
79
  : defaults?.creationModel,