@code-yeongyu/senpi 2026.10.5 → 2026.10.7

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 (151) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/bundle/chunks/{anthropic-messages-G5236QNK.js → anthropic-messages-4MNFPQ66.js} +1 -1
  3. package/dist/bundle/chunks/{app-server-command-3Q5G4CAF.js → app-server-command-SO5RJ2N7.js} +1 -1
  4. package/dist/bundle/chunks/{azure-openai-responses-NTA2HGG4.js → azure-openai-responses-A5OEHSJM.js} +1 -1
  5. package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
  6. package/dist/bundle/chunks/{chunk-ECDLITEY.js → chunk-6EH7QXOG.js} +2 -2
  7. package/dist/bundle/chunks/{chunk-JVSOW4QU.js → chunk-7A3WRAKP.js} +1 -1
  8. package/dist/bundle/chunks/chunk-7SMFF2QJ.js +14 -0
  9. package/dist/bundle/chunks/{chunk-WTJU5B2C.js → chunk-BE66QJ3Z.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-6JXPZACL.js → chunk-CM5QTRC2.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-UHRXXTFZ.js → chunk-D2W3MXRF.js} +2 -2
  12. package/dist/bundle/chunks/{chunk-U7CI2JGX.js → chunk-DDEAZX22.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-UXIQK5NQ.js → chunk-ESSQUSRC.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-EEOKVSNF.js → chunk-EUU6W5B3.js} +180 -176
  15. package/dist/bundle/chunks/{chunk-KLBKUBDV.js → chunk-GQ3YLXVZ.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-HKG7XL4H.js → chunk-HY4AOLON.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-2WV426OJ.js → chunk-JJNIU2A2.js} +8 -4
  18. package/dist/bundle/chunks/{chunk-22DAHAHO.js → chunk-LUWWM7GA.js} +2 -2
  19. package/dist/bundle/chunks/{chunk-CTQU2XNX.js → chunk-MLNJNJFE.js} +1 -1
  20. package/dist/bundle/chunks/{chunk-BPX6QWPD.js → chunk-PELNNWGO.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-BBK6SSYT.js → chunk-TAV6QZFM.js} +1 -1
  22. package/dist/bundle/chunks/{chunk-QUROHJLP.js → chunk-TFO5JGLR.js} +1 -1
  23. package/dist/bundle/chunks/chunk-VJJ53ORU.js +2 -0
  24. package/dist/bundle/chunks/{chunk-XYDZ3LIE.js → chunk-YI7SPFDN.js} +2 -2
  25. package/dist/bundle/chunks/chunk-ZJHRDQCH.js +2 -0
  26. package/dist/bundle/chunks/cli-main-XBQN73O2.js +3 -0
  27. package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-5ZUHUKMU.js → cloudflare-workers-ai-system-one-TE3OEJEL.js} +1 -1
  28. package/dist/bundle/chunks/{google-generative-ai-OTMG6ZXI.js → google-generative-ai-JBC53AMK.js} +1 -1
  29. package/dist/bundle/chunks/{google-vertex-2LXS3ICC.js → google-vertex-46ODNPM2.js} +1 -1
  30. package/dist/bundle/chunks/{help-fast-path-SHZJ3QWQ.js → help-fast-path-FUMH5NSA.js} +1 -1
  31. package/dist/bundle/chunks/host-command-YXQAXNKK.js +2 -0
  32. package/dist/bundle/chunks/{host-lifecycle-7OVWGVIZ.js → host-lifecycle-L4XQIATP.js} +1 -1
  33. package/dist/bundle/chunks/host-lifecycle.js +1 -1
  34. package/dist/bundle/chunks/interactive-mode-ROO5W56P.js +2 -0
  35. package/dist/bundle/chunks/{llama-cpp-classify-U5XAMKZE.js → llama-cpp-classify-AD6TTKNL.js} +1 -1
  36. package/dist/bundle/chunks/{multi-session-host-34WOINP4.js → multi-session-host-5WTKAQJM.js} +5 -5
  37. package/dist/bundle/chunks/{openai-codex-responses-WNTYIPFZ.js → openai-codex-responses-Q4OWIK7Y.js} +1 -1
  38. package/dist/bundle/chunks/{openai-completions-VSH22FCN.js → openai-completions-6HU7GIGD.js} +1 -1
  39. package/dist/bundle/chunks/{openai-images-XBTUJXPR.js → openai-images-M73YANTR.js} +1 -1
  40. package/dist/bundle/chunks/{openai-responses-WLBHCFYL.js → openai-responses-S4QYZXF3.js} +1 -1
  41. package/dist/bundle/chunks/{openrouter-images-3LVJZY3G.js → openrouter-images-PCJFX2KO.js} +1 -1
  42. package/dist/bundle/chunks/{package-manager-cli-2QLVGQSC.js → package-manager-cli-NCD5FQGV.js} +1 -1
  43. package/dist/bundle/chunks/{rotation-stream-JUCEXHWU.js → rotation-stream-7E37KOCX.js} +1 -1
  44. package/dist/bundle/chunks/rpc-mode-MWURKCP2.js +2 -0
  45. package/dist/bundle/chunks/{schedule-command-QTYSUYO2.js → schedule-command-UTXB465F.js} +1 -1
  46. package/dist/bundle/chunks/session-control-endpoint-J6RHZXWB.js +3 -0
  47. package/dist/bundle/chunks/{session-picker-HX4LOJZ7.js → session-picker-VTTRFGE7.js} +1 -1
  48. package/dist/bundle/chunks/session-worker.js +376 -368
  49. package/dist/bundle/chunks/{typesafe-system-one-FNI3UASE.js → typesafe-system-one-TF23RUVG.js} +1 -1
  50. package/dist/bundle/cli.js +1 -1
  51. package/dist/bundle/index.js +1 -1
  52. package/dist/bundle/rpc-entry.js +1 -1
  53. package/dist/bundle/runtime-manifest.json +1 -1
  54. package/dist/core/agent-session-runtime.d.ts +12 -0
  55. package/dist/core/agent-session.d.ts +18 -0
  56. package/dist/core/agent-session.js +55 -19
  57. package/dist/core/dynamic-prompt/handoff.d.ts +4 -2
  58. package/dist/core/dynamic-prompt/handoff.js +1 -1
  59. package/dist/core/extensions/builtin/diff.js +14 -9
  60. package/dist/core/extensions/builtin/files.js +6 -2
  61. package/dist/core/extensions/builtin/gpt-apply-patch/parser.js +5 -1
  62. package/dist/core/extensions/builtin/gpt-apply-patch/streaming-parser.d.ts +8 -0
  63. package/dist/core/extensions/builtin/gpt-apply-patch/streaming-parser.js +26 -2
  64. package/dist/core/extensions/builtin/gpt-apply-patch/streaming-render.js +82 -11
  65. package/dist/core/extensions/builtin/gpt-apply-patch/types.d.ts +9 -1
  66. package/dist/core/extensions/builtin/permission-system/auto-credentials.d.ts +2 -0
  67. package/dist/core/extensions/builtin/permission-system/auto-credentials.js +17 -0
  68. package/dist/core/extensions/builtin/permission-system/auto-paths.d.ts +14 -0
  69. package/dist/core/extensions/builtin/permission-system/auto-paths.js +93 -0
  70. package/dist/core/extensions/builtin/permission-system/auto-policy.d.ts +20 -0
  71. package/dist/core/extensions/builtin/permission-system/auto-policy.js +126 -0
  72. package/dist/core/extensions/builtin/permission-system/auto-program-rules.d.ts +6 -0
  73. package/dist/core/extensions/builtin/permission-system/auto-program-rules.js +139 -0
  74. package/dist/core/extensions/builtin/permission-system/auto-shell-grammar.d.ts +29 -0
  75. package/dist/core/extensions/builtin/permission-system/auto-shell-grammar.js +74 -0
  76. package/dist/core/extensions/builtin/permission-system/auto-shell-segments.d.ts +13 -0
  77. package/dist/core/extensions/builtin/permission-system/auto-shell-segments.js +23 -0
  78. package/dist/core/extensions/builtin/permission-system/cli.d.ts +1 -0
  79. package/dist/core/extensions/builtin/permission-system/cli.js +9 -10
  80. package/dist/core/extensions/builtin/permission-system/config.d.ts +4 -1
  81. package/dist/core/extensions/builtin/permission-system/config.js +13 -1
  82. package/dist/core/extensions/builtin/permission-system/index.js +20 -12
  83. package/dist/core/extensions/builtin/permission-system/non-interactive.d.ts +11 -6
  84. package/dist/core/extensions/builtin/permission-system/non-interactive.js +8 -33
  85. package/dist/core/extensions/builtin/permission-system/service.d.ts +20 -1
  86. package/dist/core/extensions/builtin/permission-system/service.js +43 -7
  87. package/dist/core/extensions/builtin/permission-system/settings.d.ts +1 -0
  88. package/dist/core/extensions/builtin/permission-system/settings.js +4 -3
  89. package/dist/core/extensions/builtin/permission-system/types.d.ts +1 -1
  90. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-1.js +16 -3
  91. package/dist/core/extensions/eval-handle-host.d.ts +82 -0
  92. package/dist/core/extensions/eval-handle-host.js +28 -0
  93. package/dist/core/extensions/loader.js +5 -0
  94. package/dist/core/extensions/runner.js +4 -0
  95. package/dist/core/extensions/types.d.ts +15 -0
  96. package/dist/core/retry-fallback/usage-limit.js +5 -1
  97. package/dist/core/tools/diff-render.d.ts +4 -0
  98. package/dist/core/tools/diff-render.js +13 -0
  99. package/dist/core/tools/renderers/edit.js +6 -4
  100. package/dist/index.d.ts +1 -0
  101. package/dist/index.js +1 -0
  102. package/dist/main.js +12 -0
  103. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  104. package/dist/modes/interactive/interactive-mode.js +55 -11
  105. package/dist/modes/interactive/provider-error-presentation.d.ts +25 -0
  106. package/dist/modes/interactive/provider-error-presentation.js +42 -0
  107. package/dist/modes/interactive/session-control-registry.js +2 -0
  108. package/dist/modes/interactive/theme/dark.json +5 -5
  109. package/dist/modes/interactive/theme/light.json +5 -5
  110. package/dist/modes/rpc/custom-capability.d.ts +6 -0
  111. package/dist/modes/rpc/custom-capability.js +6 -0
  112. package/dist/modes/rpc/host-daemon-registration.d.ts +9 -0
  113. package/dist/modes/rpc/host-daemon-registration.js +2 -4
  114. package/dist/modes/rpc/host-ensure-start.js +2 -0
  115. package/dist/modes/rpc/host-handoff-unregistered.d.ts +24 -0
  116. package/dist/modes/rpc/host-handoff-unregistered.js +50 -0
  117. package/dist/modes/rpc/host-handoff.d.ts +7 -2
  118. package/dist/modes/rpc/host-handoff.js +7 -3
  119. package/dist/modes/rpc/host-legacy.d.ts +3 -0
  120. package/dist/modes/rpc/host-legacy.js +10 -5
  121. package/dist/modes/rpc/host-probe.d.ts +12 -0
  122. package/dist/modes/rpc/host-probe.js +53 -5
  123. package/dist/modes/rpc/host-runner.js +1 -1
  124. package/dist/modes/rpc/host-successor.d.ts +5 -4
  125. package/dist/modes/rpc/host-successor.js +11 -4
  126. package/dist/modes/rpc/rpc-client.d.ts +3 -0
  127. package/dist/modes/rpc/rpc-input-validation.d.ts +7 -0
  128. package/dist/modes/rpc/rpc-input-validation.js +37 -0
  129. package/dist/modes/rpc/rpc-types.d.ts +9 -0
  130. package/dist/modes/rpc/session-command-activity.d.ts +2 -0
  131. package/dist/modes/rpc/session-command-activity.js +25 -0
  132. package/dist/modes/rpc/session-command-router.js +11 -4
  133. package/dist/modes/rpc/session-registry-attach.js +1 -0
  134. package/dist/modes/rpc/session-registry-types.d.ts +7 -0
  135. package/dist/modes/rpc/session-registry-types.js +11 -0
  136. package/dist/modes/rpc/session-registry.js +6 -3
  137. package/dist/modes/rpc/session-sweep.d.ts +12 -2
  138. package/dist/modes/rpc/session-sweep.js +36 -3
  139. package/dist/modes/rpc/session-teardown.d.ts +2 -0
  140. package/dist/modes/rpc/session-teardown.js +1 -0
  141. package/dist/modes/rpc/worker-session-registry.js +5 -1
  142. package/docs/rpc.md +49 -7
  143. package/docs/settings.md +3 -1
  144. package/package.json +6 -6
  145. package/dist/bundle/chunks/chunk-K3BXFPWZ.js +0 -2
  146. package/dist/bundle/chunks/chunk-UUAKEDVS.js +0 -14
  147. package/dist/bundle/chunks/cli-main-VZWSUHFB.js +0 -3
  148. package/dist/bundle/chunks/host-command-7JIGGOWR.js +0 -2
  149. package/dist/bundle/chunks/interactive-mode-JA7AHYYL.js +0 -2
  150. package/dist/bundle/chunks/rpc-mode-QEBSN2OH.js +0 -2
  151. package/dist/bundle/chunks/session-control-endpoint-UGQIP6ZN.js +0 -3
