@code-yeongyu/senpi 2026.10.10-4 → 2026.10.10-6

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 (130) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/bundle/chunks/{anthropic-messages-OE3QBX4P.js → anthropic-messages-DUULMQBF.js} +1 -1
  3. package/dist/bundle/chunks/{app-server-command-N3L2LNP7.js → app-server-command-GJ5NXQSY.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-3A3C5VVO.js → chunk-3OGEJMZ7.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-J5MUMVWB.js → chunk-64ZVBCTL.js} +8 -7
  6. package/dist/bundle/chunks/{chunk-MP2DMTI7.js → chunk-C7LEZMWS.js} +124 -124
  7. package/dist/bundle/chunks/{chunk-ZSYRWQWG.js → chunk-JMI5OCIJ.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-ODYQK5XJ.js → chunk-TALS4X7S.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-7GVD54RM.js → chunk-U3IL5N76.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-H67EVDWG.js → chunk-URQSNRHZ.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-H4UIUFU5.js → chunk-VGYIPYOK.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-M2IHGL3M.js → chunk-VW74BGIC.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-4TK7VCZZ.js → chunk-WKMUAG23.js} +1 -1
  14. package/dist/bundle/chunks/chunk-YG5UO3E4.js +7 -0
  15. package/dist/bundle/chunks/chunk-YV2XQFXG.js +14 -0
  16. package/dist/bundle/chunks/{chunk-Z7MMKKIY.js → chunk-ZYHIRFGT.js} +1 -1
  17. package/dist/bundle/chunks/{cli-main-CHDR33FZ.js → cli-main-SZ54SQLA.js} +1 -1
  18. package/dist/bundle/chunks/{help-fast-path-EHRZEBRO.js → help-fast-path-W5W3FYYM.js} +1 -1
  19. package/dist/bundle/chunks/{host-command-VATE7HRG.js → host-command-YG2FIDPN.js} +1 -1
  20. package/dist/bundle/chunks/{host-lifecycle-GC3GIR2N.js → host-lifecycle-5AV4JXDQ.js} +1 -1
  21. package/dist/bundle/chunks/{interactive-mode-RVEM62WX.js → interactive-mode-SCNDQ4R6.js} +1 -1
  22. package/dist/bundle/chunks/{list-tips-IIRFXBTO.js → list-tips-BXAA4FG5.js} +1 -1
  23. package/dist/bundle/chunks/{multi-session-host-CFND7CXH.js → multi-session-host-ZDZMPNP5.js} +7 -6
  24. package/dist/bundle/chunks/{package-manager-cli-NWNECBF7.js → package-manager-cli-4LMYHHKH.js} +1 -1
  25. package/dist/bundle/chunks/{rotation-stream-C5BM7TMM.js → rotation-stream-E3XKSST7.js} +1 -1
  26. package/dist/bundle/chunks/{rpc-mode-JQFUDLNB.js → rpc-mode-ZFNFREN5.js} +1 -1
  27. package/dist/bundle/chunks/{schedule-command-EQ4PQCRP.js → schedule-command-K6CTAB4T.js} +1 -1
  28. package/dist/bundle/chunks/{session-control-endpoint-HMB5T5VD.js → session-control-endpoint-MVSYBGM3.js} +1 -1
  29. package/dist/bundle/chunks/{session-picker-O7OESJWU.js → session-picker-TNXWY5QY.js} +1 -1
  30. package/dist/bundle/chunks/session-worker.js +224 -222
  31. package/dist/bundle/cli.js +1 -1
  32. package/dist/bundle/index.js +1 -1
  33. package/dist/bundle/rpc-entry.js +1 -1
  34. package/dist/bundle/runtime-manifest.json +1 -1
  35. package/dist/core/agent-session-runtime.d.ts +6 -0
  36. package/dist/core/agent-session-runtime.js +18 -0
  37. package/dist/core/agent-session.js +5 -2
  38. package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +3 -2
  39. package/dist/core/extensions/builtin/anthropic-subscription/options.js +11 -2
  40. package/dist/core/extensions/builtin/anthropic-subscription/session-reattach.js +4 -1
  41. package/dist/core/extensions/builtin/anthropic-subscription/session-sync.js +3 -0
  42. package/dist/core/extensions/builtin/anthropic-subscription/settings.d.ts +5 -0
  43. package/dist/core/extensions/builtin/anthropic-subscription/settings.js +11 -0
  44. package/dist/core/extensions/builtin/anthropic-subscription/stream.d.ts +1 -0
  45. package/dist/core/extensions/builtin/anthropic-subscription/stream.js +17 -1
  46. package/dist/core/extensions/builtin/compaction/lane-policy.d.ts +20 -12
  47. package/dist/core/extensions/builtin/compaction/lane-policy.js +39 -34
  48. package/dist/core/extensions/builtin/html-render/bootstrap.d.ts +15 -0
  49. package/dist/core/extensions/builtin/html-render/bootstrap.js +123 -0
  50. package/dist/core/extensions/builtin/html-render/images.d.ts +16 -0
  51. package/dist/core/extensions/builtin/html-render/images.js +155 -0
  52. package/dist/core/extensions/builtin/html-render/index.d.ts +3 -0
  53. package/dist/core/extensions/builtin/html-render/index.js +5 -0
  54. package/dist/core/extensions/builtin/html-render/tool.d.ts +19 -0
  55. package/dist/core/extensions/builtin/html-render/tool.js +74 -0
  56. package/dist/core/extensions/builtin/index.js +4 -0
  57. package/dist/core/extensions/builtin/mcp/auth/catalog-identity.d.ts +4 -0
  58. package/dist/core/extensions/builtin/mcp/auth/catalog-identity.js +41 -0
  59. package/dist/core/extensions/builtin/mcp/auth/legacy-lock.d.ts +3 -0
  60. package/dist/core/extensions/builtin/mcp/auth/legacy-lock.js +39 -0
  61. package/dist/core/extensions/builtin/mcp/auth/legacy-migration.d.ts +14 -0
  62. package/dist/core/extensions/builtin/mcp/auth/legacy-migration.js +91 -0
  63. package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.js +1 -1
  64. package/dist/core/extensions/builtin/mcp/auth/token-store.d.ts +1 -0
  65. package/dist/core/extensions/builtin/mcp/auth/token-store.js +19 -90
  66. package/dist/core/extensions/builtin/mcp/catalog-cache.d.ts +7 -3
  67. package/dist/core/extensions/builtin/mcp/catalog-cache.js +61 -12
  68. package/dist/core/extensions/builtin/mcp/catalog.d.ts +3 -1
  69. package/dist/core/extensions/builtin/mcp/catalog.js +2 -0
  70. package/dist/core/extensions/builtin/mcp/connection-types.d.ts +2 -0
  71. package/dist/core/extensions/builtin/mcp/expose/call.d.ts +12 -0
  72. package/dist/core/extensions/builtin/mcp/expose/call.js +60 -0
  73. package/dist/core/extensions/builtin/mcp/expose/proxy.js +16 -3
  74. package/dist/core/extensions/builtin/mcp/expose/register.d.ts +3 -1
  75. package/dist/core/extensions/builtin/mcp/expose/register.js +41 -50
  76. package/dist/core/extensions/builtin/mcp/expose/session.d.ts +4 -0
  77. package/dist/core/extensions/builtin/mcp/expose/session.js +5 -1
  78. package/dist/core/extensions/builtin/mcp/expose/tier-b.js +15 -4
  79. package/dist/core/extensions/builtin/mcp/index.js +5 -26
  80. package/dist/core/extensions/builtin/mcp/invocation.d.ts +42 -0
  81. package/dist/core/extensions/builtin/mcp/invocation.js +118 -0
  82. package/dist/core/extensions/builtin/mcp/service-connection.d.ts +3 -0
  83. package/dist/core/extensions/builtin/mcp/service-connection.js +6 -0
  84. package/dist/core/extensions/builtin/mcp/service-register.d.ts +4 -0
  85. package/dist/core/extensions/builtin/mcp/service-register.js +45 -1
  86. package/dist/core/extensions/builtin/mcp/service-tools-changed.js +26 -3
  87. package/dist/core/extensions/builtin/mcp/service-types.d.ts +6 -0
  88. package/dist/core/extensions/builtin/mcp/service.js +83 -49
  89. package/dist/core/extensions/builtin/mcp/shared-connection.js +17 -4
  90. package/dist/core/extensions/builtin/mcp/sharing-policy.js +1 -0
  91. package/dist/core/extensions/builtin/mcp/startup-race.js +47 -9
  92. package/dist/core/extensions/builtin/permission-system/dispatch-metadata.d.ts +8 -0
  93. package/dist/core/extensions/builtin/permission-system/dispatch-metadata.js +23 -0
  94. package/dist/core/extensions/builtin/permission-system/dispatch-policy.d.ts +14 -0
  95. package/dist/core/extensions/builtin/permission-system/dispatch-policy.js +53 -0
  96. package/dist/core/extensions/builtin/permission-system/dispatch.d.ts +33 -0
  97. package/dist/core/extensions/builtin/permission-system/dispatch.js +106 -0
  98. package/dist/core/extensions/builtin/permission-system/index.js +64 -17
  99. package/dist/core/extensions/builtin/permission-system/prompt.d.ts +1 -1
  100. package/dist/core/extensions/builtin/permission-system/prompt.js +3 -1
  101. package/dist/core/extensions/builtin/permission-system/service.d.ts +11 -1
  102. package/dist/core/extensions/builtin/permission-system/service.js +14 -4
  103. package/dist/core/extensions/builtin/ttsr/detectors/collapse-near-duplicates.d.ts +1 -0
  104. package/dist/core/extensions/builtin/ttsr/detectors/collapse-near-duplicates.js +24 -5
  105. package/dist/index.d.ts +1 -1
  106. package/dist/index.js +1 -1
  107. package/dist/modes/interactive/interactive-mode.js +21 -4
  108. package/dist/modes/rpc/host-reservations.d.ts +1 -1
  109. package/dist/modes/rpc/host-reservations.js +9 -3
  110. package/dist/modes/rpc/rpc-client.js +14 -2
  111. package/dist/modes/rpc/rpc-request-deadline.d.ts +8 -0
  112. package/dist/modes/rpc/rpc-request-deadline.js +10 -0
  113. package/dist/modes/rpc/rpc-types.d.ts +7 -4
  114. package/dist/modes/rpc/session-binding.js +15 -1
  115. package/dist/modes/rpc/session-command-router.d.ts +1 -0
  116. package/dist/modes/rpc/session-command-router.js +16 -0
  117. package/dist/modes/rpc/session-registry-attach.js +8 -2
  118. package/dist/modes/rpc/session-registry.js +5 -7
  119. package/dist/modes/rpc/session-teardown.d.ts +6 -1
  120. package/dist/modes/rpc/session-teardown.js +26 -7
  121. package/dist/modes/rpc/session-worker-client.d.ts +2 -0
  122. package/dist/modes/rpc/session-worker-client.js +6 -0
  123. package/dist/modes/rpc/session-worker-protocol.d.ts +4 -0
  124. package/dist/modes/rpc/session-worker.js +6 -0
  125. package/dist/modes/rpc/worker-session-registry.js +6 -1
  126. package/docs/providers.md +3 -2
  127. package/docs/rpc.md +26 -10
  128. package/package.json +6 -6
  129. package/dist/bundle/chunks/chunk-RADHRQ4K.js +0 -7
  130. package/dist/bundle/chunks/chunk-USRAHAW4.js +0 -14
