@code-yeongyu/senpi 2026.11.10-3 → 2026.11.11

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 (108) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/bundle/chunks/{chunk-Z2C5N2LM.js → chunk-5F2XC6UC.js} +2 -2
  3. package/dist/bundle/chunks/{chunk-H3VJVKUK.js → chunk-5PIZWSNG.js} +2 -2
  4. package/dist/bundle/chunks/{chunk-DEQLHN2J.js → chunk-BPEA5SWW.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-DIHIEJFV.js → chunk-BUDJLVMF.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-47T6YMI2.js → chunk-IBDAV2AL.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-O5WJVIEU.js → chunk-J4ADZ2OK.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-T2SMQPRE.js → chunk-K3GAGIPX.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-HZDKQOWB.js → chunk-L6DF5G6K.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-E266ORGA.js → chunk-LJFQYCFH.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-DHAAWL2H.js → chunk-NRZAJUUO.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-R2Z7CX77.js → chunk-OSNR3IRD.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-AC6MDRAE.js → chunk-PUZDKFRT.js} +81 -81
  14. package/dist/bundle/chunks/{chunk-6Q7Y2G5A.js → chunk-SANWO37B.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-NN7NESW2.js → chunk-SS5VTBAV.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-JSD6O3RJ.js → chunk-XUDKT4UH.js} +1 -1
  17. package/dist/bundle/chunks/{cli-main-ZTT3N4DO.js → cli-main-7II7GCIR.js} +1 -1
  18. package/dist/bundle/chunks/{credential-accounts-5KHVGSCY.js → credential-accounts-N3B5YJS7.js} +1 -1
  19. package/dist/bundle/chunks/github-copilot.js +1 -1
  20. package/dist/bundle/chunks/{hand-entry-UV5XPMBF.js → hand-entry-ONOLRMVB.js} +1 -1
  21. package/dist/bundle/chunks/{host-command-BZN7WYYV.js → host-command-KWMJLPBS.js} +1 -1
  22. package/dist/bundle/chunks/{host-lifecycle-TNHASSFJ.js → host-lifecycle-4WQDCN3Y.js} +1 -1
  23. package/dist/bundle/chunks/host-lifecycle.js +4 -4
  24. package/dist/bundle/chunks/{host-session-command-O5WYHZXH.js → host-session-command-ZDYEKCUB.js} +1 -1
  25. package/dist/bundle/chunks/{interactive-mode-OCU4NDPC.js → interactive-mode-EPE4M32G.js} +1 -1
  26. package/dist/bundle/chunks/{main-CNDP6HCF.js → main-OM7BKVZG.js} +1 -1
  27. package/dist/bundle/chunks/{multi-session-host-ODGJAGJP.js → multi-session-host-DB6XMY53.js} +2 -2
  28. package/dist/bundle/chunks/{package-manager-cli-HSPUG4RJ.js → package-manager-cli-GLU5Q2KV.js} +1 -1
  29. package/dist/bundle/chunks/{rpc-mode-KOZPXU3U.js → rpc-mode-SDJVBT36.js} +1 -1
  30. package/dist/bundle/chunks/{schedule-command-SG44FE23.js → schedule-command-5SQD7M5N.js} +1 -1
  31. package/dist/bundle/chunks/{session-control-endpoint-5A24JYPE.js → session-control-endpoint-R25A2QGS.js} +1 -1
  32. package/dist/bundle/chunks/{session-picker-73YRS3GE.js → session-picker-GZM5CVQL.js} +1 -1
  33. package/dist/bundle/chunks/session-worker.js +88 -88
  34. package/dist/bundle/cli.js +2 -2
  35. package/dist/bundle/host-supervisor.js +4 -4
  36. package/dist/bundle/index.js +1 -1
  37. package/dist/bundle/rpc-entry.js +1 -1
  38. package/dist/bundle/runtime-manifest.json +1 -1
  39. package/dist/core/agent-session.d.ts +2 -0
  40. package/dist/core/credential-accounts.d.ts +2 -0
  41. package/dist/core/credential-accounts.js +10 -1
  42. package/dist/core/dynamic-prompt/intent-gate.js +2 -2
  43. package/dist/core/dynamic-prompt/style.js +2 -2
  44. package/dist/core/dynamic-prompt/verification.js +2 -2
  45. package/dist/core/extensions/builtin/anthropic-subscription/accounts.d.ts +3 -0
  46. package/dist/core/extensions/builtin/anthropic-subscription/claude-cli-account-test.js +3 -1
  47. package/dist/core/extensions/builtin/anthropic-subscription/claude-cli-connection-test.d.ts +3 -1
  48. package/dist/core/extensions/builtin/anthropic-subscription/claude-cli-connection-test.js +47 -16
  49. package/dist/core/extensions/builtin/anthropic-subscription/claude-cli-login-slots.d.ts +1 -1
  50. package/dist/core/extensions/builtin/anthropic-subscription/claude-cli-login-slots.js +4 -2
  51. package/dist/core/extensions/builtin/anthropic-subscription/errors.d.ts +1 -0
  52. package/dist/core/extensions/builtin/anthropic-subscription/errors.js +14 -5
  53. package/dist/core/extensions/builtin/anthropic-subscription/oauth-login.js +3 -3
  54. package/dist/core/extensions/builtin/goal/continuation.d.ts +6 -0
  55. package/dist/core/extensions/builtin/goal/continuation.js +3 -2
  56. package/dist/core/extensions/builtin/goal/index.js +10 -26
  57. package/dist/core/extensions/builtin/goal/lifecycle-helpers.d.ts +0 -6
  58. package/dist/core/extensions/builtin/goal/lifecycle-helpers.js +3 -37
  59. package/dist/core/extensions/builtin/goal/monitor-continuation.js +66 -7
  60. package/dist/core/extensions/builtin/goal/provider-retry.d.ts +9 -0
  61. package/dist/core/extensions/builtin/goal/provider-retry.js +16 -0
  62. package/dist/core/extensions/builtin/goal/reload-reengagement.d.ts +3 -2
  63. package/dist/core/extensions/builtin/goal/reload-reengagement.js +4 -4
  64. package/dist/core/extensions/builtin/goal/session-start-continuation.d.ts +28 -0
  65. package/dist/core/extensions/builtin/goal/session-start-continuation.js +75 -0
  66. package/dist/core/extensions/builtin/goal/store.d.ts +1 -0
  67. package/dist/core/extensions/builtin/goal/store.js +1 -1
  68. package/dist/core/extensions/builtin/handoff-hint/index.js +15 -11
  69. package/dist/core/extensions/builtin/handoff-hint/replacement-relay.d.ts +16 -0
  70. package/dist/core/extensions/builtin/handoff-hint/replacement-relay.js +17 -0
  71. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-1.js +3 -3
  72. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-2.js +5 -4
  73. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.js +3 -3
  74. package/dist/core/extensions/builtin/prompt-preset/claude-haiku-5-5.js +3 -3
  75. package/dist/core/extensions/builtin/prompt-preset/claude-opus-5-5.js +3 -3
  76. package/dist/core/extensions/builtin/prompt-preset/claude-opus-5.js +4 -4
  77. package/dist/core/extensions/builtin/prompt-preset/claude-sonnet-5-5.js +3 -3
  78. package/dist/core/extensions/builtin/prompt-preset/deepseek-v4-1-flash.js +3 -3
  79. package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.js +3 -3
  80. package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js +2 -2
  81. package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.js +3 -3
  82. package/dist/core/extensions/builtin/prompt-preset/grok-4.5.js +1 -1
  83. package/dist/core/extensions/builtin/prompt-preset/grok-4.6.js +4 -4
  84. package/dist/core/extensions/builtin/prompt-preset/grok-4.7.js +5 -5
  85. package/dist/core/extensions/builtin/prompt-preset/kimi-k3-1.js +4 -4
  86. package/dist/core/extensions/builtin/prompt-preset/kimi-k3.js +7 -6
  87. package/dist/core/resource-loader.js +19 -3
  88. package/dist/core/retry-fallback/controller-types.d.ts +2 -0
  89. package/dist/core/retry-fallback/controller.js +9 -2
  90. package/dist/core/retry-fallback/cooldown.d.ts +7 -0
  91. package/dist/core/retry-fallback/cooldown.js +13 -1
  92. package/dist/core/retry-fallback/credential-recovery.d.ts +7 -0
  93. package/dist/core/retry-fallback/credential-recovery.js +24 -0
  94. package/dist/core/retry-fallback/settings.d.ts +8 -5
  95. package/dist/core/retry-fallback/settings.js +9 -6
  96. package/dist/modes/rpc/delegation-declaration.js +1 -1
  97. package/dist/modes/rpc/host-gc-evidence.d.ts +1 -1
  98. package/dist/modes/rpc/host-gc-evidence.js +9 -3
  99. package/dist/modes/rpc/host-gc.js +4 -1
  100. package/dist/modes/rpc/rpc-session-state-types.d.ts +2 -0
  101. package/dist/modes/rpc/session-attachment-gate.js +3 -0
  102. package/dist/private-fd-startup-fillers.d.ts +2 -2
  103. package/dist/private-fd-startup-fillers.js +34 -4
  104. package/docs/rpc.md +2 -1
  105. package/docs/settings.md +1 -1
  106. package/native/socket-secret/prebuilds/win32-arm64/senpi_socket_secret.win32-arm64.node +0 -0
  107. package/native/socket-secret/prebuilds/win32-x64/senpi_socket_secret.win32-x64.node +0 -0
  108. package/package.json +6 -6