@@ -1,11 +1,11 @@
1
1
  import { VERSION } from "../../config.js";
2
- import { ACCEPT_EDITS_PERMISSION_PRESET_CAPABILITY } from "../../core/extensions/builtin/permission-system/config.js";
2
+ import { ACCEPT_EDITS_PERMISSION_PRESET_CAPABILITY, AUTO_PERMISSION_PRESET_CAPABILITY, } from "../../core/extensions/builtin/permission-system/config.js";
3
3
  import { DURABLE_CLIENT_MESSAGE_ID_CAPABILITY } from "./client-admission-record.js";
4
4
  import { buildRpcSessionState } from "./connection-handler.js";
5
- import { AUTO_TITLE_PER_SESSION_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, BROWSER_ENGINE_CAPABILITY, CONTINUE_FROM_LEAF_CAPABILITY, DURABLE_SESSION_ID_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, PROMPT_SURFACE_CAPABILITY, PROMPT_SURFACE_CHAT_CAPABILITY, RETAIN_ON_DISCONNECT_CAPABILITY, SESSION_CONTEXT_CAPABILITY, SESSION_KIND_CAPABILITY, WARM_CAPABILITY, } from "./custom-capability.js";
5
+ import { AUTO_TITLE_PER_SESSION_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, BROWSER_ENGINE_CAPABILITY, CONTINUE_FROM_LEAF_CAPABILITY, DURABLE_SESSION_ID_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, PROMPT_SURFACE_CAPABILITY, PROMPT_SURFACE_CHAT_CAPABILITY, RETAIN_ON_DISCONNECT_CAPABILITY, RETRY_FALLBACK_PROFILE_CAPABILITY, SESSION_CONTEXT_CAPABILITY, SESSION_KIND_CAPABILITY, WARM_CAPABILITY, } from "./custom-capability.js";
6
6
  import { answerWarm } from "./host-warm.js";