@@ -11,7 +11,7 @@ import { attachToOpenSession } from "./session-registry-attach.js";
11
11
  import { settleClosingReservation, syncRuntimeMetadata } from "./session-registry-claims.js";
12
12
  import { createEntrySwitchSession } from "./session-registry-switch.js";
13
13
  import { frozenProfile, RpcSessionRegistryError, sessionIdentity, } from "./session-registry-types.js";
14
- import { beginSessionClose, closeMarkedSession, closeSession } from "./session-teardown.js";
14
+ import { beginSessionClose, closeMarkedSession, closeSession, releaseWithinGrace, } from "./session-teardown.js";
15
15
  export { frozenProfile, RpcSessionRegistryError, sessionIdentity, } from "./session-registry-types.js";
16
16
  /** Process-local lifecycle owner for multi-session RPC runtimes. */
17
17
  export class RpcSessionRegistry {
@@ -41,9 +41,9 @@ export class RpcSessionRegistry {
41
41
  this.options.onSizeChange?.(this.entries.size);
42
42
  return deleted;
43
43
  },
44
- releaseReservation: (key) => {
44
+ releaseReservation: async (key) => {
45
+ await this.options.pathReservations?.release(key);
45
46
  this.reservations.delete(key);
46
- this.options.pathReservations?.release(key);
47
47
  },
48
48
  markDetached: (key) => this.options.pathReservations?.setAttached(key, false),
49
49
  now: () => this.now(),
@@ -174,10 +174,8 @@ export class RpcSessionRegistry {
174
174
  finally {
175
175
  this.entries.delete(handle);
176
176
  this.options.onSizeChange?.(this.entries.size);
177
- if (sessionPath) {
178
- this.reservations.delete(sessionPath);
179
- this.options.pathReservations?.release(sessionPath);
180
- }
177
+ if (sessionPath)
178
+ await releaseWithinGrace(this.teardownHost, handle, sessionPath);
181
179
  }
182
180
  }
183
181
  if (error instanceof RpcSessionRegistryError)
@@ -3,7 +3,7 @@ export interface SessionTeardownHost {
3
3
  readonly closeGraceMs: number;
4
4
  get(handle: string): RpcSessionEntry | undefined;
5
5
  delete(handle: string): void;
6
- releaseReservation(key: string): void;
6
+ releaseReservation(key: string): Promise<void>;
7
7
  /** Publishes that this path is retained with no client attached, for the cross-generation claim. */
8
8
  markDetached(key: string): void;
9
9
  /** The registry's clock, so the detach stamp shares the sweep's timeline. */
@@ -13,6 +13,11 @@ export interface SessionTeardownHost {
13
13
  export declare function beginSessionClose(host: SessionTeardownHost, handle: string, onRole?: (finalizer: boolean) => void, options?: {
14
14
  detach?: boolean;
15
15
  }): RpcSessionEntry;
16
+ /**
17
+ * Close waits for the claim file to be gone, but a removal that never settles (a wedged mount)
18
+ * must not hold the close past the grace window: the close completes and the failure is reported.
19
+ */
20
+ export declare function releaseWithinGrace(host: Pick<SessionTeardownHost, "closeGraceMs" | "releaseReservation">, handle: string, key: string): Promise<void>;
16
21
  export declare function closeSession(host: SessionTeardownHost, handle: string): Promise<void>;
17
22
  export declare function closeMarkedSession(host: SessionTeardownHost, handle: string): Promise<void>;
18
23
  //# sourceMappingURL=session-teardown.d.ts.map
@@ -40,6 +40,25 @@ export function beginSessionClose(host, handle, onRole, options) {
40
40
  });
41
41
  return entry;
42
42
  }
43
+ /**
44
+ * Close waits for the claim file to be gone, but a removal that never settles (a wedged mount)
45
+ * must not hold the close past the grace window: the close completes and the failure is reported.
46
+ */
47
+ export async function releaseWithinGrace(host, handle, key) {
48
+ let timer;
49
+ const late = new Promise((resolve) => {
50
+ timer = setTimeout(() => resolve("late"), host.closeGraceMs);
51
+ });
52
+ const removal = host.releaseReservation(key).then(() => "removed", (cause) => {
53
+ reportDetachedFailure(handle, cause);
54
+ return "failed";
55
+ });
56
+ const outcome = await Promise.race([removal, late]);
57
+ clearTimeout(timer);
58
+ if (outcome === "late") {
59
+ reportDetachedFailure(handle, new Error(`session path reservation was not removed within ${host.closeGraceMs} ms; closing anyway`));
60
+ }
61
+ }
43
62
  export function closeSession(host, handle) {
44
63
  const entry = beginSessionClose(host, handle);
45
64
  if (entry.state !== "closing")
@@ -80,16 +99,16 @@ export function closeMarkedSession(host, handle) {
80
99
  await disposeOnce().catch(() => undefined);
81
100
  await entry.scope.close?.();
82
101
  };
83
- const release = () => {
102
+ const release = async () => {
84
103
  if (released)
85
104
  return;
86
105
  released = true;
87
106
  if (releaseTimer)
88
107
  clearTimeout(releaseTimer);
108
+ if (entry.reservationKey)
109
+ await releaseWithinGrace(host, handle, entry.reservationKey);
89
110
  entry.state = "closed";
90
111
  host.delete(handle);
91
- if (entry.reservationKey)
92
- host.releaseReservation(entry.reservationKey);
93
112
  };
94
113
  const graceful = (async () => {
95
114
  await previousLifecycle;
@@ -119,22 +138,22 @@ export function closeMarkedSession(host, handle) {
119
138
  }
120
139
  })();
121
140
  let settled = false;
122
- const finish = () => {
141
+ const finish = async () => {
123
142
  if (settled)
124
143
  return;
125
144
  settled = true;
126
- release();
145
+ await release();
127
146
  entry.closeResolve?.();
128
147
  };
129
148
  releaseTimer = setTimeout(() => {
130
149
  void disposeOnce().catch((cause) => reportDetachedFailure(handle, cause));
131
150
  void closeScopeOnce().catch((cause) => reportDetachedFailure(handle, cause));
132
151
  void graceful.catch((cause) => reportDetachedFailure(handle, cause));
133
- finish();
152
+ void finish();
134
153
  }, host.closeGraceMs);
135
154
  void graceful.then(finish, (cause) => {
136
155
  reportDetachedFailure(handle, cause);
137
- finish();
156
+ return finish();
138
157
  });
139
158
  return completion;
140
159
  }
@@ -40,6 +40,8 @@ export declare class SessionWorkerClient {
40
40
  setPromptSurface(surface: PromptSurface): Promise<void>;
41
41
  /** Moves the worker's live session to another browser engine (a later `open_session.browserEngine`). */
42
42
  setBrowserEngine(engine: BrowserEngine): Promise<void>;
43
+ /** Moves the worker's live session to another permission preset (a later `open_session.permissionPreset`). */
44
+ setPermissionPreset(preset: string): Promise<void>;
43
45
  commit(): Promise<WorkerSnapshot>;
44
46
  bind(sessionId: string, writer: SessionEventWriter, requestClose: () => void, options: Pick<RpcConnectionOptions, "capabilities" | "clientInfo">): Promise<RpcSessionBinding>;
45
47
  get busy(): boolean;
@@ -60,6 +60,12 @@ export class SessionWorkerClient {
60
60
  if (result.type !== "result")
61
61
  throw new Error("Invalid worker browser_engine response");
62
62
  }
63
+ /** Moves the worker's live session to another permission preset (a later `open_session.permissionPreset`). */
64
+ async setPermissionPreset(preset) {
65
+ const result = await this.request({ type: "permission_preset", preset });
66
+ if (result.type !== "result")
67
+ throw new Error("Invalid worker permission_preset response");
68
+ }
63
69
  async commit() {
64
70
  const result = await this.request({ type: "commit" });
65
71
  if (result.type !== "ready")
@@ -60,6 +60,10 @@ export type HostToSessionWorker = {
60
60
  type: "browser_engine";
61
61
  request: number;
62
62
  engine: BrowserEngine;
63
+ } | {
64
+ type: "permission_preset";
65
+ request: number;
66
+ preset: string;
63
67
  } | {
64
68
  type: "cancel_ui";
65
69
  } | {
@@ -183,6 +183,12 @@ async function handle(message) {
183
183
  entry.runtime.setBrowserEngine(message.engine);
184
184
  send({ type: "result", request: message.request });
185
185
  return;
186
+ case "permission_preset":
187
+ if (!entry?.runtime)
188
+ throw new Error("session_closing");
189
+ entry.runtime.setPermissionPreset(message.preset);
190
+ send({ type: "result", request: message.request });
191
+ return;
186
192
  case "cancel_ui":
187
193
  binding?.cancelPendingExtensionUiRequests?.();
188
194
  send({ type: "control_done", control: "cancel_ui" });
@@ -233,7 +233,7 @@ export class WorkerSessionRegistry {
233
233
  return undefined;
234
234
  }
235
235
  async attach(owner, path, options, requested = { cwd: "" }) {
236
- const { promptSurface, browserEngine } = requested;
236
+ const { promptSurface, browserEngine, permissionPreset } = requested;
237
237
  const entry = this.entries.get(owner);
238
238
  if (entry?.state !== "open" || !entry.worker?.bindingReady || entry.worker.snapshot?.sessionPath !== path)
239
239
  throw new RpcSessionRegistryError("session_path_in_use");
@@ -246,6 +246,11 @@ export class WorkerSessionRegistry {
246
246
  entry.profile = frozenProfile({ ...entry.profile, browserEngine });
247
247
  await entry.worker.setBrowserEngine(browserEngine);
248
248
  }
249
+ // Sent even when the record already names it: only the worker sees its live session (senpi#2842).
250
+ if (permissionPreset !== undefined) {
251
+ entry.profile = frozenProfile({ ...entry.profile, permissionPreset });
252
+ await entry.worker.setPermissionPreset(permissionPreset);
253
+ }
249
254
  const result = this.openResult(owner, entry);
250
255
  entry.attachments++;
251
256
  // Retention is a property of the live session: any attach may ask for it, and
package/docs/providers.md CHANGED
@@ -54,7 +54,7 @@ The `anthropic-subscription` provider routes LLM calls through the official [Cla
54
54
  - Settings (`anthropicSubscriptionProvider`):
55
55
  - `systemPromptMode` — controls how the system prompt is delivered. **`full`** (default) sends senpi's composed system prompt verbatim; the lane no longer rebuilds from the SDK `claude_code` preset, so all prompt regions (project rules, response-language instructions, etc.) reach the model. **`preset-append`** is the previous behaviour (deprecated, kept for one release; emits a one-time warning). **`override`** loads the system prompt from a file (`systemPromptFile`). The legacy `appendSystemPrompt` key still works: `false` → `preset-append`, `true`/unset → `full`; setting both keys makes `systemPromptMode` win and warns.
56
56
  - In `full` and `override` modes, `settingSources` defaults to `[]` on every lane because senpi's prompt already carries project context — loading the SDK's own CLAUDE.md would double-inject it. The CLI always prepends its own `"You are a Claude agent, built on Anthropic's Claude Agent SDK."` block, which senpi cannot suppress; `full` means the prompt is delivered intact, not that it is the only system-prompt text.
57
- - `settingSources` (filesystem settings load only in the ambient lane, so they cannot override your selected account), `strictMcpConfig`, `pinnedAccount`, `tokenInjection` (`oauth-slots` | `config-dir` | `ambient`), `resumeMode` (`auto` default | `off` restores per-turn sessions), `systemPromptFile`, `enabled` (default `false`; gates the ambient lane only).
57
+ - `settingSources` (filesystem settings load only in the ambient lane, so they cannot override your selected account), `strictMcpConfig`, `pinnedAccount`, `tokenInjection` (`oauth-slots` | `config-dir` | `ambient`), `resumeMode` (`auto` default | `off` restores per-turn sessions), `compactionOwner` (`senpi` default | `sdk` hands compaction to Claude Code's native auto-compact), `systemPromptFile`, `enabled` (default `false`; gates the ambient lane only).
58
58
  - **Environment overrides** (precedence: `env > project settings > global settings > default`; no new CLI flags):
59
59
 
60
60
  | Variable | Purpose |
@@ -66,9 +66,10 @@ The `anthropic-subscription` provider routes LLM calls through the official [Cla
66
66
  | `SENPI_CLAUDE_SDK_OAUTH_TOKEN_INJECTION` | `oauth-slots` \| `config-dir` \| `ambient` |
67
67
  | `SENPI_CLAUDE_SDK_OAUTH_SETTING_SOURCES` | Overrides `settingSources` |
68
68
  | `SENPI_CLAUDE_SDK_OAUTH_PINNED_ACCOUNT` | Overrides `pinnedAccount` |
69
+ | `SENPI_CLAUDE_SDK_OAUTH_COMPACTION_OWNER` | Overrides `compactionOwner` (`senpi` \| `sdk`) |
69
70
 
70
71
  Every `SENPI_*` variable is stripped from the Claude Code subprocess environment on all three lanes; other inherited variables are preserved.
71
- - **Native compaction.** This lane pins the SDK's session-scoped `autoCompactEnabled: true`, overriding the user's global Claude Code preference because native compaction is part of the lane contract. It does not set `autoCompactWindow`. With `resumeMode: "auto"`, the resident Claude Code session owns compaction; `resumeMode: "off"` restores per-turn sessions and returns compaction ownership to senpi.
72
+ - **Compaction.** senpi owns compaction on this lane by default: speculative and idle compaction, restoration, degradation recovery and per-model thresholds apply, the summary is written by the session's own model unless `compaction.model` is set, and an accepted compaction cold-seeds a fresh resident session from the compacted branch. So that only one side compacts, the lane pins the SDK's session-scoped `autoCompactEnabled: false` (overriding the user's global Claude Code preference) and sets `CLAUDE_CODE_TOTAL_TOKENS_REMINDER=off` for the Claude Code subprocess, whose per-turn token reminder otherwise defeats prompt-cache reuse; at the hard limit Claude Code rejects the turn and senpi's overflow recovery compacts and retries it. If that compaction fails, the turn ends with the error instead of Claude Code compacting natively. Set `compactionOwner: "sdk"` (or `SENPI_CLAUDE_SDK_OAUTH_COMPACTION_OWNER=sdk`) to let the resident Claude Code session compact instead: the lane pins `autoCompactEnabled: true`, does not set `autoCompactWindow`, and senpi stands down its automatic compaction (manual `/compact` stays senpi's). The owner is fixed into the resident Claude Code process when it starts, so changing it mid-session restarts that process on the next turn. Under either owner senpi never rewrites a message the resident session already received, so its per-turn context reduction stays off on this lane. `resumeMode: "off"` restores per-turn sessions, where senpi always owns compaction.
72
73
  - **Session reuse.** By default one long-lived SDK query spans the entire senpi session instead of a fresh one per turn, so conversations continue with only the new delta sent. An accepted senpi compaction rewrites the transcript and forks to a fresh resident session; a rejected compaction leaves continuity unchanged. Reuse also fails closed on branch/fork navigation, account failover, an aborted turn, or any configuration change. Idle sessions retire after 30 minutes (max 32 resident); a session with an in-flight turn is never evicted. After a senpi process restart the lane always starts fresh. Set `resumeMode: "off"` (or `SENPI_CLAUDE_SDK_OAUTH_RESUME=off`) to restore the old per-turn behaviour. Accepted values are `"auto"` (default) and `"off"`; any other value is silently ignored.
73
74
  - Account state is exposed to desktop/automation clients: RPC `get_provider_accounts`, `account_pin`, `account_remove` and the `auth_accounts_changed` / `account_failover` events, mirrored through the app-server protocol. Token material is never included.
74
75
 
package/docs/rpc.md CHANGED
@@ -870,7 +870,8 @@ session's prompt for the named surface, so one host serves a terminal client and
870
870
  uses the host process's `SENPI_PROMPT_SURFACE` (`app` or `chat` selects that surface; unset or any other value is `terminal`),
871
871
  which remains the only switch for a single-process `--mode rpc` client. A later `open_session` that attaches to a live
872
872
  session with another `promptSurface` rebuilds that session's prompt; an attach without the field keeps the current
873
- surface, and `new_session` / `switch_session` / `fork` inside the session keep it too. Probe `prompt_surface` in
873
+ surface, and `new_session` / `switch_session` / `fork` inside the session keep it too, also when the attach lands while
874
+ one of them is still building the replacement. Probe `prompt_surface` in
874
875
  `get_protocol_info` before sending the field; an older host ignores it. Probe `prompt_surface_chat` before sending
875
876
  `chat`: a host without it refuses `chat` with `invalid_launch_profile`, so a gateway falls back to `app`. Any value other
876
877
  than `terminal`, `app` or `chat` is refused with `invalid_launch_profile`.
@@ -900,9 +901,10 @@ subprocesses (the bash tool and everything it spawns) and its eval kernels, incl
900
901
  variable, even when the host process itself exports one (a daemon never inherits it from the process that ensured it).
901
902
  `BSK_HOME` and `BSK_BIN` are per install, not per session: they pass from the host environment to every session
902
903
  unchanged. A later `open_session` that attaches to a live session with another `browserEngine` moves that session to
903
- it, and an attach without the field keeps the current engine; `new_session` / `switch_session` / `fork` keep it too.
904
- Probe `browser_engine` in `get_protocol_info` before sending the field; the capability is advertised only because the
905
- value reaches every consumer above. Any other value is refused with `invalid_launch_profile`.
904
+ it, and an attach without the field keeps the current engine; `new_session` / `switch_session` / `fork` keep it too,
905
+ also when the attach lands while one of them is still building the replacement. Probe `browser_engine` in
906
+ `get_protocol_info` before sending the field; the capability is advertised only because the value reaches every
907
+ consumer above. Any other value is refused with `invalid_launch_profile`.
906
908
 
907
909
  A skill that must tell the client what the browser is doing publishes it as that session's own event: an extension
908
910
  registers a tool that calls `pi.rpc.emit(name, data)`, an eval cell reaches it as `tool.<name>(...)`, and the host adds
@@ -1306,6 +1308,12 @@ REPORT: nothing here aborts a turn, kills a session, or refuses an `open_session
1306
1308
  - **Event-loop stalls**: a 200 ms timer measures how late it is actually invoked; that lateness is the time the loop
1307
1309
  could serve nobody. Drift above `SENPI_RPC_LOOP_LAG_WARN_MS` (default 500) writes one stderr line per 10 seconds,
1308
1310
  `senpi rpc host stall: event loop blocked <drift>ms (sessionId=<handle> tool=<tool>)`. Drift above
1311
+ `SENPI_RPC_LOOP_LAG_ERROR_MS` (default 5000) additionally broadcasts a `host_stalled` record
1312
+ (`{ type, driftMs, sessionId?, tool?, processCpuMs?, heapDeltaMb? }`) to every connection, like the other content-free lifecycle records.
1313
+ `processCpuMs` is the process CPU time spent during the stalled window and `heapDeltaMb` the JS heap change across it, so a
1314
+ stall explains itself: CPU close to `driftMs` means the host was busy (a large heap drop in the same window points at a
1315
+ collection), and CPU close to zero means the process did not run at all (the machine starved it, or it sat in a blocking
1316
+ wait). The stderr line carries the same two numbers as `cpu=<ms> heap=<+/-MB>`.
1309
1317
  - On accepting an `open_session`, the host sends that connection a `queued` record
1310
1318
  (`{ type, for_request, position, in_flight }`) before the open enters the session loop.
1311
1319
  `position` is 1-based and `in_flight` counts opens already accepted across the whole host,
@@ -1315,12 +1323,12 @@ REPORT: nothing here aborts a turn, kills a session, or refuses an `open_session
1315
1323
  acknowledged open waits up to 10 minutes for its response instead of the 30 s request
1316
1324
  deadline (a busy host was measured answering after 57 s), and a timeout after it names the
1317
1325
  queue position instead of a bare deadline. A lost transport still rejects at once.
1318
- `SENPI_RPC_LOOP_LAG_ERROR_MS` (default 5000) additionally broadcasts a `host_stalled` record
1319
- (`{ type, driftMs, sessionId?, tool?, processCpuMs?, heapDeltaMb? }`) to every connection, like the other content-free lifecycle records.
1320
- `processCpuMs` is the process CPU time spent during the stalled window and `heapDeltaMb` the JS heap change across it, so a
1321
- stall explains itself: CPU close to `driftMs` means the host was busy (a large heap drop in the same window points at a
1322
- collection), and CPU close to zero means the process did not run at all (the machine starved it, or it sat in a blocking
1323
- wait). The stderr line carries the same two numbers as `cpu=<ms> heap=<+/-MB>`.
1326
+ - A multi-session host sends the same `queued` record for a session-routed `prompt` the moment it receives it,
1327
+ before the prompt's preflight runs on the session's loop (`position` counts that session's requests in flight).
1328
+ The bundled `RpcClient` then waits up to 5 minutes for the prompt to be accepted instead of the 30 s request
1329
+ deadline (a compaction's longer wait is never shortened by it), and a timeout after it says the host received the prompt. A prompt the host never acknowledged still
1330
+ fails at the request deadline. A reply written for a session that already closed is dropped, with one stderr
1331
+ line naming the session.
1324
1332
  - **Stall attribution**: each routed command is dispatched inside an `AsyncLocalStorage` scope carrying its routing
1325
1333
  `sessionId`, and an in-process session's tool executions open a span carrying `{ sessionId, tool }` for as long as
1326
1334
  the tool runs. A stall is blamed on the synchronous work that finished inside the measured window, or on the tool
@@ -1486,6 +1494,14 @@ capability must use `ask`, never `workspace`, for an edit-only approval promise.
1486
1494
  Hosts that support `permissionPreset: "auto"` advertise `permission_preset_auto`;
1487
1495
  clients without it must not send `auto`.
1488
1496
  Explicit permission rules and remembered approvals retain their existing precedence, except under `auto`: there settings and CLI rules can only narrow the preset (the more restrictive decision wins), and an "Always" answer (saved in `.senpi/permissions-approved.jsonl`, from this or an earlier session) still allows its pattern.
1497
+ An `open_session` that attaches to a live session with another `permissionPreset` moves that session to it: the next
1498
+ tool call is decided under the new preset, in either direction, and `new_session` / `switch_session` / `fork` keep it,
1499
+ also when the attach lands while one of them or a `reload` (or two overlapping reloads) is still rebuilding the
1500
+ session. An attach that names a preset is applied even when the session was opened or last attached with the same one.
1501
+ An attach without the field keeps the current preset. An attach accepts exactly the values `open_session` accepts and
1502
+ treats them the same way: an unknown preset name is applied like one given to `open_session`, so the session's next
1503
+ tool call is refused with `Permission setup failed: Invalid --permission-preset "<name>". ...` until a later attach
1504
+ names a valid preset.
1489
1505
 
1490
1506
  | Command | Params | Success data | Notes |
1491
1507
  | --- | --- | --- | --- |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi",
3
- "version": "2026.10.10-4",
3
+ "version": "2026.10.10-6",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -57,9 +57,9 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "@earendil-works/chord": "0.99.1",
60
- "@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.10-4",
61
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.10-4",
62
- "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.10-4",
60
+ "@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.10-6",
61
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.10-6",
62
+ "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.10-6",
63
63
  "@silvia-odwyer/photon-node": "0.3.4",
64
64
  "chalk": "6.0.0",
65
65
  "cross-spawn": "7.0.6",
@@ -78,8 +78,8 @@
78
78
  "yaml": "2.9.1",
79
79
  "@anthropic-ai/claude-agent-sdk": "0.3.289",
80
80
  "@anthropic-ai/sdk": "0.127.0",
81
- "@code-yeongyu/senpi-codemode": "2026.10.10-4",
82
- "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.10-4",
81
+ "@code-yeongyu/senpi-codemode": "2026.10.10-6",
82
+ "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.10-6",
83
83
  "@modelcontextprotocol/sdk": "1.30.0",
84
84
  "@mozilla/readability": "0.6.0",
85
85
  "linkedom": "0.18.13",
@@ -1,7 +0,0 @@
1
- import { createRequire as __piCreateRequire } from "node:module"; const require = __piCreateRequire(import.meta.url); { const __piWorkerThreads = require("node:worker_threads"); if (typeof __piWorkerThreads.markAsUncloneable !== "function") { __piWorkerThreads.markAsUncloneable = () => {}; } }
2
- import{getAgentDir}from"./chunk-463OK2Y4.js";import{existsSync,realpathSync}from"node:fs";import{basename,dirname,join,resolve}from"node:path";function canonicalSessionPath(path){let absolutePath=resolve(path),missing=[],existing=absolutePath;for(;!existsSync(existing);){let parent=dirname(existing);if(parent===existing)return absolutePath;missing.unshift(basename(existing)),existing=parent}try{return join(realpathSync(existing),...missing)}catch{return absolutePath}}function sessionDirectoryRemoved(sessionFile){return!existsSync(dirname(sessionFile))}import{createHash,randomUUID}from"node:crypto";import{chmod,link,mkdir,readFile,rename,rm,writeFile}from"node:fs/promises";import{basename as basename2,dirname as dirname2,join as join2,win32}from"node:path";var HOST_DAEMON_LAYOUT=2,HOST_DAEMON_DIR_ENV="SENPI_RPC_HOST_DAEMON_DIR",ENDPOINT_REGISTRY_VERSION=1,DIRECTORY_MODE=448,HOST_STATE_FILE_MODE=384;function canonicalEndpointPath(socket,platform=process.platform){return platform==="win32"?win32.normalize(socket).toLowerCase():socket.startsWith("\0")?socket:join2(canonicalSessionPath(dirname2(socket)),basename2(socket))}function daemonDirectoryName(socket,platform=process.platform){return directoryNameOf(canonicalEndpointPath(socket,platform))}function sameEndpoint(socket,other){return socket===other||canonicalEndpointPath(socket)===canonicalEndpointPath(other)}function socketNamesDirectory(socket,name){return daemonDirectoryName(socket)===name||process.platform!=="win32"&&directoryNameOf(socket)===name}function directoryNameOf(endpoint){return createHash("sha256").update(endpoint,"utf8").digest("hex").slice(0,16)}function shardKey(kind,ownerId){return createHash("sha256").update(`${kind}:${ownerId}`,"utf8").digest("hex").slice(0,16)}function shardSocketPathForKey(root,kind,key){return join2(root,`${kind}-${key}.sock`)}function shardSocketPath(root,kind,ownerId){return shardSocketPathForKey(root,kind,shardKey(kind,ownerId))}var SHARD_SOCKET_NAME=/^(p|i)-([0-9a-f]{16})\.sock$/;function parseShardSocket(socket){let match=SHARD_SOCKET_NAME.exec(basename2(socket)),key=match?.[2];return key===void 0?null:{kind:match?.[1]==="i"?"i":"p",key}}function createHostDaemonPaths(target){let flatDir=join2(target.agentDir??getAgentDir(),"rpc-host-daemon");return{socket:target.socket,flatDir,layoutMarker:join2(flatDir,"layout.json"),legacyPidFile:join2(flatDir,"host.pid"),...hostDaemonDirectoryPaths(join2(flatDir,daemonDirectoryName(target.socket)))}}function hostDaemonDirectoryPaths(dir){return{dir,endpointFile:join2(dir,"endpoint.json"),pointerFile:join2(dir,"host.pid"),lockFile:join2(dir,"daemon.lock"),settingsFile:join2(dir,"settings.json"),stderrLog:join2(dir,"stderr.log"),generationsDir:join2(dir,"generations"),reservationsDir:join2(dir,"reservations")}}function generationPaths(paths,instanceId){let dir=join2(paths.generationsDir,instanceId);return{dir,relativeDir:`generations/${instanceId}`,pidFile:join2(dir,"host.pid"),settingsFile:join2(dir,"settings.json"),scratchDir:join2(dir,"scratch")}}var HostDaemonStateError=class extends Error{constructor(path,cause){super(`RPC daemon state directory ${path} is not usable: ${cause instanceof Error?cause.message:cause}`,{cause}),this.name="HostDaemonStateError",this.path=path}};async function createDaemonDirectories(paths,identity={}){try{await mkdir(paths.flatDir,{recursive:!0,mode:DIRECTORY_MODE}),await makePrivateDirectory(paths.dir),await Promise.all([makePrivateDirectory(paths.generationsDir),makePrivateDirectory(paths.reservationsDir),writeFile(paths.layoutMarker,`${JSON.stringify({layout:HOST_DAEMON_LAYOUT,dir:basename2(paths.dir)})}
3
- `,{mode:HOST_STATE_FILE_MODE})])}catch(cause){throw new HostDaemonStateError(paths.dir,cause)}await ensureEndpointIdentity(paths,paths.socket,identity)}async function makePrivateDirectory(directory){await mkdir(directory,{recursive:!0,mode:DIRECTORY_MODE})===void 0&&await chmod(directory,DIRECTORY_MODE)}async function ensureEndpointIdentity(paths,socket,options={}){let temporary=`${paths.endpointFile}.${process.pid}-${randomUUID()}.tmp`;try{await mkdir(paths.dir,{recursive:!0,mode:DIRECTORY_MODE});let record=`${JSON.stringify({layout:HOST_DAEMON_LAYOUT,registry_version:ENDPOINT_REGISTRY_VERSION,endpoint_kind:options.kind??"rpc_host",socket,created_at:new Date().toISOString()})}
4
- `;await writeFile(temporary,record,{mode:HOST_STATE_FILE_MODE,flag:"wx"}),!await link(temporary,paths.endpointFile).then(()=>!0,cause=>isErrorCode(cause,"EEXIST")?!1:createExclusively(paths.endpointFile,record))&&options.repair===!0&&!await namesThisDirectory(paths)&&await rename(temporary,paths.endpointFile)}catch(cause){throw new HostDaemonStateError(paths.endpointFile,cause)}finally{await rm(temporary,{force:!0})}}async function createExclusively(path,content){try{return await writeFile(path,content,{mode:HOST_STATE_FILE_MODE,flag:"wx"}),!0}catch(cause){if(isErrorCode(cause,"EEXIST"))return!1;throw cause}}function isErrorCode(cause,code){return cause instanceof Error&&"code"in cause&&cause.code===code}async function namesThisDirectory(paths){let record;try{record=JSON.parse(await readFile(paths.endpointFile,"utf8"))}catch{return!1}return typeof record!="object"||record===null||!("socket"in record)?!1:typeof record.socket=="string"&&record.socket!==""&&socketNamesDirectory(record.socket,basename2(paths.dir))}async function createGenerationDirectory(generation){try{await mkdir(generation.scratchDir,{recursive:!0,mode:DIRECTORY_MODE}),await chmod(generation.dir,DIRECTORY_MODE)}catch(cause){throw new HostDaemonStateError(generation.dir,cause)}}import{execFile}from"node:child_process";function parseDaemonPidFile(text){let parsed;try{parsed=JSON.parse(text)}catch(error){if(error instanceof SyntaxError)return;throw error}let unguarded=parsed!==null&&isRecord(parsed)&&parsed.processStartTime===null;if(!(!isRecord(parsed)||typeof parsed.pid!="number"||!unguarded&&typeof parsed.processStartTime!="string")&&!(!Number.isInteger(parsed.pid)||parsed.pid<=0)){if(unguarded)return{pid:parsed.pid,processStartTime:null};if(!(typeof parsed.processStartTime!="string"||parsed.processStartTime.trim()===""))return{pid:parsed.pid,processStartTime:parsed.processStartTime}}}var ProcessIdentityUnreadableError=class extends Error{constructor(pid,attempts,cause){super(`process identity for live pid ${pid} stayed unreadable after ${attempts} probe attempt(s): ${cause instanceof Error?cause.message:String(cause)}`),this.name="ProcessIdentityUnreadableError",this.pid=pid,this.attempts=attempts,this.cause=cause}};async function processMatchesPidFile(pidFile,readStartTime=readProcessStartTime,isLive=processIsLive,retry={}){if(pidFile.processStartTime===null){if(!isLive(pidFile.pid))return!1;throw new ProcessIdentityUnreadableError(pidFile.pid,0,new Error("pidfile carries no process identity guard"))}let attempts=Math.max(1,retry.attempts??5),delayMs=retry.delayMs??200,lastError;for(let attempt=1;attempt<=attempts;attempt++)try{let current=await readStartTime(pidFile.pid);if(current!==void 0)return current===pidFile.processStartTime;if(!isLive(pidFile.pid))return!1;lastError=new Error("process identity probe returned no identity for a live process"),attempt<attempts&&await new Promise(resolve2=>setTimeout(resolve2,delayMs))}catch(error){if(!isLive(pidFile.pid))return!1;lastError=error,attempt<attempts&&await new Promise(resolve2=>setTimeout(resolve2,delayMs))}throw new ProcessIdentityUnreadableError(pidFile.pid,attempts,lastError)}async function readProcessIdentity(pid,platform=process.platform,timeoutMs,isLive=processIsLive){let command=platform==="win32"?{executable:"powershell.exe",args:["-NoProfile","-NonInteractive","-Command",`$process = Get-CimInstance Win32_Process -Filter "ProcessId=${String(pid)}" -ErrorAction Stop; if ($null -eq $process) { Write-Output '__SENPI_ABSENT__'; exit 0 }; $process.CreationDate.ToFileTimeUtc().ToString("D", [Globalization.CultureInfo]::InvariantCulture)`]}:{executable:"ps",args:["-o","lstart=","-p",String(pid)]};return new Promise(resolve2=>{let effectiveTimeoutMs=timeoutMs??(platform==="win32"?1e3:void 0);execFile(command.executable,command.args,{windowsHide:!0,...effectiveTimeoutMs===void 0?{}:{timeout:effectiveTimeoutMs}},(error,stdout)=>{if(error){let code="code"in error?error.code:void 0;if(platform!=="win32"&&code===1||!isLive(pid)){resolve2({kind:"absent"});return}resolve2({kind:"error",error});return}let output=stdout.trim();if(output==="__SENPI_ABSENT__")return resolve2({kind:"absent"});if(!output||platform==="win32"&&!/^\d+$/.test(output))return resolve2({kind:"error",error:new Error("invalid process identity output")});resolve2({kind:"present",identity:output})}).once("error",error=>resolve2({kind:"error",error}))})}async function stopValidatedPid(pidFile,signal){if(await processMatchesPidFile(pidFile)){try{process.kill(pidFile.pid,signal)}catch(error){if(!isNodeErrorCode(error,"ESRCH"))throw error}signal==="SIGTERM"&&await waitForGone(pidFile,1e4),signal==="SIGKILL"&&await waitForGone(pidFile,2e3)}}async function waitForGone(pidFile,timeoutMs){let deadline=Date.now()+timeoutMs;for(;Date.now()<=deadline;){if(!await processMatchesPidFile(pidFile))return!0;await delay(100)}return!await processMatchesPidFile(pidFile)}async function readProcessStartTime(pid,platform=process.platform,timeoutMs){return readProcessIdentity(pid,platform,timeoutMs).then(result=>{if(result.kind==="error")throw result.error;return result.kind==="present"?result.identity:void 0})}function processIsLive(pid){try{return process.kill(pid,0),!0}catch(error){if(isNodeErrorCode(error,"ESRCH"))return!1;if(isNodeErrorCode(error,"EPERM"))return!0;throw error}}async function waitForStartTime(pid,timeoutMs,readStartTime=readProcessStartTime,isLive=processIsLive){let deadline=Date.now()+timeoutMs;for(;Date.now()<=deadline;){let startTime;try{startTime=await readStartTime(pid)}catch{}if(startTime)return startTime;await delay(20)}if(!isLive(pid))throw new Error(`spawned daemon pid ${pid} had no process start time`)}function delay(ms){return new Promise(resolveDelay=>setTimeout(resolveDelay,ms))}function isRecord(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function isNodeErrorCode(error,code){return error instanceof Error&&"code"in error&&error.code===code}import{createHash as createHash2}from"node:crypto";import{mkdir as mkdir2,readdir,readFile as readFile3,rename as rename2,rm as rm2,writeFile as writeFile3}from"node:fs/promises";import{join as join3}from"node:path";import{readFile as readFile2,writeFile as writeFile2}from"node:fs/promises";async function writeHostSettings(paths,settings){let generation=generationPaths(paths,settings.instanceId);await createGenerationDirectory(generation),await writeStateFile(paths.settingsFile,settings),await writeStateFile(generation.settingsFile,settings)}async function readHostSettings(paths){let parsed=parseJson(await readFileOrUndefined(paths.settingsFile));if(parsed)return{...typeof parsed.coldStart=="string"&&{coldStart:parsed.coldStart},...typeof parsed.idleExitMs=="number"&&{idleExitMs:parsed.idleExitMs}}}async function writeStateFile(path,content){try{await writeFile2(path,`${JSON.stringify(content)}
5
- `,{mode:HOST_STATE_FILE_MODE})}catch(cause){throw new HostDaemonStateError(path,cause)}}async function readFileOrUndefined(path){try{return await readFile2(path,"utf8")}catch(error){if(isNodeErrorCode2(error,"ENOENT")||isNodeErrorCode2(error,"ENOTDIR"))return;throw error}}function parseJson(text){if(text!==void 0)try{let parsed=JSON.parse(text);return isRecord2(parsed)?parsed:void 0}catch{return}}function isRecord2(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function isNodeErrorCode2(error,code){return error instanceof Error&&"code"in error&&error.code===code}var SESSION_PATH_RETRY_AFTER_MS=2e3;function reservationFile(dir,sessionPath){return join3(dir,`${createHash2("sha256").update(sessionPath,"utf8").digest("hex").slice(0,16)}.json`)}function createEndpointReservations(host){let told=process.env[HOST_DAEMON_DIR_ENV],dir=told!==void 0&&told.trim()!==""?told:host.socket===void 0?void 0:createHostDaemonPaths({socket:host.socket,agentDir:host.agentDir}).dir;if(dir!==void 0)return createSessionPathReservations({daemonDir:dir,instanceId:host.instanceId,...host.onFailure?{onFailure:host.onFailure}:{}})}function createSessionPathReservations(options){let paths=hostDaemonDirectoryPaths(options.daemonDir),dir=paths.reservationsDir,pid=options.pid??process.pid,startTime=readProcessStartTime(pid).then(value=>value??null,()=>null),held=new Set,report=message=>options.onFailure?.(message),publish=async(sessionPath,attached,stillHeld)=>{let owner={instanceId:options.instanceId,pid,processStartTime:await startTime,sessionPath,attached},file=reservationFile(dir,sessionPath);await mkdir2(dir,{recursive:!0,mode:448});let staging=`${file}.${pid}.tmp`;if(await writeFile3(staging,`${JSON.stringify(owner)}
6
- `,{mode:384}),stillHeld?.()===!1){await rm2(staging,{force:!0});return}await rename2(staging,file)};return{async claim(sessionPath,attached=!0){let existing=await readOwner(reservationFile(dir,sessionPath));if(existing&&existing.pid!==pid){let standing=await standingOwner(existing,paths.pointerFile);if(standing)return standing}try{await publish(sessionPath,attached),held.add(sessionPath)}catch(cause){report(`session path reservation for ${sessionPath} could not be written (${errorMessage(cause)})`)}},release(sessionPath){held.delete(sessionPath)&&rm2(reservationFile(dir,sessionPath),{force:!0}).catch(cause=>{report(`session path reservation for ${sessionPath} could not be removed (${errorMessage(cause)})`)})},setAttached(sessionPath,attached){held.has(sessionPath)&&publish(sessionPath,attached,()=>held.has(sessionPath)).catch(cause=>{report(`session path reservation for ${sessionPath} could not be updated (${errorMessage(cause)})`)})}}}async function readSessionPathClaims(dir){let entries=await readdir(dir).catch(()=>[]),claims=[];for(let entry of entries){if(!entry.endsWith(".json"))continue;let file=join3(dir,entry),owner=await readOwner(file);owner&&claims.push({file,owner})}return claims}async function standingOwner(owner,pointerFile){if(!await claimOwnerIsLive(owner))return;let current=await currentInstanceId(pointerFile)===owner.instanceId;return current||owner.attached!==!1?{...owner,current}:void 0}async function currentInstanceId(pointerFile){let pointer=parseJson(await readFileOrUndefined(pointerFile).catch(()=>{}));return typeof pointer?.instance_id=="string"?pointer.instance_id:void 0}async function readOwner(file){let parsed;try{parsed=JSON.parse(await readFile3(file,"utf8"))}catch{return}if(typeof parsed!="object"||parsed===null)return;let{instanceId,pid,processStartTime,sessionPath,attached}=parsed;if(!(typeof instanceId!="string"||typeof pid!="number"||typeof sessionPath!="string"))return{instanceId,pid,processStartTime:typeof processStartTime=="string"?processStartTime:null,sessionPath,...typeof attached=="boolean"?{attached}:{}}}async function claimOwnerIsLive(owner){if(!processIsLive(owner.pid))return!1;if(owner.processStartTime===null)return!0;let current=await readProcessStartTime(owner.pid).catch(()=>{});return current===void 0||current===owner.processStartTime}function errorMessage(cause){return cause instanceof Error?cause.message:String(cause)}import{rename as rename3,rm as rm4}from"node:fs/promises";import{readdir as readdir3,rm as rm3}from"node:fs/promises";import{readdir as readdir2}from"node:fs/promises";import{readdirSync,readFileSync}from"node:fs";function supportedPlatform(platform){return platform==="darwin"||platform==="linux"?platform:void 0}async function loadProcessTableReader(platform=process.platform,options={}){let supported=supportedPlatform(platform);return supported===void 0||typeof globalThis.Bun>"u"?void 0:supported==="darwin"?await darwinReader(options):linuxReader}var KINFO_PROC_STRIDE=648,KINFO_PID_OFFSET=40,KINFO_PPID_OFFSET=560,KINFO_STAT_OFFSET=36,SZOMB=5,PROC_PIDTASKINFO=4,TASKINFO_RESIDENT_OFFSET=8,TABLE_START_BYTES=1<<20,TABLE_MAX_BYTES=32<<20;function parseKernelProcessTable(table,rowCount,residentKb){if(rowCount<=0||table.byteLength<rowCount*KINFO_PROC_STRIDE)return;let view=new DataView(table.buffer,table.byteOffset,table.byteLength),rows=[],selfSeen=!1;for(let index=0;index<rowCount;index++){let base=index*KINFO_PROC_STRIDE,pid=view.getUint32(base+KINFO_PID_OFFSET,!0);if(!Number.isInteger(pid)||pid<=0)continue;let ppid=view.getUint32(base+KINFO_PPID_OFFSET,!0),zombie=table[base+KINFO_STAT_OFFSET]===SZOMB;if(pid===process.pid){if(ppid!==process.ppid||zombie)return;selfSeen=!0}rows.push({pid,ppid,state:zombie?"Z":"U",rssKb:residentKb(pid)})}return selfSeen?rows:void 0}async function darwinReader(options){let{dlopen,FFIType,ptr}=await import("bun:ffi"),library=dlopen("libSystem.B.dylib",{sysctl:{args:[FFIType.ptr,FFIType.u32,FFIType.ptr,FFIType.ptr,FFIType.ptr,FFIType.i32],returns:FFIType.i32},proc_pidinfo:{args:[FFIType.i32,FFIType.i32,FFIType.u32,FFIType.ptr,FFIType.i32],returns:FFIType.i32}}),mib=new Int32Array([1,14,0]),none=new Uint8Array(1),requested=options.initialTableBytes,table=new Uint8Array(Number.isFinite(requested)&&requested!==void 0&&requested>0?Math.min(Math.floor(requested),TABLE_MAX_BYTES):TABLE_START_BYTES),length=new Uint32Array(1),taskInfo=new Uint8Array(256),taskView=new DataView(taskInfo.buffer),residentKb=pid=>(taskInfo.fill(0),library.symbols.proc_pidinfo(pid,PROC_PIDTASKINFO,0,ptr(taskInfo),taskInfo.byteLength)>0?Math.round(Number(taskView.getBigUint64(TASKINFO_RESIDENT_OFFSET,!0))/1024):0);return()=>{for(;;){if(length[0]=table.byteLength,library.symbols.sysctl(ptr(mib),mib.length,ptr(table),ptr(length),ptr(none),0)!==0){if(table.byteLength>=TABLE_MAX_BYTES)return;let reported=length[0]>table.byteLength?length[0]:0;table=new Uint8Array(Math.min(TABLE_MAX_BYTES,Math.max(table.byteLength*2,reported)));continue}return parseKernelProcessTable(table,Math.floor(length[0]/KINFO_PROC_STRIDE),residentKb)}}}function linuxReader(){let rows=[];for(let entry of readdirSync("/proc")){let pid=Number(entry);if(!Number.isInteger(pid)||pid<=0)continue;let stat2;try{stat2=readFileSync(`/proc/${pid}/stat`,"utf8")}catch{continue}let tail=stat2.slice(stat2.lastIndexOf(")")+2).split(" "),state=tail[0],ppid=Number(tail[1]);if(state===void 0||!Number.isInteger(ppid))continue;let rssPages=Number(tail[21]);rows.push({pid,ppid,state,rssKb:Number.isFinite(rssPages)?Math.round(rssPages*4):0})}return rows}var UNOBSERVED={rss_mb:null,host_rss_mb:null,open_fds:null,zombies:null},readers=new Map;function tableReader(platform){let cached=readers.get(platform);if(cached!==void 0)return cached;let loading=loadProcessTableReader(platform);return readers.set(platform,loading),loading}async function readHostProcessMetrics(pid,platform=process.platform){if(platform==="win32")return UNOBSERVED;let read=await tableReader(platform);if(read===void 0)return UNOBSERVED;let rows=read();if(rows===void 0)return UNOBSERVED;let tree=descendants(rows,pid);return tree.length===0?UNOBSERVED:{rss_mb:residentMegabytes(tree),host_rss_mb:residentMegabytes(tree.filter(row=>row.pid===pid||row.ppid===pid)),open_fds:platform==="linux"?await openDescriptors(tree):null,zombies:tree.filter(row=>row.state.startsWith("Z")).length}}function residentMegabytes(rows){return Math.round(rows.reduce((total,row)=>total+row.rssKb,0)/KILOBYTES_PER_MEGABYTE)}function descendants(rows,root){let tree=rows.filter(row=>row.pid===root);if(tree.length===0)return tree;for(let index=0;index<tree.length;index++){let parent=tree[index].pid;for(let row of rows)row.ppid===parent&&!tree.includes(row)&&tree.push(row)}return tree}var KILOBYTES_PER_MEGABYTE=1024;async function openDescriptors(tree){let total=0,counted=!1;for(let row of tree){let entries=await readdir2(`/proc/${row.pid}/fd`).catch(()=>{});entries!==void 0&&(counted=!0,total+=entries.length)}return counted?total:null}async function pruneDeadGenerations(paths){let pointer=parseJson(await readFileOrUndefined(paths.pointerFile).catch(()=>{})),pointedAt=typeof pointer?.instance_id=="string"?pointer.instance_id:void 0,generations=[];for(let instanceId of await readdir3(paths.generationsDir).catch(()=>[])){let record=parseJson(await readFileOrUndefined(generationPaths(paths,instanceId).pidFile));typeof record?.pid!="number"||processIsLive(record.pid)||(await rm3(generationPaths(paths,instanceId).dir,{recursive:!0,force:!0}).catch(()=>{}),generations.push(instanceId))}let pointerRemoved=pointedAt!==void 0&&generations.includes(pointedAt);pointerRemoved&&await rm3(paths.pointerFile,{force:!0}).catch(()=>{});let claims=[];for(let claim of await readSessionPathClaims(paths.reservationsDir))await claimOwnerIsLive(claim.owner)||(await rm3(claim.file,{force:!0}).catch(()=>{}),claims.push(claim.owner.sessionPath));return{generations,claims,pointer:pointerRemoved}}function memoryOf(metrics){return{rss_mb:metrics.rss_mb,host_rss_mb:metrics.host_rss_mb}}async function readGenerationRows(paths,options={}){let pointer=parseJson(await readFileOrUndefined(paths.pointerFile).catch(()=>{})),currentId=typeof pointer?.instance_id=="string"?pointer.instance_id:void 0,claims=await readSessionPathClaims(paths.reservationsDir),rows=[];for(let instanceId of await readdir3(paths.generationsDir).catch(()=>[])){let record=parseJson(await readFileOrUndefined(generationPaths(paths,instanceId).pidFile));if(typeof record?.pid!="number")continue;let alive=processIsLive(record.pid);!alive&&options.includeDead!==!0||rows.push({instanceId,generation:typeof record.generation=="number"?record.generation:0,pid:record.pid,engineVersion:typeof record.engineVersion=="string"?record.engineVersion:null,...alive?memoryOf(await readHostProcessMetrics(record.pid)):{rss_mb:null,host_rss_mb:null},sessions:claims.filter(claim=>claim.owner.instanceId===instanceId).length,current:instanceId===currentId,alive})}return rows.sort((left,right)=>left.generation-right.generation)}async function readHostRegistration(paths){let pointer=parseJson(await readFileOrUndefined(paths.pointerFile));if(!pointer||typeof pointer.instance_id!="string")return;let text=await readFileOrUndefined(generationPaths(paths,pointer.instance_id).pidFile),record=text===void 0?void 0:parseDaemonPidFile(text);if(record===void 0)return;let parsed=parseJson(text)??{},writer=parseWriter(parsed);return{record,...writer&&{writer},...typeof parsed.socket=="string"&&{socket:parsed.socket},instanceId:pointer.instance_id,generation:typeof parsed.generation=="number"?parsed.generation:0}}async function writeHostRegistration(paths,registration,options={}){options.fresh!==!0&&await pruneDeadGenerations(paths);let{generation,writer}=await writeGenerationRecord(paths,registration);await writeStateFile(`${paths.pointerFile}.${process.pid}.tmp`,{layout:HOST_DAEMON_LAYOUT,instance_id:registration.instanceId,generation_dir:generation.relativeDir,writer}),await rename3(`${paths.pointerFile}.${process.pid}.tmp`,paths.pointerFile)}async function writeGenerationRecord(paths,registration){let generation=generationPaths(paths,registration.instanceId),writer={pid:process.pid,startTime:await thisProcessStartTime()},{build}=registration;return await createGenerationDirectory(generation),await writeStateFile(generation.pidFile,{...registration.record,instance_id:registration.instanceId,generation:registration.generation,...build&&{engineVersion:build.text,engineOrdinal:build.ordinal},launchProfileId:registration.launchProfileId,socket:registration.socket,writer}),{generation,writer}}async function clearHostRegistration(paths){let pointer=parseJson(await readFileOrUndefined(paths.pointerFile));typeof pointer?.instance_id=="string"&&await rm4(generationPaths(paths,pointer.instance_id).dir,{recursive:!0,force:!0}),await rm4(paths.settingsFile,{force:!0}),await rm4(paths.pointerFile,{force:!0})}async function releaseGeneration(paths,owner){let generation=generationPaths(paths,owner.instanceId),record=parseDaemonPidFile(await readFileOrUndefined(generation.pidFile)??"");if(record!==void 0&&record.pid!==owner.pid)return;if(owner.superseded===!0){await rm4(generation.dir,{recursive:!0,force:!0});return}let ownsPointer=parseJson(await readFileOrUndefined(paths.pointerFile))?.instance_id===owner.instanceId;ownsPointer&&await rm4(paths.settingsFile,{force:!0}),await rm4(generation.dir,{recursive:!0,force:!0}),ownsPointer&&await rm4(paths.pointerFile,{force:!0})}async function provenOwner(registered,socket){let record=registered?.record;if(!record||record.processStartTime===null||registered?.socket!==void 0&&!sameEndpoint(registered.socket,socket))return;let identity={pid:record.pid,processStartTime:record.processStartTime};return await processMatchesPidFile(identity,readProcessStartTime).catch(()=>!1)?{...identity,instanceId:registered.instanceId}:void 0}async function writtenByThisProcess(writer,readStartTime){if(writer===void 0||writer.pid!==process.pid||writer.startTime===null)return!1;let startTime=readStartTime?await readStartTime(process.pid).then(value=>value??null,()=>null):await thisProcessStartTime();return writer.startTime===startTime}var selfStartTime;function thisProcessStartTime(){return selfStartTime??=readProcessStartTime(process.pid).then(value=>value??null,()=>null),selfStartTime}function parseWriter(parsed){if(!isRecord2(parsed.writer)||typeof parsed.writer.pid!="number")return;let{pid,startTime}=parsed.writer;return{pid,startTime:typeof startTime=="string"?startTime:null}}import{createHash as createHash3,randomBytes,timingSafeEqual}from"node:crypto";import{chmod as chmod2,mkdir as mkdir3,readFile as readFile4,writeFile as writeFile4}from"node:fs/promises";import{dirname as dirname3,win32 as win322}from"node:path";var WINDOWS_PIPE_PREFIX="\\\\.\\pipe\\senpi-rpc-",SOCKET_SECRET_BYTES=32,SOCKET_SECRET_SUFFIX=".secret",SOCKET_SECRET_FILE_ENV="SENPI_RPC_SOCKET_SECRET_FILE",SOCKET_HANDSHAKE_TIMEOUT_MS=2e3;function socketSecretPath(logicalPath){if(logicalPath.toLowerCase().startsWith("\\\\.\\pipe\\"))throw new Error(`Windows RPC named-pipe addresses need a logical filesystem path for their secret: ${logicalPath}`);return`${logicalPath}${SOCKET_SECRET_SUFFIX}`}async function ensureSocketSecret(path){try{let existing=await readFile4(path);if(existing.length===SOCKET_SECRET_BYTES)return existing}catch(error){if(!isNodeErrorCode3(error,"ENOENT"))throw error}return createSocketSecret(path)}async function createSocketSecret(path){await mkdir3(dirname3(path),{recursive:!0});let secret=randomBytes(SOCKET_SECRET_BYTES);return await writeFile4(path,secret,{mode:384}),await chmod2(path,384),secret}async function readSocketSecret(path){let secret=await readFile4(path);if(secret.length!==SOCKET_SECRET_BYTES)throw new Error(`invalid RPC socket secret: ${path}`);return secret}function resolveSocketTransportAddress(socketPath,platform,secret){if(platform!=="win32"||socketPath.toLowerCase().startsWith("\\\\.\\pipe\\"))return socketPath;if(!/^[a-zA-Z]:[\\/]/.test(socketPath)&&!/^\\\\[^/]/.test(socketPath))throw new Error(`Windows RPC socket path must be drive-qualified or UNC: ${socketPath}`);let canonical=win322.normalize(socketPath).toLowerCase(),identity=Buffer.concat([Buffer.from(canonical,"utf8"),Buffer.from(secret??[])]),name=createHash3("sha256").update(identity).digest("hex").slice(0,32);return`${WINDOWS_PIPE_PREFIX}${name}`}function authenticateSocket(socket,secret,onAuthenticated){let received=Buffer.alloc(0),finish=()=>{clearTimeout(timer),socket.off("data",onData),socket.off("error",onError)},onError=()=>{finish(),socket.destroy()},timer=setTimeout(()=>{finish(),socket.destroy()},SOCKET_HANDSHAKE_TIMEOUT_MS),onData=chunk=>{if(received=Buffer.concat([received,chunk]),received.length<secret.length)return;if(finish(),!timingSafeEqual(received.subarray(0,secret.length),Buffer.from(secret))){socket.destroy();return}let remainder=received.subarray(secret.length);remainder.length>0&&socket.unshift(remainder),onAuthenticated()};socket.on("data",onData),socket.once("error",onError),socket.once("close",finish)}function sendSocketHandshake(socket,secret){socket.write(Buffer.from(secret))}function isNodeErrorCode3(error,code){return error instanceof Error&&"code"in error&&error.code===code}import{mkdir as mkdir4,readFile as readFile5,rename as rename4,stat,unlink,writeFile as writeFile5}from"node:fs/promises";import{dirname as dirname4}from"node:path";var PUBLIC_SOCKET_IDENTITY_FILE="public-socket.owner",SOCKET_IDENTITY_WAIT_MS=3e4,MAX_SOCKET_PATH_BYTES=103;function generationBindPath(publicSocket,generation){return`${publicSocket}.next-${generation}`}function sameSocketIdentity(a,b){return a.dev===b.dev&&a.ino===b.ino}async function statSocketIdentity(socketPath){try{let{dev,ino}=await stat(socketPath);return{dev,ino}}catch(cause){if(isNodeErrorCode4(cause,"ENOENT"))return;throw cause}}async function classifyEndpointOwnership(socketPath,identity){if(identity===void 0||process.platform==="win32"||socketPath.startsWith("\0"))return"unknown";let current;try{current=await statSocketIdentity(socketPath)}catch{return"unknown"}return current===void 0?"absent":sameSocketIdentity(current,identity)?"held":"replaced"}async function socketEntryReplaced(socketPath,identity){return await classifyEndpointOwnership(socketPath,identity)==="replaced"}async function writeSocketIdentityFile(path,identity){await mkdir4(dirname4(path),{recursive:!0,mode:448});let tmpPath=`${path}.${process.pid}.tmp`;await writeFile5(tmpPath,`${JSON.stringify(identity)}
7
- `,{mode:384}),await rename4(tmpPath,path)}async function readSocketIdentityFile(path){let raw;try{raw=await readFile5(path,"utf8")}catch(cause){if(isNodeErrorCode4(cause,"ENOENT"))return;throw cause}let parsed;try{parsed=JSON.parse(raw)}catch{return}if(!(typeof parsed!="object"||parsed===null||typeof parsed.dev!="number"||typeof parsed.ino!="number"))return{dev:parsed.dev,ino:parsed.ino}}async function waitForSocketIdentityFile(path,timeoutMs=SOCKET_IDENTITY_WAIT_MS,intervalMs=25){let deadline=Date.now()+timeoutMs;for(;;){let identity=await readSocketIdentityFile(path);if(identity!==void 0)return identity;if(Date.now()>=deadline)return;await delayUnrefed(intervalMs)}}async function unlinkOwnedSocket(socketPath,identity,log){if(process.platform==="win32"||socketPath.startsWith("\0"))return;let current;try{current=await statSocketIdentity(socketPath)}catch(cause){log(`socket ${socketPath} ownership could not be verified (${errorMessage2(cause)}); leaving it`);return}if(current!==void 0){if(identity===void 0){log(`socket path ${socketPath} ownership unknown; leaving it`);return}if(!sameSocketIdentity(current,identity)){log(`socket path ${socketPath} now owned by another host; leaving it`);return}try{await unlink(socketPath)}catch(cause){isNodeErrorCode4(cause,"ENOENT")||log(`socket ${socketPath} removal failed (${errorMessage2(cause)})`)}}}async function shieldSocketDuringClose(socketPath,close){if(process.platform==="win32"||socketPath.startsWith("\0"))return close();let shieldPath=`${socketPath}.shield-${process.pid}`,shielded=!1;try{await statSocketIdentity(socketPath)!==void 0&&(await rename4(socketPath,shieldPath),shielded=!0)}catch{}try{await close()}finally{if(shielded)try{await rename4(shieldPath,socketPath)}catch{}}}function delayUnrefed(ms){return new Promise(resolve2=>{setTimeout(resolve2,ms).unref?.()})}function errorMessage2(cause){return cause instanceof Error?cause.message:String(cause)}function isNodeErrorCode4(cause,code){return cause instanceof Error&&"code"in cause&&cause.code===code}export{canonicalSessionPath,sessionDirectoryRemoved,HOST_DAEMON_LAYOUT,HOST_DAEMON_DIR_ENV,ENDPOINT_REGISTRY_VERSION,HOST_STATE_FILE_MODE,canonicalEndpointPath,daemonDirectoryName,sameEndpoint,socketNamesDirectory,shardKey,shardSocketPathForKey,shardSocketPath,parseShardSocket,createHostDaemonPaths,hostDaemonDirectoryPaths,generationPaths,createDaemonDirectories,ensureEndpointIdentity,writeHostSettings,readHostSettings,writeStateFile,readFileOrUndefined,parseJson,parseDaemonPidFile,ProcessIdentityUnreadableError,processMatchesPidFile,stopValidatedPid,waitForGone,readProcessStartTime,processIsLive,waitForStartTime,readHostProcessMetrics,SESSION_PATH_RETRY_AFTER_MS,createEndpointReservations,readSessionPathClaims,claimOwnerIsLive,pruneDeadGenerations,readGenerationRows,readHostRegistration,writeHostRegistration,writeGenerationRecord,clearHostRegistration,releaseGeneration,provenOwner,writtenByThisProcess,thisProcessStartTime,SOCKET_SECRET_FILE_ENV,socketSecretPath,ensureSocketSecret,createSocketSecret,readSocketSecret,resolveSocketTransportAddress,authenticateSocket,sendSocketHandshake,PUBLIC_SOCKET_IDENTITY_FILE,MAX_SOCKET_PATH_BYTES,generationBindPath,statSocketIdentity,classifyEndpointOwnership,socketEntryReplaced,writeSocketIdentityFile,readSocketIdentityFile,waitForSocketIdentityFile,unlinkOwnedSocket,shieldSocketDuringClose};