@@ -29,4 +29,28 @@ export async function releaseOnCredentialChange(state, original, deps, unusable)
29
29
  trigger: "sign-in",
30
30
  });
31
31
  }
32
+ /**
33
+ * A billing pin a usage limit caused holds only until the reset that limit stated: the account serves again
34
+ * then, so the pin and the original's cooldown, unusable mark and circuit are released and the existing
35
+ * revert path tries it once, with no re-login (senpi#4085). A pin with no stated reset (an empty credit
36
+ * balance) still holds until a sign-in. A still-spent original falls back and pins again.
37
+ */
38
+ export function releaseOnLimitReset(state, original, deps, unusable) {
39
+ if (!state.pinnedByBilling || state.pinnedUntil === undefined)
40
+ return;
41
+ if (deps.cooldowns.wallClock() < state.pinnedUntil)
42
+ return;
43
+ state.pinnedByBilling = false;
44
+ state.pinnedUntil = undefined;
45
+ state.pinned = state.pinnedByRefusal;
46
+ const selector = formatSelector(original);
47
+ deps.cooldowns.clearModel(selector);
48
+ unusable.clear(original);
49
+ deps.circuits?.close(selector);
50
+ deps.logger.info("credential_recovery_released", {
51
+ chainKey: state.chainKey,
52
+ originalSelector: state.originalSelector,
53
+ trigger: "limit-reset",
54
+ });
55
+ }
32
56
  //# sourceMappingURL=credential-recovery.js.map