7
7
  import { protocolIdentity } from "./protocol-identity.js";
8
- import { sessionAutoTitleError, sessionBrowserEngineError, sessionContextError, sessionKindError, sessionPromptSurfaceError, } from "./rpc-input-validation.js";
8
+ import { sessionAutoTitleError, sessionBrowserEngineError, sessionContextError, sessionKindError, sessionPromptSurfaceError, sessionRetryFallbackError, } from "./rpc-input-validation.js";
9
9
  import { RPC_ERROR_INVALID_LAUNCH_PROFILE, RPC_ERROR_INVALID_SESSION_CONTEXT, RPC_ERROR_INVALID_SESSION_KIND, RPC_ERROR_MISSING_SESSION_ID, RPC_ERROR_OPEN_FAILED, RPC_ERROR_SESSION_CLOSING, RPC_ERROR_UNKNOWN_SESSION, } from "./rpc-types.js";
10
10
  import { runWithSessionAttribution } from "./session-attribution.js";
11
11
  import { createRpcSessionBinding } from "./session-binding.js";
@@ -213,7 +213,10 @@ export class SessionCommandRouter {
213
213
  PROMPT_SURFACE_CHAT_CAPABILITY,
214
214
  // Each session's tool subprocesses and eval kernel get its own OMO_BROWSER_ENGINE from its launch profile.
215
215
  BROWSER_ENGINE_CAPABILITY,
216
+ // Each session's fallback chain is its own in-memory settings override, never the host's file.
217
+ RETRY_FALLBACK_PROFILE_CAPABILITY,
216
218
  ACCEPT_EDITS_PERMISSION_PRESET_CAPABILITY,
219
+ AUTO_PERMISSION_PRESET_CAPABILITY,
217
220
  // Only an in-process runtime shares the loop a warm loads into (senpi#2314).
218
221
  ...(this.registry.warm ? [WARM_CAPABILITY] : []),
219
222
  ...(this.connectionOptions?.capabilities ?? []),
@@ -317,7 +320,7 @@ export class SessionCommandRouter {
317
320
  sweepIdleSessions() {
318
321
  const now = this.idleNow();
319
322
  const idleEvictionMs = this.memoryPressure ? this.idleEvictionMs / 2 : this.idleEvictionMs;
320
- const verdicts = selectSweepEvictions(this.registry, now, idleEvictionMs);
323
+ const verdicts = selectSweepEvictions(this.registry, now, idleEvictionMs, (sessionId) => this.activeRequests.has(sessionId) || (this.bindings.get(sessionId)?.pendingPrompts?.().length ?? 0) > 0);
321
324
  for (const sessionId of verdicts.orphaned)
322
325
  void this.evictIdleSession(sessionId, "session_dir_removed");
323
326
  for (const sessionId of verdicts.idle)
@@ -486,6 +489,9 @@ export class SessionCommandRouter {
486
489
  const browserEngineError = sessionBrowserEngineError(command.browserEngine);
487
490
  if (browserEngineError)
488
491
  return error(command.id, "open_session", `${RPC_ERROR_INVALID_LAUNCH_PROFILE}: ${browserEngineError}`);
492
+ const retryFallbackError = sessionRetryFallbackError(command.retryFallback);
493
+ if (retryFallbackError)
494
+ return error(command.id, "open_session", `${RPC_ERROR_INVALID_LAUNCH_PROFILE}: ${retryFallbackError}`);
489
495
  let opened;
490
496
  try {
491
497
  opened = await this.registry.openSession({
@@ -504,6 +510,7 @@ export class SessionCommandRouter {
504
510
  ...(typeof command.auto_title === "boolean" ? { autoTitle: command.auto_title } : {}),
505
511
  ...(command.promptSurface !== undefined ? { promptSurface: command.promptSurface } : {}),
506
512
  ...(command.browserEngine !== undefined ? { browserEngine: command.browserEngine } : {}),
513
+ ...(command.retryFallback !== undefined ? { retryFallback: command.retryFallback } : {}),
507
514
  },
508
515
  // Host lifecycle policy, deliberately outside the immutable launch profile.
509
516
  { retainOnDisconnect: command.retain_on_disconnect === true });
@@ -6,6 +6,7 @@ export async function attachToOpenSession(entries, sessionPath, profile, options
6
6
  const [handle, entry] = existing;
7
7
  const wasParked = entry.retainOnDisconnect === true && entry.attachments === 0;
8
8
  entry.attachments += 1;
9
+ entry.detachedAt = undefined;
9
10
  // The claim carries the attachment state another generation decides on: a path this host is
10
11
  // actively serving a client on is never reclaimable from it.
11
12
  pathReservations?.setAttached(sessionPath, true);
@@ -50,6 +50,13 @@ export interface RpcSessionEntry {
50
50
  retainOnDisconnect?: boolean;
51
51
  /** Timestamp of the last routed command / observed activity; drives idle eviction. */
52
52
  lastCommandAt: number;
53
+ /**
54
+ * When the last attachment left (a retained session now standing detached), on the registry's
55
+ * clock; `undefined` while anyone is attached. A detached observational read does not refresh
56
+ * `lastCommandAt`, and the early-retirement sweep honors a minimum detach age so a transient
57
+ * reconnect keeps its runtime instead of re-opening cold.
58
+ */
59
+ detachedAt?: number;
53
60
  lifecycleMutex: Promise<void>;
54
61
  closeCompletion?: Promise<void>;
55
62
  closeResolve?: () => void;
@@ -14,6 +14,17 @@ export function frozenProfile(profile) {
14
14
  ...profile,
15
15
  ...(profile.creationModel ? { creationModel: Object.freeze({ ...profile.creationModel }) } : {}),
16
16
  ...(profile.sessionContext ? { sessionContext: Object.freeze({ ...profile.sessionContext }) } : {}),
17
+ ...(profile.retryFallback
18
+ ? {
19
+ retryFallback: Object.freeze({
20
+ modelFallback: profile.retryFallback.modelFallback,
21
+ fallbackChains: Object.freeze(Object.fromEntries(Object.entries(profile.retryFallback.fallbackChains).map(([key, entries]) => [
22
+ key,
23
+ Object.freeze([...entries]),
24
+ ]))),
25
+ }),
26
+ }
27
+ : {}),
17
28
  });
18
29
  }
19
30
  /**
@@ -5,6 +5,7 @@ import { createAgentSessionRuntime } from "../../core/agent-session-runtime.js";
5
5
  import { assertValidSessionId, SessionManager } from "../../core/session-manager.js";
6
6
  import { SESSION_PATH_RETRY_AFTER_MS } from "./host-reservations.js";
7
7
  import { createRegistryWarm } from "./host-warm.js";
8
+ import { refreshesSessionActivity } from "./session-command-activity.js";
8
9
  import { canonicalSessionPath } from "./session-path-key.js";
9
10
  import { attachToOpenSession } from "./session-registry-attach.js";
10
11
  import { settleClosingReservation, syncRuntimeMetadata } from "./session-registry-claims.js";
@@ -45,6 +46,7 @@ export class RpcSessionRegistry {
45
46
  this.options.pathReservations?.release(key);
46
47
  },
47
48
  markDetached: (key) => this.options.pathReservations?.setAttached(key, false),
49
+ now: () => this.now(),
48
50
  sync: () => this.syncRuntimeMetadata(),
49
51
  };
50
52
  }
@@ -202,9 +204,10 @@ export class RpcSessionRegistry {
202
204
  }
203
205
  if (entry.state !== "open" && entry.state !== "closing")
204
206
  throw new RpcSessionRegistryError("unknown_session");
205
- // Every routed command counts as activity for idle eviction. Lookups that
206
- // must not refresh idleness (sweeps, listing) use peek()/list() instead.
207
- entry.lastCommandAt = this.now();
207
+ // Polling a session nobody holds is observation, not work that needs its runtime;
208
+ // an attached client's polling keeps its session alive exactly as before.
209
+ if (entry.attachments > 0 || refreshesSessionActivity(command))
210
+ entry.lastCommandAt = this.now();
208
211
  return entry;
209
212
  }
210
213
  /**
@@ -8,11 +8,21 @@ export interface SweepVerdicts {
8
8
  */
9
9
  readonly orphaned: readonly string[];
10
10
  }
11
+ /**
12
+ * How long a session must stand detached before the early-retirement branch may
13
+ * park it: a disconnect that overlaps a sweep is a client about to come back,
14
+ * and re-opening its runtime cold is the expensive path.
15
+ */
16
+ export declare const DETACHED_RETIREMENT_GRACE_MS = 5000;
11
17
  /**
12
18
  * One occupancy sweep's decisions. "Idle" is the COMPLETE session-owned activity contract
13
19
  * (`AgentSession.isSessionBusy`: agent run, bash, background terminal jobs and other published
14
20
  * wake sources, compaction, barrier-held session work): a busy session restarts its idle clock
15
- * instead, so work that outlives a turn is never killed. A non-finite window evicts nothing idle.
21
+ * instead, so work that outlives a turn is never killed. A quiet detached in-process worker
22
+ * with flushed history can reopen by path and need not keep its runtime for the idle window -
23
+ * once it has been detached for at least `DETACHED_RETIREMENT_GRACE_MS`, so a transient
24
+ * reconnect keeps the runtime it already owns.
25
+ * A non-finite window evicts nothing idle.
16
26
  */
17
- export declare function selectSweepEvictions(registry: Pick<RpcSessionRegistry, "list" | "peek">, now: number, idleEvictionMs: number): SweepVerdicts;
27
+ export declare function selectSweepEvictions(registry: Pick<RpcSessionRegistry, "list" | "peek">, now: number, idleEvictionMs: number, hasPendingRequest?: (sessionId: string) => boolean): SweepVerdicts;
18
28
  //# sourceMappingURL=session-sweep.d.ts.map
@@ -1,11 +1,21 @@
1
1
  import { sessionDirectoryRemoved } from "./session-path-key.js";
2
+ /**
3
+ * How long a session must stand detached before the early-retirement branch may
4
+ * park it: a disconnect that overlaps a sweep is a client about to come back,
5
+ * and re-opening its runtime cold is the expensive path.
6
+ */
7
+ export const DETACHED_RETIREMENT_GRACE_MS = 5_000;
2
8
  /**
3
9
  * One occupancy sweep's decisions. "Idle" is the COMPLETE session-owned activity contract
4
10
  * (`AgentSession.isSessionBusy`: agent run, bash, background terminal jobs and other published
5
11
  * wake sources, compaction, barrier-held session work): a busy session restarts its idle clock
6
- * instead, so work that outlives a turn is never killed. A non-finite window evicts nothing idle.
12
+ * instead, so work that outlives a turn is never killed. A quiet detached in-process worker
13
+ * with flushed history can reopen by path and need not keep its runtime for the idle window -
14
+ * once it has been detached for at least `DETACHED_RETIREMENT_GRACE_MS`, so a transient
15
+ * reconnect keeps the runtime it already owns.
16
+ * A non-finite window evicts nothing idle.
7
17
  */
8
- export function selectSweepEvictions(registry, now, idleEvictionMs) {
18
+ export function selectSweepEvictions(registry, now, idleEvictionMs, hasPendingRequest = () => false) {
9
19
  const idle = [];
10
20
  const orphaned = [];
11
21
  for (const { sessionId, status, sessionPath } of registry.list()) {
@@ -20,10 +30,33 @@ export function selectSweepEvictions(registry, now, idleEvictionMs) {
20
30
  }
21
31
  if (!Number.isFinite(idleEvictionMs))
22
32
  continue;
23
- if (entry.worker?.busy || entry.runtime?.session.isSessionBusy) {
33
+ if (hasPendingRequest(sessionId))
34
+ continue;
35
+ const session = entry.runtime?.session;
36
+ if (entry.worker?.busy || session?.isSessionBusy) {
24
37
  entry.lastCommandAt = now;
25
38
  continue;
26
39
  }
40
+ if (entry.attachments > 0) {
41
+ // An attached client holds the live routing handle; only the ordinary deadline applies.
42
+ if (now - entry.lastCommandAt >= idleEvictionMs)
43
+ idle.push(sessionId);
44
+ continue;
45
+ }
46
+ if (entry.kind === "worker" && entry.retainOnDisconnect && session?.sessionManager.isTranscriptFlushed()) {
47
+ // Delivery debt is still owed a turn, even after the ordinary idle deadline.
48
+ if (session.pendingMessageCount > 0 || session.externalAdmission.list().pending.length > 0)
49
+ continue;
50
+ // A just-detached worker is a client about to reconnect; park it only once the
51
+ // disconnect has stood longer than the grace age. An entry at zero attachments
52
+ // with no detach stamp was never disconnected, so it keeps its normal window.
53
+ if (entry.detachedAt !== undefined) {
54
+ if (now - entry.detachedAt < DETACHED_RETIREMENT_GRACE_MS)
55
+ continue;
56
+ idle.push(sessionId);
57
+ continue;
58
+ }
59
+ }
27
60
  if (now - entry.lastCommandAt >= idleEvictionMs)
28
61
  idle.push(sessionId);
29
62
  }
@@ -6,6 +6,8 @@ export interface SessionTeardownHost {
6
6
  releaseReservation(key: string): 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
+ /** The registry's clock, so the detach stamp shares the sweep's timeline. */
10
+ now(): number;
9
11
  sync(): void;
10
12
  }
11
13
  export declare function beginSessionClose(host: SessionTeardownHost, handle: string, onRole?: (finalizer: boolean) => void, options?: {
@@ -25,6 +25,7 @@ export function beginSessionClose(host, handle, onRole, options) {
25
25
  // reservation, and is torn down only by an explicit close or the idle window.
26
26
  if (options?.detach && entry.retainOnDisconnect) {
27
27
  entry.attachments = 0;
28
+ entry.detachedAt ??= host.now();
28
29
  // A retained session nobody is attached to is what another generation may reclaim the path
29
30
  // from (#1893) - but only once nothing is still WRITING it. A session mid-turn keeps its claim
30
31
  // until it parks, because reclaiming it would put two writers on one transcript.
@@ -1,6 +1,7 @@
1
1
  import { isAbsolute } from "node:path";
2
2
  import { ProviderScope } from "@earendil-works/pi-ai/node/provider-scope";
3
3
  import { assertValidSessionId } from "../../core/session-manager.js";
4
+ import { refreshesSessionActivity } from "./session-command-activity.js";
4
5
  import { RESERVATION_DENIAL_CODES, SessionPathReservations, } from "./session-path-reservations.js";
5
6
  import { frozenProfile, RpcSessionRegistryError, sessionIdentity, } from "./session-registry.js";
6
7
  import { SessionWorkerClient } from "./session-worker-client.js";
@@ -139,7 +140,10 @@ export class WorkerSessionRegistry {
139
140
  throw new RpcSessionRegistryError("session_closing");
140
141
  if (entry.state !== "open" && entry.state !== "closing")
141
142
  throw new RpcSessionRegistryError("unknown_session");
142
- entry.lastCommandAt = this.now();
143
+ // Polling a session nobody holds is observation, not work that needs its runtime;
144
+ // an attached client's polling keeps its session alive exactly as before.
145
+ if (entry.attachments > 0 || refreshesSessionActivity(command))
146
+ entry.lastCommandAt = this.now();
143
147
  return entry;
144
148
  }
145
149
  beginClose(handle, onRole, options) {
package/docs/rpc.md CHANGED
@@ -288,8 +288,9 @@ Two guards decide whether a handoff is attempted at all, and both fail closed:
288
288
  handler for it, so a host from before the drain existed is never signalled - `handoffHost` answers
289
289
  `{ action: "refuse", reason: "handoff_unsupported" }` and `decideHostAction` reports `upgradeable: false`.
290
290
  - The registration must prove which process serves the socket (pid + start time, and the record's `socket`
291
- must be this endpoint). An unprovable owner refuses with `unknown_owner` rather than signalling a
292
- stranger (I1).
291
+ must be this endpoint) before that process is signalled. Without such a registration a handoff never
292
+ signals a stranger (I1): it refuses while that host holds a session, and replaces an idle one without
293
+ a signal unless a flat pre-layout-2 record proves it (see "Daemon state directory (layout 2)").
293
294
 
294
295
  On win32 a named pipe can be neither renamed nor drained: `handoffHost` refuses with `upgrade_unsupported`
295
296
  and `decideHostAction` never yields `handoff` there. Upgrades apply after `stopHost({ drain: true })` or an
@@ -430,6 +431,13 @@ pid and start time must match the live process, so a recycled pid proves nothing
430
431
  its endpoint answers with `generation_handoff` and `list_sessions` (workers included) lists no session.
431
432
  Otherwise it refuses `legacy_host`, and the CLI refusal carries `detail`: the pid, its endpoint, how many
432
433
  sessions it holds and the `host stop --drain --socket <endpoint>` that retires it.
434
+ - A handoff (`host handoff`, and an ensure's upgrade) on an endpoint no layout-2 record proves counts the
435
+ running host's sessions over a connection it keeps open. With any session, or no answer, it refuses:
436
+ `legacy_host` with the same `detail` when a flat record proves the process, `unknown_owner` with the
437
+ session count otherwise. With none, the successor takes the socket; a proven legacy process is sent the
438
+ drain only if a recount over that held connection still lists no session, and a host no record proves
439
+ is never signalled - it drains itself once another generation owns the public entry, and the handoff
440
+ writes one stderr warning naming the socket, that host's instance and its engine.
433
441
 
434
442
  `ensureHost` fails with a typed `HostDaemonStateError` naming the directory it could not create or write,
435
443
  and starts no host in that case.
@@ -867,6 +875,22 @@ surface, and `new_session` / `switch_session` / `fork` inside the session keep i
867
875
  `chat`: a host without it refuses `chat` with `invalid_launch_profile`, so a gateway falls back to `app`. Any value other
868
876
  than `terminal`, `app` or `chat` is refused with `invalid_launch_profile`.
869
877
 
878
+ #### Fallback chain per session (`retry_fallback_profile`)
879
+
880
+ `open_session.retryFallback: { modelFallback: boolean, fallbackChains: Record<string, string[]> }` is the fallback
881
+ policy THIS session runs with, e.g. a task child's own chain. It overrides that session's `retry.modelFallback` and
882
+ `retry.fallbackChains` in memory only: it is never written to a settings file, and no other session on the host sees
883
+ it. A session opened without the field keeps the host's settings. It is applied when the open creates the session; an
884
+ attach to a live session keeps the policy that session was created with. Probe `retry_fallback_profile` in
885
+ `get_protocol_info` before sending the field. A malformed profile (a missing `modelFallback`, a chain that is not an
886
+ array of non-empty selectors, a selector longer than 512 characters, more than 32 chains or 32 entries per chain) is
887
+ refused with `invalid_launch_profile` rather than opening a session without the chain its caller asked for; an empty
888
+ chain (`[]`) is accepted and means no fallback for that key. If something inside that session later changes a
889
+ fallback setting through the settings setters, that write follows the setters' own rules (it persists to the global
890
+ settings file) and replaces the session's override for that one key only: a `setFallbackChain` drops the profile's
891
+ chains, while `modelFallback` keeps the profile's value. The policy is not stored with the session, so a client that
892
+ reopens a session after it was closed sends the profile again.
893
+
870
894
  #### Browser engine per session (`browser_engine`)
871
895
 
872
896
  `open_session.browserEngine: "connected" | "builtin" | "none"` names the browser THIS session's skills drive: the
@@ -1029,7 +1053,7 @@ extensions and contents; neither runtime provides process-fatal OOM containment.
1029
1053
 
1030
1054
  What the host does enforce are lifecycle windows, and they only ever return memory from work nobody is doing:
1031
1055
 
1032
- - **Idle eviction**: a session with no routed command and no session-owned work for
1056
+ - **Idle eviction**: a session with no activity-refreshing command and no session-owned work for
1033
1057
  `SENPI_RPC_SESSION_IDLE_EVICTION_MS` (default 30 minutes) is closed through the exact `close_session` sequence
1034
1058
  (abort → waitForIdle → dispose, all attachments drained, path reservation released) and every attached connection
1035
1059
  receives that handle's `session_closed { reason: "idle_evicted" }` broadcast plus a final `close_session` response
@@ -1048,6 +1072,21 @@ What the host does enforce are lifecycle windows, and they only ever return memo
1048
1072
  background terminal jobs and any other published wake source (terminal monitors, loop-guard holds), compaction,
1049
1073
  and barrier-held session work all defer eviction, and the idle clock restarts when that work settles. An evicted
1050
1074
  session resumes like any other: the next `open_session` with the same `sessionPath` reopens it.
1075
+ - **Observational reads**: session state, history, model/auth inventory, loaded surfaces, and `memory_report`
1076
+ do not restart the idle clock of a **detached** session in either runtime. With any client still attached,
1077
+ these reads keep refreshing activity as before: an attached client polling `get_state` keeps the same live
1078
+ routing handle and runtime. An attached session receiving no commands can still idle-evict as before.
1079
+ Commands that change the session restart the clock even when detached. An in-flight request or prompt
1080
+ preflight prevents ordinary idle parking while the request is running; directory-removal teardown is separate.
1081
+ - **Completed detached workers**: on the in-process runtime, a retained `kind: "worker"` session with zero
1082
+ attachments is parked on the next occupancy sweep once its transcript is flushed and it has no session-owned
1083
+ work, queued input, admitted unwritten delivery, pending prompt, or in-flight request - provided the disconnect
1084
+ has already stood for at least five seconds (a short grace age, so a transient reconnect keeps the live runtime
1085
+ instead of re-opening cold). It does not wait out the normal idle window. This releases its runtime and eval
1086
+ kernels, not its durable history: reopen its
1087
+ `sessionPath` to continue under a new routing handle. Active monitors and other wake sources still prevent
1088
+ early parking. Unflushed workers, worker-isolate runtimes, and interactive sessions keep their normal window.
1089
+ A zero-attachment worker without a recorded disconnect time also keeps its normal idle deadline.
1051
1090
  - **Worker capacity** (worker runtime ONLY - a stdio host, `--listen stdio://`, an embedder, or a socket host that
1052
1091
  passed `--session-runtime worker` explicitly): at most 20 workers may be preparing, open, closing, or quarantined
1053
1092
  together, because each one is an isolate the host must keep alive. Admission beyond this bound fails explicitly
@@ -1064,7 +1103,8 @@ What the host does enforce are lifecycle windows, and they only ever return memo
1064
1103
  `open_session` with that `sessionPath` — from any connection — attaches to the same routing handle and returns
1065
1104
  `attached: true`. Retention never outranks an explicit teardown: a `close_session` from an attached connection still
1066
1105
  reaches zero attachments and closes the session, host shutdown closes it, and the idle-eviction window above still
1067
- parks it — announced as `session_parked { sessionId, sessionPath }`, after which the file reopens by path like any
1106
+ parks it (or the completed-detached-worker rule above parks it sooner) — announced as
1107
+ `session_parked { sessionId, sessionPath }`, after which the file reopens by path like any
1068
1108
  evicted session, and the parked session counts as gone for the empty-host exit below. It is also bounded by the empty-host exit
1069
1109
  and the supervisor's idle-exit window below: retention survives a client, not the host. Any attach may turn retention
1070
1110
  on for a live session; no attach turns it off for clients that already rely on it. Omitting the flag is byte-identical
@@ -1427,12 +1467,14 @@ Hosts that support `open_session.permissionPreset: "accept-edits"` advertise
1427
1467
  `permission_preset_accept_edits`. This preset allows project read/list/grep/edit
1428
1468
  and asks for bash, external_directory and other tools. Clients without that
1429
1469
  capability must use `ask`, never `workspace`, for an edit-only approval promise.
1430
- Explicit permission rules and remembered approvals retain their existing precedence.
1470
+ Hosts that support `permissionPreset: "auto"` advertise `permission_preset_auto`;
1471
+ clients without it must not send `auto`.
1472
+ 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.
1431
1473
 
1432
1474
  | Command | Params | Success data | Notes |
1433
1475
  | --- | --- | --- | --- |
1434
1476
  | `get_protocol_info` | - | `{ protocolVersion: 1, serverVersion: string, capabilities: string[], mode: "classic"\|"multi", instanceId: string, generation: number, engineVersion: string, engineOrdinal: [y, m, d, n, epoch], launch_profile: { profile_id, core }, memory_pressure?: boolean }` | Answered in BOTH modes; side-effect-free; the capability probe. Multi-session hosts include `multi_session`, `retain_on_disconnect`, `session_kind`, `session_context`, `auto_title_per_session`, `durable_session_id`, `prompt_surface` and `prompt_surface_chat` plus the negotiated launch capabilities. Those are HOST capabilities (a client never sends them) and are advertised only in multi-session mode, where the host owns the attachment refcount and the per-session launch profile. The identity fields are described under "Host identity" above; compatibility is decided from `protocolVersion`, `capabilities` and `engineOrdinal`, NEVER from `serverVersion`. |
1435
- | `open_session` | `sessionPath?`, `cwd?`, `provider?`, `modelId?`, `thinkingLevel?`, `permissionPreset?`, `retain_on_disconnect?`, `kind?`, `context?`, `auto_title?`, `durableSessionId?`, `promptSurface?`, `browserEngine?` (all optional; paths MUST be absolute) | `{ sessionId, state: RpcSessionState, attached?: true }` | `sessionPath` = today's `--session` semantics (open-if-exists else create persisting there, `session-manager.ts:926-940`); `provider`/`modelId` applied only on create (resume restores the session's model — mirrors `SenpiSessionRuntime.ts:198-200`); params form the immutable launch profile (D8). When the path is already held by a fully-open session, the open ATTACHES to it: same routing handle, `attached: true`, one more attachment counted; the runtime is torn down only when the last attachment closes. Idle sessions past the eviction window are closed by the host itself. `retain_on_disconnect: true` (default false) makes a dropped connection DETACH from this session instead of closing it — see "Retained sessions" below. `kind` (default `interactive`) and the opaque `context` map are described under "Session kind and context" above; both are stored frozen for the session's life and never influence auth, model or resource resolution. |
1477
+ | `open_session` | `sessionPath?`, `cwd?`, `provider?`, `modelId?`, `thinkingLevel?`, `permissionPreset?`, `retain_on_disconnect?`, `kind?`, `context?`, `auto_title?`, `durableSessionId?`, `promptSurface?`, `browserEngine?`, `retryFallback?` (all optional; paths MUST be absolute) | `{ sessionId, state: RpcSessionState, attached?: true }` | `sessionPath` = today's `--session` semantics (open-if-exists else create persisting there, `session-manager.ts:926-940`); `provider`/`modelId` applied only on create (resume restores the session's model — mirrors `SenpiSessionRuntime.ts:198-200`); params form the immutable launch profile (D8). When the path is already held by a fully-open session, the open ATTACHES to it: same routing handle, `attached: true`, one more attachment counted; the runtime is torn down only when the last attachment closes. Idle sessions past the eviction window are closed by the host itself. `retain_on_disconnect: true` (default false) makes a dropped connection DETACH from this session instead of closing it — see "Retained sessions" below. `kind` (default `interactive`) and the opaque `context` map are described under "Session kind and context" above; both are stored frozen for the session's life and never influence auth, model or resource resolution. |
1436
1478
  | `close_session` | `sessionId` | `{}` | Refused with `unknown_session` when the requesting connection never attached to that handle (a close releases the CALLER's attachment, and `list_sessions` publishes every handle). Otherwise aborts active work, awaits agent idle + settled persistence for up to the host grace window (default 10s), then quarantines any worker that has not exited without releasing its path reservation; its response is the LAST record tagged with that handle for the first closer — no events after (test-pinned). An admitted concurrent close joins the same teardown and receives its own successful response; output saturation rejects admission with the bounded close-overflow/resync notice described above. |
1437
1479
  | `list_sessions` | `include_workers?` (default false) | `{ sessions: [{ sessionId, durableSessionId, sessionPath, cwd, name, status, attachments, kind, context? }] }` | Includes `opening`/`closing` entries. Internally quarantined workers remain externally `closing` until exit. `attachments` is the session's live client attachment count; `0` on an `open` row is a retained session with no client attached. Every row carries `kind`. Rows with `kind: "worker"` are omitted unless `include_workers: true`, and `context` is published ONLY on that listing — a default listing carries no `context` at all. |
1438
1480
  | `release_session` | `sessionId`, `reason: "takeover"`, `interrupt?`, `force?` | `{ released: true, session_path, attachments }` | Hands the session to a runtime outside this host. See "Handing a session over (`release_session`)" below. |
@@ -1465,7 +1507,7 @@ In the response `error` field, machine-matchable:
1465
1507
  - `open_failed: <detail>`
1466
1508
  - `invalid_session_context: <detail>` (`open_session.context` past a documented cap: more than 32 keys, a key that does not match `^[a-z][a-z0-9_]*$`, a non-string or >16 KiB value, or more than 32 KiB of JSON in total; the detail names the cap and its byte budget)
1467
1509
  - `invalid_session_kind: <detail>` (`open_session.kind` other than `interactive` or `worker`)
1468
- - `invalid_launch_profile: <detail>` (`open_session.auto_title` present but not a boolean, `open_session.promptSurface` other than `terminal`, `app` or `chat`, or `open_session.browserEngine` other than `connected`, `builtin` or `none`)
1510
+ - `invalid_launch_profile: <detail>` (`open_session.auto_title` present but not a boolean, `open_session.promptSurface` other than `terminal`, `app` or `chat`, `open_session.browserEngine` other than `connected`, `builtin` or `none`, or a malformed `open_session.retryFallback`)
1469
1511
  - `host_memory_pressure` (sent only by hosts released before #2207, which declined to CREATE a `kind: "worker"` session above `SENPI_RPC_HOST_RSS_REFUSE_MB`; `errorData { rssMb, retry_after_ms }` says when to ask again. Current hosts never refuse an open for memory; a client talking to an older generation waits and retries, it never starts a second host or a per-child process)
1470
1512
  - `warm_failed: <detail>` (`warm` could not load its profile, for example a `cwd` that does not exist; not remembered, so a retry loads again)
1471
1513
  - `media_not_found` (`get_media` for an unknown `toolCallId`, or a `contentIndex` that does not point at an image block)
package/docs/settings.md CHANGED
@@ -27,7 +27,7 @@ Senpi includes a built-in permission system for tool calls. It evaluates a prese
27
27
 
28
28
  | Setting | Type | Default | Description |
29
29
  |---------|------|---------|-------------|
30
- | `permissionPreset` | string | `"full-access"` | Permission preset: `"full-access"`, `"workspace"`, `"read-only"`, or `"ask"` |
30
+ | `permissionPreset` | string | `"full-access"` | Permission preset: `"full-access"`, `"workspace"`, `"accept-edits"`, `"auto"`, `"read-only"`, or `"ask"` |
31
31
  | `permission` | object | - | Explicit permission rules that override the selected preset |
32
32
 
33
33
  Presets:
@@ -36,6 +36,8 @@ Presets:
36
36
  |--------|----------|
37
37
  | `full-access` | Allow all permission checks without prompting |
38
38
  | `workspace` | Allow `read`, `list`, `grep`, `edit`, and `bash`; ask for `external_directory` |
39
+ | `accept-edits` | Allow project `read`, `list`, `grep`, and `edit`; ask for `bash`, `external_directory`, and other tools |
40
+ | `auto` | Asks for everything except what it can prove stays inside the project, judged on the exact file the tool will open (the tool's own path resolution, symlinks followed): reads, listings and writes of project files through the file tools (`read`, `ls`, `find`, `write`, `edit`), `apply_patch` adding, changing or moving project files (a delete asks), a content search of one or more project files, and a small set of read-only shell commands (`ls cat head tail wc diff stat file cut grep rg echo pwd true which`, read-only git) with plain in-project arguments. Files count only when they land inside the project with no hidden component (except repository and formatter files such as `.gitignore`, `.github/`) and no credential-shaped name. Everything else asks: anything outside the project, `.env` and other dotfiles, `.git/`, `.vscode/`, keys, directory-wide content searches, any shell command with quotes, pipes, redirects, `cd` or a write (`cp`, `mv`, `rm`, `mkdir`), `git show`, text sent to a running process (`bash_input`), test runners, builds, installs and network tools. A session rooted at `/`, your home directory, any directory above it, or inside a hidden directory (such as `~/.config`) asks for everything. Each call is decided as the more restrictive of `auto`'s decision and your own rules' decision (deny over ask over allow), whatever order or layer (settings, CLI, RPC) your rules come from: your rules can narrow `auto`, never widen it. <br>`auto` prevents accidents; it is not a sandbox. Credential detection is by name, so a secret in a plainly named project file (for example `secrets.yml`, `terraform.tfvars`) can be read. A path the judge approved can be swapped for a symlink before the tool runs. An "Always" answer you gave (saved in `.senpi/permissions-approved.jsonl`, from this or an earlier session) still allows its pattern. |
39
41
  | `read-only` | Allow `read`, `list`, and `grep`; ask for `edit`, `bash`, and `external_directory` |
40
42
  | `ask` | Restore prompt-on-unknown behavior |
41
43
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi",
3
- "version": "2026.10.5",
3
+ "version": "2026.10.7",
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.5",
61
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.5",
62
- "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.5",
60
+ "@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.7",
61
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.7",
62
+ "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.7",
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.288",
80
80
  "@anthropic-ai/sdk": "0.127.0",
81
- "@code-yeongyu/senpi-codemode": "2026.10.5",
82
- "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.5",
81
+ "@code-yeongyu/senpi-codemode": "2026.10.7",
82
+ "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.7",
83
83
  "@modelcontextprotocol/sdk": "1.30.0",
84
84
  "@mozilla/readability": "0.6.0",
85
85
  "linkedom": "0.18.13",
@@ -1,2 +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{HOST_GENERATION_ENV,HOST_INSTANCE_ID_ENV}from"./chunk-CIRZTP7W.js";import{engineBuildIdentity}from"./chunk-XYDZ3LIE.js";import{parseArgs,resolveSessionRuntime}from"./chunk-MH2BFYGN.js";import*as crypto from"node:crypto";var CUSTOM_UNSUPPORTED_CAPABILITY="custom_unsupported",EXTENSION_EVENTS_CAPABILITY="extension_events",AUTO_TITLE_SESSIONS_CAPABILITY="auto_title_sessions",CONTINUE_FROM_LEAF_CAPABILITY="continue_from_leaf",MEDIA_PLACEHOLDERS_CAPABILITY="media_placeholders",QUESTION_CAPABILITY="question",RETAIN_ON_DISCONNECT_CAPABILITY="retain_on_disconnect",SESSION_CONTEXT_CAPABILITY="session_context",SESSION_KIND_CAPABILITY="session_kind",AUTO_TITLE_PER_SESSION_CAPABILITY="auto_title_per_session",WARM_CAPABILITY="warm",DURABLE_SESSION_ID_CAPABILITY="durable_session_id",PROMPT_SURFACE_CAPABILITY="prompt_surface",BROWSER_ENGINE_CAPABILITY="browser_engine",PROMPT_SURFACE_CHAT_CAPABILITY="prompt_surface_chat",RPC_CLIENT_CAPABILITIES_ENV="SENPI_RPC_CLIENT_CAPABILITIES";function parseClientCapabilities(value){return value?value.split(",").map(part=>part.trim()).filter(part=>part.length>0):[]}var DEFAULT_CUSTOM_EXTENSION_LABEL="custom UI component";function buildCustomUnsupportedRequest(capabilities,extensionName){if(!capabilities?.includes(CUSTOM_UNSUPPORTED_CAPABILITY))return;let name=extensionName.trim()||DEFAULT_CUSTOM_EXTENSION_LABEL;return{type:"extension_ui_request",id:crypto.randomUUID(),method:"custom_unsupported",extensionName:name}}import{createHash,randomUUID as randomUUID2}from"node:crypto";import{resolve}from"node:path";var INSTANCE_ID=resolveInstanceId(process.env[HOST_INSTANCE_ID_ENV]);function resolveInstanceId(value){return value!==void 0&&value.trim()!==""?value:randomUUID2()}function hostInstanceId(){return INSTANCE_ID}var cachedProfile;function hostGeneration(env){let value=env[HOST_GENERATION_ENV];return value!==void 0&&/^\d+$/.test(value)?Number(value):0}function hostLaunchProfile(argv,cwd){let parsed=parseArgs([...argv],{grokNeoEnabled:!1}),core={extensions:[...new Set((parsed.extensions??[]).map(path=>resolve(cwd,path)))].sort(),multi_session:parsed.multiSession===!0,session_runtime:resolveSessionRuntime(parsed)},canonical=JSON.stringify({extensions:core.extensions,multi_session:core.multi_session,session_runtime:core.session_runtime});return{profile_id:createHash("sha256").update(canonical).digest("hex"),core}}function socketHostLaunchProfile(hostArgs,cwd){return hostLaunchProfile(["--mode","rpc","--multi-session","--listen","unix://",...hostArgs],cwd)}function protocolIdentity(){cachedProfile??=hostLaunchProfile(process.argv.slice(2),process.cwd());let build=engineBuildIdentity();return{instanceId:INSTANCE_ID,generation:hostGeneration(process.env),engineVersion:build.text,engineOrdinal:build.ordinal,launch_profile:cachedProfile}}export{CUSTOM_UNSUPPORTED_CAPABILITY,EXTENSION_EVENTS_CAPABILITY,AUTO_TITLE_SESSIONS_CAPABILITY,CONTINUE_FROM_LEAF_CAPABILITY,MEDIA_PLACEHOLDERS_CAPABILITY,QUESTION_CAPABILITY,RETAIN_ON_DISCONNECT_CAPABILITY,SESSION_CONTEXT_CAPABILITY,SESSION_KIND_CAPABILITY,AUTO_TITLE_PER_SESSION_CAPABILITY,WARM_CAPABILITY,DURABLE_SESSION_ID_CAPABILITY,PROMPT_SURFACE_CAPABILITY,BROWSER_ENGINE_CAPABILITY,PROMPT_SURFACE_CHAT_CAPABILITY,RPC_CLIENT_CAPABILITIES_ENV,parseClientCapabilities,DEFAULT_CUSTOM_EXTENSION_LABEL,buildCustomUnsupportedRequest,hostInstanceId,hostGeneration,hostLaunchProfile,socketHostLaunchProfile,protocolIdentity};