@@ -33,11 +33,14 @@ export interface ResolvedRetryFallbackSettings {
33
33
  * follows Fable 5 whichever provider serves it - the builtin Anthropic provider, the
34
34
  * Anthropic Subscription extension, a gateway, or Bedrock.
35
35
  *
36
- * The ladder never leaves the Anthropic Opus family. The previous shipped default was
37
- * removed because it led with cross-family rungs (`k3`, `kimi-k3`), which moved a Claude
38
- * session onto another vendor mid-turn and could rank a guaranteed-refusal OAuth lane
39
- * first; a same-family step-down carries neither problem. There is still deliberately no
40
- * wildcard lane.
36
+ * The ladder never leaves Anthropic. The previous shipped default was removed because it
37
+ * led with cross-family rungs (`k3`, `kimi-k3`), which moved a Claude session onto another
38
+ * vendor mid-turn and could rank a guaranteed-refusal OAuth lane first; an Anthropic-only
39
+ * ladder carries neither problem. There is still deliberately no wildcard lane.
40
+ *
41
+ * Opus 5.5 steps to Fable 5.2 before Opus 5: Anthropic meters Opus on its own quota window,
42
+ * so a rate limit on Opus 5.5 also holds every other Opus rung on that account or gateway
43
+ * pool (senpi#6385).
41
44
  *
42
45
  * Rungs stay on current-generation Opus: a Claude session must never step down to
43
46
  * `claude-opus-4-8` or `claude-opus-4-6` (senpi#3727).
@@ -4,11 +4,14 @@
4
4
  * follows Fable 5 whichever provider serves it - the builtin Anthropic provider, the
5
5
  * Anthropic Subscription extension, a gateway, or Bedrock.
6
6
  *
7
- * The ladder never leaves the Anthropic Opus family. The previous shipped default was
8
- * removed because it led with cross-family rungs (`k3`, `kimi-k3`), which moved a Claude
9
- * session onto another vendor mid-turn and could rank a guaranteed-refusal OAuth lane
10
- * first; a same-family step-down carries neither problem. There is still deliberately no
11
- * wildcard lane.
7
+ * The ladder never leaves Anthropic. The previous shipped default was removed because it
8
+ * led with cross-family rungs (`k3`, `kimi-k3`), which moved a Claude session onto another
9
+ * vendor mid-turn and could rank a guaranteed-refusal OAuth lane first; an Anthropic-only
10
+ * ladder carries neither problem. There is still deliberately no wildcard lane.
11
+ *
12
+ * Opus 5.5 steps to Fable 5.2 before Opus 5: Anthropic meters Opus on its own quota window,
13
+ * so a rate limit on Opus 5.5 also holds every other Opus rung on that account or gateway
14
+ * pool (senpi#6385).
12
15
  *
13
16
  * Rungs stay on current-generation Opus: a Claude session must never step down to
14
17
  * `claude-opus-4-8` or `claude-opus-4-6` (senpi#3727).
@@ -18,7 +21,7 @@ export const DEFAULT_FALLBACK_CHAINS = {
18
21
  "claude-fable-5-2": ["claude-opus-5-5", "claude-opus-5"],
19
22
  "claude-fable-5-1": ["claude-opus-5-5", "claude-opus-5"],
20
23
  "claude-fable-5": ["claude-opus-5-5", "claude-opus-5"],
21
- "claude-opus-5-5": ["claude-opus-5"],
24
+ "claude-opus-5-5": ["claude-fable-5-2", "claude-opus-5"],
22
25
  };
23
26
  function cloneDefaultFallbackChains() {
24
27
  const chains = {};
@@ -54,7 +54,7 @@ export function samePrincipal(a, b) {
54
54
  function parseRoles(value) {
55
55
  if (value === undefined)
56
56
  return undefined;
57
- const invalid = "roles holds each of interactive_user, task_parent at most once";
57
+ const invalid = "roles holds each of interactive_user, task_parent, peer_agent at most once";
58
58
  if (!Array.isArray(value))
59
59
  return invalid;
60
60
  const roles = new Set();
@@ -1,5 +1,5 @@
1
1
  import { type HostDaemonDirectory } from "./host-daemon-paths.ts";
2
- export type EndpointInUse = "live_generation" | "live_claim" | "reachable";
2
+ export type EndpointInUse = "live_generation" | "live_claim" | "reachable" | "credential_unavailable";
3
3
  export type SocketSilence = "socket_refused" | "socket_absent";
4
4
  /**
5
5
  * The three-part test, in the order its answers are reported: (a) no generation pidfile names a live
@@ -33,12 +33,17 @@ export async function endpointInUse(paths, socket) {
33
33
  return { inUse: "live_claim" };
34
34
  }
35
35
  const silence = await socketSilence(socket);
36
+ if (silence === "credential_unavailable")
37
+ return { inUse: silence };
36
38
  if (silence === undefined)
37
39
  return { inUse: "reachable" };
38
40
  // A successor mid-handoff listens on `<socket>.next-<gen>` before it registers anywhere.
39
41
  const successors = (await socketSiblings(socket)).filter((name) => name.startsWith(`${basename(socket)}.next-`) && !isBindScratchName(name));
40
42
  for (const successor of successors) {
41
- if ((await socketSilence(join(dirname(socket), successor))) === undefined)
43
+ const successorSilence = await socketSilence(join(dirname(socket), successor));
44
+ if (successorSilence === "credential_unavailable")
45
+ return { inUse: successorSilence };
46
+ if (successorSilence === undefined)
42
47
  return { inUse: "reachable" };
43
48
  }
44
49
  return { inUse: undefined, silence };
@@ -101,10 +106,11 @@ async function recordIsLive(record) {
101
106
  async function socketSilence(socket) {
102
107
  let secret;
103
108
  if (process.platform === "win32") {
104
- // The pipe name includes the secret; without one no client can reach the pipe either.
109
+ // The pipe name includes the secret, so without a readable one nothing proves the pipe silent: a
110
+ // live host may still serve it under the key whose file was lost (sisyphuslabs/omo-desktop-app#4199).
105
111
  secret = await readSocketSecret(socketSecretPath(socket)).catch(() => undefined);
106
112
  if (secret === undefined)
107
- return "socket_absent";
113
+ return "credential_unavailable";
108
114
  }
109
115
  return new Promise((resolveProbe) => {
110
116
  const connection = createConnection(resolveSocketTransportAddress(socket, process.platform, secret));
@@ -14,6 +14,7 @@
14
14
  * live_generation a generation pidfile (any, the pointer's included) names a live process
15
15
  * live_claim a session-path claim in `reservations/` has a live owner
16
16
  * reachable the socket (or a `.next-*` successor bind) did not refuse the connection
17
+ * credential_unavailable win32: the socket secret cannot be read, so its pipe cannot be proven silent
17
18
  * locked the ensure lock was not free within 2 s
18
19
  * legacy_layout the agent dir predates layout 2; its flat files belong to a legacy host
19
20
  * unknown_identity nothing in the directory names its socket, so its lock cannot be taken
@@ -94,7 +95,9 @@ export async function gcEndpoint(socket, dir, options) {
94
95
  try {
95
96
  await options._test?.afterLockAcquired?.(socket);
96
97
  const evidence = await endpointInUse(hostDaemonDirectoryPaths(dir), socket);
97
- if (evidence.inUse === "live_claim" || (evidence.inUse !== undefined && options.staleEngine === undefined)) {
98
+ if (evidence.inUse === "live_claim" ||
99
+ evidence.inUse === "credential_unavailable" ||
100
+ (evidence.inUse !== undefined && options.staleEngine === undefined)) {
98
101
  return { removed: false, reason: evidence.inUse };
99
102
  }
100
103
  if (evidence.inUse !== undefined)
@@ -74,6 +74,8 @@ export interface RpcProviderAccount {
74
74
  model: string;
75
75
  outcome: "ok" | "failed";
76
76
  reason?: string;
77
+ message?: string;
78
+ status?: number;
77
79
  at: number;
78
80
  };
79
81
  /** `needs_sign_in`: the account must sign in again before it is used. */
@@ -68,6 +68,9 @@ export function attachmentGate(host, command) {
68
68
  return undefined;
69
69
  if (host.sessionsByConnection.get(owner)?.has(command.sessionId))
70
70
  return undefined;
71
+ // senpi#6305: the forwarder's peer lane delivers an agent's message without ever attaching; only its admit passes.
72
+ if (command.type === "admit_external_message" && host.connectionRoles.grantedByForwarder(owner, "peer_agent"))
73
+ return undefined;
71
74
  if (process.env.SENPI_RPC_REQUIRE_ATTACH === "1")
72
75
  return error(command.id, command.type, "session_not_attached", {
73
76
  reason: "this connection is not attached to that session",
@@ -22,8 +22,8 @@ export declare function releaseStartupFillers(child: {
22
22
  * them, see `withStartupPipe`) and removes their list from the hand-off, so a process this one re-executes or
23
23
  * spawns never closes a descriptor that is not a filler. A slot the #4749 private table names is that
24
24
  * channel's, never a filler, and stays open. A table that does not decode (the `unparseable` marker included)
25
- * cannot prove any slot free of a channel, so no filler is closed. Called first by every entry that can
26
- * receive a hand-off.
25
+ * cannot prove any slot free of a channel, so no filler is closed. A slot that no longer holds its filler is
26
+ * the runtime's own and stays open (`inheritedFillerCheck`). Called first by every entry that can receive a hand-off.
27
27
  */
28
28
  export declare function releaseInheritedStartupFillers(env: NodeJS.ProcessEnv): void;
29
29
  //# sourceMappingURL=private-fd-startup-fillers.d.ts.map
@@ -3,7 +3,7 @@
3
3
  * the release of the ones this process inherited. Side-effect free and never imports `private-fds.ts`: the
4
4
  * startup stub releases its fillers before that module adopts the table at import.
5
5
  */
6
- import { closeSync } from "node:fs";
6
+ import { closeSync, fstatSync } from "node:fs";
7
7
  import { HOST_STARTUP_EVENTS_ENV } from "./modes/rpc/host-identity-env.js";
8
8
  import { encodeStartupHandOff, parseStartupHandOff, STARTUP_EVENT_FD } from "./modes/rpc/host-startup-wire.js";
9
9
  import { decodePrivateFdTable, PRIVATE_FDS_TABLE_ENV } from "./private-fd-table.js";
@@ -53,13 +53,42 @@ function namedPrivateSlots(encoded) {
53
53
  slots.add(fd);
54
54
  return slots;
55
55
  }
56
+ function statOf(fd) {
57
+ try {
58
+ return fstatSync(fd);
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ }
64
+ const sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino;
65
+ /**
66
+ * Whether a listed slot still holds the filler the spawn put there. On POSIX a "pipe" slot is an AF_UNIX socketpair
67
+ * end under Node and Bun, and neither runtime holds a socket of its own this early; the startup pipe the same spawn
68
+ * placed at `STARTUP_EVENT_FD` must still be there too. A process that replaced its image between the spawn and this
69
+ * entry lost both, because Bun marks every inherited descriptor from 4 up close-on-exec at startup, and its runtime
70
+ * reuses the numbers: the compiled launcher's trust exec left Bun's own copies of stdout and stderr at 5 and 6, and
71
+ * closing them by number failed the host's first stderr write with EBADF (senpi#6282). Windows keeps the plain list.
72
+ */
73
+ function inheritedFillerCheck() {
74
+ if (process.platform === "win32")
75
+ return () => true;
76
+ const startup = statOf(STARTUP_EVENT_FD);
77
+ if (!startup?.isSocket())
78
+ return () => false;
79
+ const kept = [startup, ...[0, 1, 2].map(statOf).filter((stat) => stat !== undefined)];
80
+ return (slot) => {
81
+ const stat = statOf(slot);
82
+ return stat !== undefined && stat.isSocket() && !kept.some((other) => sameFile(stat, other));
83
+ };
84
+ }
56
85
  /**
57
86
  * Closes the filler pipes this process inherited (they only kept libuv from aborting the spawn that created
58
87
  * them, see `withStartupPipe`) and removes their list from the hand-off, so a process this one re-executes or
59
88
  * spawns never closes a descriptor that is not a filler. A slot the #4749 private table names is that
60
89
  * channel's, never a filler, and stays open. A table that does not decode (the `unparseable` marker included)
61
- * cannot prove any slot free of a channel, so no filler is closed. Called first by every entry that can
62
- * receive a hand-off.
90
+ * cannot prove any slot free of a channel, so no filler is closed. A slot that no longer holds its filler is
91
+ * the runtime's own and stays open (`inheritedFillerCheck`). Called first by every entry that can receive a hand-off.
63
92
  */
64
93
  export function releaseInheritedStartupFillers(env) {
65
94
  const handOff = parseStartupHandOff(env[HOST_STARTUP_EVENTS_ENV]);
@@ -67,8 +96,9 @@ export function releaseInheritedStartupFillers(env) {
67
96
  return;
68
97
  const privateSlots = namedPrivateSlots(env[PRIVATE_FDS_TABLE_ENV]);
69
98
  if (privateSlots) {
99
+ const inherited = inheritedFillerCheck();
70
100
  for (const slot of handOff.fillers) {
71
- if (privateSlots.has(slot))
101
+ if (privateSlots.has(slot) || !inherited(slot))
72
102
  continue;
73
103
  try {
74
104
  closeSync(slot);
package/docs/rpc.md CHANGED
@@ -2263,7 +2263,8 @@ which prints the bare socket path).
2263
2263
  `reason` is `socket_refused` or `socket_absent`. A sibling that is a directory is not unlinked: it stays and
2264
2264
  the removed entry lists it as `skipped: [{ path, type: "directory" }]`. Because the directory goes last, a
2265
2265
  removal that fails part-way leaves the endpoint listed for the next gc. Everything else is kept with its
2266
- reason: `live_generation`, `live_claim`, `reachable`, `locked` (an ensure held the lock for 2 s),
2266
+ reason: `live_generation`, `live_claim`, `reachable`, `credential_unavailable` (Windows: the socket secret
2267
+ is missing or invalid, so the pipe it names cannot be proven silent), `locked` (an ensure held the lock for 2 s),
2267
2268
  `legacy_layout` (a flat directory without `layout.json` - a legacy host's, never touched), `unknown_identity`
2268
2269
  (nothing names the socket, so its lock cannot be taken), or `failed` (reading the evidence or removing threw;
2269
2270
  the entry carries `error`) - and a `failed` endpoint never stops gc from going on to the others.
package/docs/settings.md CHANGED
@@ -332,7 +332,7 @@ Keep `retry.provider.maxRetries` at `0` unless provider-level retries are explic
332
332
 
333
333
  #### Model fallback chains
334
334
 
335
- `retry.fallbackChains` maps a primary-model selector to an ordered list of fallback selectors. A selector is `provider/model` with an optional `:thinking-level` suffix, or a bare `model` id that applies to that model vendor's own providers - for example `anthropic-subscription` and `anthropic` for Claude, `chatgpt-subscription` and `openai` for GPT, `google` and `google-vertex` for Gemini. A bare selector never expands onto a reseller, gateway, cloud re-host or custom provider that also serves the model; name one as `provider/model` to use it. A bare id from a vendor senpi does not know stays on the provider the session is already using. Within the vendor's providers, those holding an OAuth credential are preferred, then a fixed precedence order. Senpi ships bare default chains for `claude-fable-5-2`, `claude-fable-5-1` and `claude-fable-5` - `claude-opus-5-5`, then `claude-opus-5` - and for `claude-opus-5-5` - `claude-opus-5` - with no thinking level of their own, so a shipped rung inherits the session's thinking level (clamped to what the model supports), so Fable 5.2, Fable 5.1, Fable 5 and Opus 5.5 keep a fallback chain whichever provider serves them, and a fallback never leaves the Anthropic family; set a key to `[]` to opt out entirely, or set one `provider/claude-fable-5-1` key to override just that provider. For example, this switches Fable 5.1 to Kimi K3 at `max` thinking when an eligible failure occurs:
335
+ `retry.fallbackChains` maps a primary-model selector to an ordered list of fallback selectors. A selector is `provider/model` with an optional `:thinking-level` suffix, or a bare `model` id that applies to that model vendor's own providers - for example `anthropic-subscription` and `anthropic` for Claude, `chatgpt-subscription` and `openai` for GPT, `google` and `google-vertex` for Gemini. A bare selector never expands onto a reseller, gateway, cloud re-host or custom provider that also serves the model; name one as `provider/model` to use it. A bare id from a vendor senpi does not know stays on the provider the session is already using. Within the vendor's providers, those holding an OAuth credential are preferred, then a fixed precedence order. Senpi ships bare default chains for `claude-fable-5-2`, `claude-fable-5-1` and `claude-fable-5` - `claude-opus-5-5`, then `claude-opus-5` - and for `claude-opus-5-5` - `claude-fable-5-2`, then `claude-opus-5`, because a rate limit on one Opus model holds the whole Opus quota - with no thinking level of their own, so a shipped rung inherits the session's thinking level (clamped to what the model supports), so Fable 5.2, Fable 5.1, Fable 5 and Opus 5.5 keep a fallback chain whichever provider serves them, and a fallback never leaves the Anthropic family; set a key to `[]` to opt out entirely, or set one `provider/claude-fable-5-1` key to override just that provider. For example, this switches Fable 5.1 to Kimi K3 at `max` thinking when an eligible failure occurs:
336
336
 
337
337
  ```json
338
338
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi",
3
- "version": "2026.11.10-3",
3
+ "version": "2026.11.11",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -92,9 +92,9 @@
92
92
  },
93
93
  "dependencies": {
94
94
  "@earendil-works/chord": "0.99.1",
95
- "@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.11.10-3",
96
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.11.10-3",
97
- "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.11.10-3",
95
+ "@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.11.11",
96
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.11.11",
97
+ "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.11.11",
98
98
  "@silvia-odwyer/photon-node": "0.3.4",
99
99
  "ajv": "8.20.0",
100
100
  "chalk": "6.0.0",
@@ -115,8 +115,8 @@
115
115
  "yaml": "2.9.1",
116
116
  "@anthropic-ai/claude-agent-sdk": "0.3.321",
117
117
  "@anthropic-ai/sdk": "0.127.0",
118
- "@code-yeongyu/senpi-codemode": "2026.11.10-3",
119
- "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.11.10-3",
118
+ "@code-yeongyu/senpi-codemode": "2026.11.11",
119
+ "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.11.11",
120
120
  "@modelcontextprotocol/sdk": "1.32.1",
121
121
  "@mozilla/readability": "0.6.0",
122
122
  "linkedom": "0.18.13",