@code-yeongyu/senpi 2026.10.24-3 → 2026.10.25-1

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 (94) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/bundle/chunks/{args-6T5EQVY3.js → args-JXULI4EH.js} +1 -1
  3. package/dist/bundle/chunks/{chunk-VUKNGMDT.js → chunk-3LNSLJ7N.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-MRN6IKOQ.js → chunk-3VOG6WFU.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-AVLYZET7.js → chunk-7WAJCESX.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-4TRKE3ZC.js → chunk-BMYNMUVZ.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-LR6Z6V54.js → chunk-BTNYLNWZ.js} +2 -2
  8. package/dist/bundle/chunks/{chunk-JAXZOEJE.js → chunk-BWA4FVFW.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-AGQSRWNN.js → chunk-EHQXLHRB.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-I5NIJZDY.js → chunk-EYBDBR7V.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-YJA2B7FQ.js → chunk-FDL4CF6F.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-HCF5M6NA.js → chunk-GXY5JEI5.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-CYG4LS4S.js → chunk-J5P2VL3Y.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-DYWHYQUD.js → chunk-PXKF2RHD.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-H6IG2ITJ.js → chunk-QH2HSCMX.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-CZYADQ2E.js → chunk-SGDOZT6B.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-BZ75DRZB.js → chunk-U67ML6CQ.js} +4 -2
  18. package/dist/bundle/chunks/{chunk-DITSGW4R.js → chunk-URBJS24R.js} +75 -75
  19. package/dist/bundle/chunks/{chunk-CZR7C6CY.js → chunk-VOBD6FQG.js} +1 -1
  20. package/dist/bundle/chunks/{chunk-SGATKHO4.js → chunk-Y5OTHMXO.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-GOB5BLDV.js → chunk-YDCRA5ES.js} +2 -2
  22. package/dist/bundle/chunks/{cli-main-BOYWBHOB.js → cli-main-TABBP55A.js} +1 -1
  23. package/dist/bundle/chunks/{dispatch-KQEGIXQW.js → dispatch-DEP4PEDZ.js} +3 -2
  24. package/dist/bundle/chunks/{help-fast-path-6PVN7WI5.js → help-fast-path-ZHZGXAUG.js} +1 -1
  25. package/dist/bundle/chunks/{host-command-7MAXSZIH.js → host-command-OQR4RDTM.js} +1 -1
  26. package/dist/bundle/chunks/{host-session-command-L2UTORL5.js → host-session-command-IGB27ZPJ.js} +1 -1
  27. package/dist/bundle/chunks/{interactive-mode-NVBFOE5R.js → interactive-mode-DGQNSXUA.js} +1 -1
  28. package/dist/bundle/chunks/{list-tips-BO5RP34W.js → list-tips-CREX4IXZ.js} +1 -1
  29. package/dist/bundle/chunks/{multi-session-host-3TVASSQD.js → multi-session-host-M7HVUMRH.js} +2 -2
  30. package/dist/bundle/chunks/{package-manager-cli-5BHVDVJC.js → package-manager-cli-FSR5P6YL.js} +1 -1
  31. package/dist/bundle/chunks/{role-BHA5ROSF.js → role-M2MTVACJ.js} +1 -1
  32. package/dist/bundle/chunks/{rotation-stream-RXMCJJUL.js → rotation-stream-U7YQMC7H.js} +1 -1
  33. package/dist/bundle/chunks/{rpc-mode-LPP46I7H.js → rpc-mode-SZAJNKKL.js} +1 -1
  34. package/dist/bundle/chunks/{schedule-command-QW67J57R.js → schedule-command-XBW4EOG5.js} +1 -1
  35. package/dist/bundle/chunks/{session-control-endpoint-QQBBY5AT.js → session-control-endpoint-WEF3XTJR.js} +1 -1
  36. package/dist/bundle/chunks/{session-picker-HIC5ZTE6.js → session-picker-KQUFWDQC.js} +1 -1
  37. package/dist/bundle/chunks/session-worker.js +98 -95
  38. package/dist/bundle/cli.js +2 -2
  39. package/dist/bundle/index.js +1 -1
  40. package/dist/bundle/rpc-entry.js +1 -1
  41. package/dist/bundle/runtime-manifest.json +1 -1
  42. package/dist/cli/args.d.ts +2 -0
  43. package/dist/cli/args.js +27 -0
  44. package/dist/core/agent-session.js +1 -1
  45. package/dist/core/extensions/builtin/anthropic-subscription/index.js +4 -2
  46. package/dist/core/extensions/builtin/anthropic-subscription/options.js +2 -1
  47. package/dist/core/extensions/builtin/anthropic-subscription/sdk-boundary.js +9 -2
  48. package/dist/core/extensions/builtin/anthropic-subscription/session-binding.d.ts +2 -0
  49. package/dist/core/extensions/builtin/anthropic-subscription/session-commit-boundary.d.ts +6 -1
  50. package/dist/core/extensions/builtin/anthropic-subscription/session-commit-boundary.js +18 -7
  51. package/dist/core/extensions/builtin/anthropic-subscription/session-commit-divergence.d.ts +23 -0
  52. package/dist/core/extensions/builtin/anthropic-subscription/session-commit-divergence.js +107 -0
  53. package/dist/core/extensions/builtin/anthropic-subscription/session-cwd.d.ts +3 -0
  54. package/dist/core/extensions/builtin/anthropic-subscription/session-cwd.js +9 -0
  55. package/dist/core/extensions/builtin/anthropic-subscription/session-entry-annotations.d.ts +1 -1
  56. package/dist/core/extensions/builtin/anthropic-subscription/session-entry-annotations.js +3 -1
  57. package/dist/core/extensions/builtin/anthropic-subscription/session-observability.d.ts +3 -0
  58. package/dist/core/extensions/builtin/anthropic-subscription/session-observability.js +4 -0
  59. package/dist/core/extensions/builtin/anthropic-subscription/session-registry-wiring.js +12 -7
  60. package/dist/core/extensions/builtin/anthropic-subscription/session-registry.d.ts +3 -1
  61. package/dist/core/extensions/builtin/anthropic-subscription/session-registry.js +3 -2
  62. package/dist/core/extensions/builtin/anthropic-subscription/session-stream.js +2 -0
  63. package/dist/core/extensions/builtin/anthropic-subscription/settings.d.ts +1 -1
  64. package/dist/core/extensions/builtin/anthropic-subscription/settings.js +3 -1
  65. package/dist/core/extensions/builtin/anthropic-subscription/stream.js +4 -1
  66. package/dist/core/extensions/builtin/btw/index.js +4 -1
  67. package/dist/core/extensions/builtin/compaction/lane-policy.js +3 -1
  68. package/dist/core/extensions/builtin/look-at/runner.js +1 -0
  69. package/dist/core/extensions/builtin/mcp/connection-types.d.ts +2 -0
  70. package/dist/core/extensions/builtin/mcp/connection.js +3 -0
  71. package/dist/core/extensions/builtin/mcp/elicitation.d.ts +5 -2
  72. package/dist/core/extensions/builtin/mcp/elicitation.js +57 -11
  73. package/dist/core/extensions/builtin/mcp/index.js +1 -1
  74. package/dist/core/extensions/builtin/mcp/modern/dispatch.js +6 -9
  75. package/dist/core/extensions/builtin/mcp/service-connection.d.ts +1 -0
  76. package/dist/core/extensions/builtin/mcp/service-connection.js +1 -0
  77. package/dist/core/extensions/builtin/mcp/service.js +1 -0
  78. package/dist/core/extensions/builtin/mcp/transport-sdk.d.ts +1 -0
  79. package/dist/core/extensions/builtin/mcp/transport-sdk.js +5 -5
  80. package/dist/core/extensions/builtin/mcp/transport.d.ts +1 -0
  81. package/dist/core/extensions/builtin/mcp/transport.js +1 -0
  82. package/dist/core/external-admission.d.ts +4 -1
  83. package/dist/core/external-admission.js +18 -2
  84. package/dist/core/lifecycle-session-binding.js +20 -0
  85. package/dist/core/sdk.js +1 -0
  86. package/dist/core/session-spawned-by.d.ts +4 -0
  87. package/dist/core/session-spawned-by.js +16 -0
  88. package/dist/core/skill-access.js +15 -3
  89. package/dist/core/skill-discovery.d.ts +2 -0
  90. package/dist/core/skill-discovery.js +1 -1
  91. package/dist/main.js +7 -2
  92. package/dist/modes/rpc/session-router-open-profile.js +2 -10
  93. package/docs/usage.md +14 -0
  94. package/package.json +6 -6
@@ -4,7 +4,7 @@ export type AnnotatableRegistry = {
4
4
  touch(entry: AnthropicSubscriptionSessionEntry): void;
5
5
  };
6
6
  export declare function switchEntryModel(registry: AnnotatableRegistry, senpiSessionId: string, modelId: string, sdkModel?: string): Promise<boolean>;
7
- export declare function annotatePendingFork(registry: AnnotatableRegistry, senpiSessionId: string, reason: string): void;
7
+ export declare function annotatePendingFork(registry: AnnotatableRegistry, senpiSessionId: string, reason: string, divergedPath?: string): void;
8
8
  export declare function annotateTainted(registry: AnnotatableRegistry, senpiSessionId: string, reason: string): void;
9
9
  export declare function annotateBranchInfo(registry: AnnotatableRegistry, senpiSessionId: string, info: SessionBranchInfo): void;
10
10
  //# sourceMappingURL=session-entry-annotations.d.ts.map
@@ -8,11 +8,13 @@ export async function switchEntryModel(registry, senpiSessionId, modelId, sdkMod
8
8
  registry.touch(entry);
9
9
  return true;
10
10
  }
11
- export function annotatePendingFork(registry, senpiSessionId, reason) {
11
+ export function annotatePendingFork(registry, senpiSessionId, reason, divergedPath) {
12
12
  const entry = registry.get(senpiSessionId);
13
13
  if (!entry)
14
14
  return;
15
15
  entry.pendingForkReason = reason;
16
+ // A fork recorded without a path (compaction, tree change) must not inherit an earlier rewrite's path.
17
+ entry.pendingForkDivergedPath = divergedPath ?? null;
16
18
  registry.touch(entry);
17
19
  }
18
20
  export function annotateTainted(registry, senpiSessionId, reason) {
@@ -27,6 +27,8 @@ export type ContinuityObservation = {
27
27
  seat?: string;
28
28
  /** The attempt runs on a different seat than the lineage's last one: its prompt cache is cold. */
29
29
  seatChanged?: boolean;
30
+ /** senpi#1975: first diverged field path behind an assistant_rewritten decision. Paths only, never values. */
31
+ divergedPath?: string;
30
32
  };
31
33
  export type ContinuityObservabilityBoundary = {
32
34
  emit: (observation: ContinuityObservation) => void;
@@ -62,6 +64,7 @@ export declare function observeSessionSyncDecision(input: {
62
64
  senpiSessionId: string;
63
65
  payloadBytes?: number;
64
66
  collapsedDirectives?: number;
67
+ divergedPath?: string;
65
68
  }): ContinuityObservation;
66
69
  export type StagedContinuityDecision = {
67
70
  observation: ContinuityObservation;
@@ -133,6 +133,7 @@ export function consumePendingCloseCause(senpiSessionId) {
133
133
  * incremental→delta, resume→fork, cold-seed→flatten.
134
134
  */
135
135
  export function observeSessionSyncDecision(input) {
136
+ const diverged = input.divergedPath === undefined ? {} : { divergedPath: input.divergedPath };
136
137
  if (input.kind === "incremental") {
137
138
  return { kind: "delta", reason: "prefix_matched", deltaMessages: input.deltaMessages };
138
139
  }
@@ -142,6 +143,7 @@ export function observeSessionSyncDecision(input) {
142
143
  kind: "fork",
143
144
  reason: retainedCause ?? (input.reason === undefined ? "branch_resume" : sanitizeReason(input.reason)),
144
145
  deltaMessages: input.deltaMessages,
146
+ ...diverged,
145
147
  };
146
148
  }
147
149
  // Peek, not consume: the staged observation is emitted only when the
@@ -155,6 +157,7 @@ export function observeSessionSyncDecision(input) {
155
157
  deltaMessages: input.deltaMessages,
156
158
  ...(input.payloadBytes !== undefined ? { payloadBytes: input.payloadBytes } : {}),
157
159
  ...(input.collapsedDirectives !== undefined ? { collapsedDirectives: input.collapsedDirectives } : {}),
160
+ ...diverged,
158
161
  };
159
162
  }
160
163
  /** Stages an attempt's decision; `emit` fires only when the attempt is retained. */
@@ -181,6 +184,7 @@ export function emitContinuityObservation(observation, sessionId, onDecision) {
181
184
  ...(observation.settingSource === undefined ? {} : { settingSource: observation.settingSource }),
182
185
  ...(observation.seat === undefined ? {} : { seat: observation.seat }),
183
186
  ...(observation.seatChanged === undefined ? {} : { seatChanged: observation.seatChanged }),
187
+ ...(observation.divergedPath === undefined ? {} : { divergedPath: observation.divergedPath }),
184
188
  ...(sessionId === undefined ? {} : { sessionId }),
185
189
  });
186
190
  }
@@ -9,17 +9,22 @@ import { bindingFromEntry, forgetBinding, getBinding, rememberBinding, rememberB
9
9
  import { closeSession, getSession, recordBranchInfo, recordPendingFork, switchSessionModel, } from "./session-registry.js";
10
10
  import { sentHashesForEntry, sentMessageHashes, sentMessages } from "./session-sync.js";
11
11
  const commitBoundary = new AssistantCommitBoundary();
12
- function persistBindingInvalidation(pi, sessionId, reason) {
13
- pi.appendEntry?.(BINDING_ENTRY_TYPE, { schemaVersion: 1, invalidated: true, reason });
12
+ function persistBindingInvalidation(pi, sessionId, reason, divergedPath) {
13
+ pi.appendEntry?.(BINDING_ENTRY_TYPE, {
14
+ schemaVersion: 1,
15
+ invalidated: true,
16
+ reason,
17
+ ...(divergedPath === undefined ? {} : { divergedPath }),
18
+ });
14
19
  rememberBindingInvalidation(sessionId, reason);
15
20
  }
16
- async function invalidateBinding(pi, ctx, reason) {
21
+ async function invalidateBinding(pi, ctx, reason, divergedPath) {
17
22
  const sessionId = ctx.sessionManager.getSessionId();
18
23
  forgetBinding(sessionId);
19
24
  const sessionFile = ctx.sessionManager.getSessionFile?.();
20
25
  if (sessionFile)
21
26
  await deleteStoredBinding(sessionFile);
22
- persistBindingInvalidation(pi, sessionId, reason);
27
+ persistBindingInvalidation(pi, sessionId, reason, divergedPath);
23
28
  }
24
29
  function keepBindingThenClose(sessionId, reason) {
25
30
  const entry = getSession(sessionId);
@@ -129,10 +134,10 @@ export function registerSessionRegistry(pi) {
129
134
  commitBoundary.forget(sessionId);
130
135
  return;
131
136
  }
132
- const outcome = commitBoundary.commit(sessionId, event.message, modelId);
137
+ const { outcome, divergedPath } = commitBoundary.commit(sessionId, event.message, modelId);
133
138
  if (outcome === "rewritten") {
134
- recordPendingFork(sessionId, "assistant_rewritten");
135
- await invalidateBinding(pi, ctx, "assistant_rewritten");
139
+ recordPendingFork(sessionId, "assistant_rewritten", divergedPath);
140
+ await invalidateBinding(pi, ctx, "assistant_rewritten", divergedPath);
136
141
  return;
137
142
  }
138
143
  // Only an assistant this provider actually produced may anchor a record.
@@ -41,6 +41,8 @@ export interface AnthropicSubscriptionSessionEntry {
41
41
  taintedReason: string | null;
42
42
  /** Wave C seam: a divergence boundary recorded for the next continuity decision. */
43
43
  pendingForkReason: string | null;
44
+ /** senpi#1975: first diverged field path the commit boundary named for the pending fork, when it named one. */
45
+ pendingForkDivergedPath: string | null;
44
46
  lastUsedAt: number;
45
47
  /** Digest of the access token the resident subprocess was spawned with. */
46
48
  credentialDigest?: string;
@@ -91,7 +93,7 @@ export declare function closeSession(senpiSessionId: string, reason: string): vo
91
93
  export declare function isIdleExpired(entry: Pick<AnthropicSubscriptionSessionEntry, "lastUsedAt">): boolean;
92
94
  export declare function markTainted(senpiSessionId: string, reason: string): void;
93
95
  export declare function switchSessionModel(senpiSessionId: string, modelId: string, sdkModel?: string): Promise<boolean>;
94
- export declare function recordPendingFork(senpiSessionId: string, reason: string): void;
96
+ export declare function recordPendingFork(senpiSessionId: string, reason: string, divergedPath?: string): void;
95
97
  export declare function recordBranchInfo(senpiSessionId: string, info: SessionBranchInfo): void;
96
98
  export declare function isCurrentGeneration(senpiSessionId: string, generation: number): boolean;
97
99
  export declare function isBoundAccountTokenExpiring(entry: Pick<AnthropicSubscriptionSessionEntry, "accountName">, accounts: readonly Pick<AccountSlot, "name" | "expires" | "source">[]): boolean;
@@ -162,6 +162,7 @@ export class AnthropicSubscriptionSessionRegistry {
162
162
  branchInfo: null,
163
163
  taintedReason: null,
164
164
  pendingForkReason: null,
165
+ pendingForkDivergedPath: null,
165
166
  lastUsedAt: now,
166
167
  };
167
168
  // oxlint-disable-next-line eslint/prefer-const -- a closure reads it; const would throw in the TDZ if that closure ran before the assignment, so the rewrite is reviewed separately
@@ -250,8 +251,8 @@ export function markTainted(senpiSessionId, reason) {
250
251
  export async function switchSessionModel(senpiSessionId, modelId, sdkModel) {
251
252
  return switchEntryModel(sessionRegistry, senpiSessionId, modelId, sdkModel);
252
253
  }
253
- export function recordPendingFork(senpiSessionId, reason) {
254
- annotatePendingFork(sessionRegistry, senpiSessionId, reason);
254
+ export function recordPendingFork(senpiSessionId, reason, divergedPath) {
255
+ annotatePendingFork(sessionRegistry, senpiSessionId, reason, divergedPath);
255
256
  }
256
257
  export function recordBranchInfo(senpiSessionId, info) {
257
258
  annotateBranchInfo(sessionRegistry, senpiSessionId, info);
@@ -155,6 +155,7 @@ async function createResidentAttempt(input, auth) {
155
155
  ? flattenResult.blocks
156
156
  : buildDeltaPromptBlocks(messages.slice(from), input.customToolNameToSdk);
157
157
  const payloadBytes = flattenResult ? serializedPayloadBytes(flattenResult.blocks) : undefined;
158
+ const divergedPath = observedReason === "assistant_rewritten" ? (existing?.pendingForkDivergedPath ?? undefined) : undefined;
158
159
  const staged = stageContinuityDecision({
159
160
  ...observeSessionSyncDecision({
160
161
  kind: observedKind,
@@ -166,6 +167,7 @@ async function createResidentAttempt(input, auth) {
166
167
  ...(flattenResult?.collapsedDirectives !== undefined
167
168
  ? { collapsedDirectives: flattenResult.collapsedDirectives }
168
169
  : {}),
170
+ ...(divergedPath === undefined ? {} : { divergedPath }),
169
171
  }),
170
172
  ...seat,
171
173
  }, sessionId, input.onContinuityDecision,
@@ -42,6 +42,6 @@ export declare function resolveSystemPromptMode(settings: AnthropicSubscriptionP
42
42
  /** Load the provider block with env values taking precedence over project and global values. */
43
43
  export declare function loadAnthropicSubscriptionProviderSettings(settingsManager: SettingsManager, environment?: Environment): AnthropicSubscriptionProviderSettings;
44
44
  /** Load settings from Senpi's configured global and project settings.json paths. */
45
- export declare function loadAnthropicSubscriptionProviderSettingsFromDisk(cwd: string): AnthropicSubscriptionProviderSettings;
45
+ export declare function loadAnthropicSubscriptionProviderSettingsFromDisk(sessionCwd: string): AnthropicSubscriptionProviderSettings;
46
46
  export {};
47
47
  //# sourceMappingURL=settings.d.ts.map
@@ -1,6 +1,7 @@
1
1
  import { getAgentDir } from "../../../../config.js";
2
2
  import { getFileContentRevision } from "../../../../utils/paths.js";
3
3
  import { getSettingsPath, SettingsManager } from "../../../settings-manager.js";
4
+ import { requireAnthropicSubscriptionCwd } from "./session-cwd.js";
4
5
  /** The one place the default lives: senpi owns compaction unless `compactionOwner: "sdk"` opts out. */
5
6
  export function resolveCompactionOwner(settings) {
6
7
  return settings.compactionOwner ?? "senpi";
@@ -152,7 +153,8 @@ function settingsFingerprint(path) {
152
153
  }
153
154
  let cachedAnthropicSubscriptionManager;
154
155
  /** Load settings from Senpi's configured global and project settings.json paths. */
155
- export function loadAnthropicSubscriptionProviderSettingsFromDisk(cwd) {
156
+ export function loadAnthropicSubscriptionProviderSettingsFromDisk(sessionCwd) {
157
+ const cwd = requireAnthropicSubscriptionCwd(sessionCwd);
156
158
  // fallbackEligible() calls this per candidate probe; cache the manager by
157
159
  // (cwd, settings content revision) and re-apply env live to avoid locked disk reads.
158
160
  const agentDir = getAgentDir();
@@ -13,6 +13,7 @@ import { refusalError } from "./refusal.js";
13
13
  import { getSdkBoundary, loadClaudeAgentSdk } from "./sdk-boundary.js";
14
14
  import { emitContinuityObservation } from "./session-observability.js";
15
15
  import { forgetBinding } from "./session-reattach.js";
16
+ import { requireAnthropicSubscriptionCwd } from "./session-cwd.js";
16
17
  import { residentSessionMessages } from "./session-stream.js";
17
18
  import { loadAnthropicSubscriptionProviderSettingsFromDisk, resolveCompactionOwner, resumeModeSource, } from "./settings.js";
18
19
  import { applyStreamEvent } from "./stream-events.js";
@@ -58,6 +59,7 @@ export function streamAnthropicSubscription(model, context, options) {
58
59
  let coldSeedEstimate;
59
60
  let nativeCompactionStopped = false;
60
61
  try {
62
+ const cwd = requireAnthropicSubscriptionCwd(options?.cwd);
61
63
  // Resident before the synchronous SDK member below (getSdkBoundary().query)
62
64
  // reads it - see sdk-boundary.lazy.ts.
63
65
  await loadClaudeAgentSdk();
@@ -67,7 +69,7 @@ export function streamAnthropicSubscription(model, context, options) {
67
69
  if (sessionKey)
68
70
  toolWatch.reconcileWithContext(sessionKey, context);
69
71
  const toolWatchNote = toolWatch.buildPromptNote(sessionKey, context, resolvedTools.customToolNameToSdk);
70
- const providerSettings = loadAnthropicSubscriptionProviderSettingsFromDisk(process.cwd());
72
+ const providerSettings = loadAnthropicSubscriptionProviderSettingsFromDisk(cwd);
71
73
  const senpiOwnsCompaction = resolveCompactionOwner(providerSettings) === "senpi";
72
74
  const toolLessRequest = options?.toolChoice === "none";
73
75
  const mcpServers = toolLessRequest ? undefined : await buildCustomToolServers(resolvedTools.customTools);
@@ -79,6 +81,7 @@ export function streamAnthropicSubscription(model, context, options) {
79
81
  model,
80
82
  context,
81
83
  streamOptions: options,
84
+ cwd,
82
85
  providerSettings,
83
86
  authLane,
84
87
  tools: resolvedTools.sdkTools,
@@ -103,7 +103,10 @@ export default function btwExtension(pi) {
103
103
  },
104
104
  sessionId,
105
105
  thinkingLevel: thinkingLevel === "off" ? undefined : thinkingLevel,
106
- streamFn: (streamModel, streamContext, options) => ctx.modelRegistry.modelRuntime.streamSimple(streamModel, streamContext, options),
106
+ streamFn: (streamModel, streamContext, options) => ctx.modelRegistry.modelRuntime.streamSimple(streamModel, streamContext, {
107
+ ...options,
108
+ cwd: ctx.cwd,
109
+ }),
107
110
  }, context, {
108
111
  signal: controller.signal,
109
112
  onTextDelta: (delta) => {
@@ -22,6 +22,7 @@
22
22
  */
23
23
  import { ANTHROPIC_SUBSCRIPTION_PROVIDER_ID } from "../anthropic-subscription/account-management.js";
24
24
  import { isColdSeedOverflowMessage } from "../anthropic-subscription/cold-seed-budget.js";
25
+ import { requireAnthropicSubscriptionCwd } from "../anthropic-subscription/session-cwd.js";
25
26
  import { loadAnthropicSubscriptionProviderSettingsFromDisk, resolveCompactionOwner, } from "../anthropic-subscription/settings.js";
26
27
  /** Custom session entry type carrying a mirrored SDK compaction boundary. */
27
28
  export const ANTHROPIC_SUBSCRIPTION_COMPACT_ENTRY_TYPE = "claude-sdk-oauth-compact";
@@ -76,8 +77,9 @@ export function createCompactionLanePolicy(options = {}) {
76
77
  // undefined when the settings cannot be read, so callers fail closed to
77
78
  // senpi's full behavior.
78
79
  const readProviderSettings = (cwd) => {
80
+ const sessionCwd = requireAnthropicSubscriptionCwd(cwd);
79
81
  try {
80
- return load(cwd);
82
+ return load(sessionCwd);
81
83
  }
82
84
  catch {
83
85
  return undefined;
@@ -33,6 +33,7 @@ export async function runLookAt(args, signal, ctx, store, dependencies = {}) {
33
33
  ((model, context, options) => ctx.modelRegistry.modelRuntime.streamSimple(model, context, options));
34
34
  const reasoning = toStreamReasoning(resolved.thinkingLevel);
35
35
  const response = await streamRunner(resolved.model, { systemPrompt: LOOK_AT_SYSTEM_PROMPT, messages: [userMessage] }, {
36
+ cwd: ctx.cwd,
36
37
  ...(reasoning === undefined ? {} : { reasoning }),
37
38
  maxTokens: 4096,
38
39
  signal: request.signal,
@@ -29,6 +29,8 @@ export interface ServerConnectionOptions {
29
29
  readonly env?: Record<string, string | undefined>;
30
30
  readonly authProvider?: McpOAuthProvider;
31
31
  readonly elicitationUiProvider?: McpElicitationUiProvider;
32
+ /** Print/json processes cannot present elicitation; UI-less lessees fail independently. */
33
+ readonly elicitationEnabled?: boolean;
32
34
  /** Present when the server declares references: every new transport re-reads them (senpi#3858). */
33
35
  readonly resolveConfig?: () => Promise<McpServerConfig>;
34
36
  /** Called when a shared renewal finds this owner's generation changed, so the owner re-keys. */
@@ -14,6 +14,7 @@ export class ServerConnection {
14
14
  #config;
15
15
  #authProvider;
16
16
  #elicitationUiProvider;
17
+ #elicitationEnabled;
17
18
  #resolveConfig;
18
19
  #projectPolicy;
19
20
  #policyAbort;
@@ -34,6 +35,7 @@ export class ServerConnection {
34
35
  this.#env = options.env;
35
36
  this.#authProvider = options.authProvider;
36
37
  this.#elicitationUiProvider = options.elicitationUiProvider;
38
+ this.#elicitationEnabled = options.elicitationEnabled;
37
39
  this.#resolveConfig = options.resolveConfig;
38
40
  this.#projectPolicy = options.projectPolicy;
39
41
  this.#startedGrantNames = options.projectPolicy?.initial.grantedNames ?? [];
@@ -239,6 +241,7 @@ export class ServerConnection {
239
241
  return current;
240
242
  },
241
243
  elicitationUiProvider: this.#elicitationUiProvider,
244
+ elicitationEnabled: this.#elicitationEnabled,
242
245
  logger: this.#logger,
243
246
  resolveConfig: this.#resolveConfig,
244
247
  serverName: this.serverName,
@@ -4,8 +4,11 @@ import type { ExtensionUIContext } from "../../types.ts";
4
4
  export declare const MCP_CLIENT_ELICITATION_CAPABILITY: {
5
5
  readonly elicitation: {};
6
6
  };
7
+ export declare function mcpClientCapabilities(elicitationEnabled?: boolean): {
8
+ elicitation?: Record<string, never>;
9
+ };
7
10
  export declare const MCP_ELICITATION_TIMEOUT_MS: number;
8
- type ElicitationUi = Pick<ExtensionUIContext, "input" | "select" | "confirm">;
11
+ type ElicitationUi = Pick<ExtensionUIContext, "input" | "select">;
9
12
  export type McpElicitationUiProvider = () => ElicitationUi | undefined;
10
13
  export interface McpElicitationUiOwner {
11
14
  getMcpElicitationUi(): ElicitationUi | undefined;
@@ -29,7 +32,7 @@ export declare function setMcpElicitationUiProvider(provider: McpElicitationUiPr
29
32
  * in `transport-sdk.ts`) awaits it before any connect, so a mid-call
30
33
  * elicitation still cannot race registration.
31
34
  */
32
- export declare function configureMcpElicitation(client: Client, owner?: McpElicitationUiOwner | McpElicitationUiProvider | undefined, timeoutMs?: number): Promise<void>;
35
+ export declare function configureMcpElicitation(client: Client, owner?: McpElicitationUiOwner | McpElicitationUiProvider | undefined, timeoutMs?: number, elicitationEnabled?: boolean): Promise<void>;
33
36
  /** Exported for tests: drive the form directly with a scripted UI. */
34
37
  export declare function runElicitationForm(ui: ElicitationUi, message: string, requestedSchema: {
35
38
  properties?: Record<string, ElicitProperty>;
@@ -3,9 +3,9 @@
3
3
  // The client declares the capability as an EMPTY object — Spring-AI servers
4
4
  // reject richer shapes (FieldCoding lesson from the comparison corpus) — and
5
5
  // answers elicitation/create requests mid tool-call by walking the requested
6
- // flat-primitive schema through sequential ctx.ui primitives (input/select/
7
- // confirm; NEVER ctx.ui.custom, which is undefined in RPC mode). Without a UI
8
- // (print mode) the request is declined immediately — spec-correct, no hang.
6
+ // flat-primitive schema through sequential ctx.ui primitives (input/select;
7
+ // NEVER ctx.ui.custom, which is undefined in RPC mode). Without a UI
8
+ // (print mode) the request fails with a processing error, never a user refusal.
9
9
  // The whole form is bounded by a timeout that resolves to cancel so a wedged
10
10
  // dialog can never wedge the tool-call pipeline. URL mode is deliberately not
11
11
  // implemented in v1.
@@ -14,28 +14,46 @@ import { loadMcpSdkTypes } from "./sdk.lazy.js";
14
14
  import { safeTimer } from "./wrap.js";
15
15
  /** Declared empty on purpose: maximum server compatibility (form mode only). */
16
16
  export const MCP_CLIENT_ELICITATION_CAPABILITY = { elicitation: {} };
17
+ export function mcpClientCapabilities(elicitationEnabled = true) {
18
+ return elicitationEnabled ? MCP_CLIENT_ELICITATION_CAPABILITY : {};
19
+ }
17
20
  export const MCP_ELICITATION_TIMEOUT_MS = 5 * 60 * 1000;
18
21
  let legacyUiProvider;
19
22
  /** Legacy classic-session seam. Session-owned services pass themselves directly. */
20
23
  export function setMcpElicitationUiProvider(provider) {
21
24
  legacyUiProvider = provider;
22
25
  }
26
+ async function rejectElicitation() {
27
+ const { ErrorCode, McpError } = await loadMcpSdkTypes();
28
+ throw new McpError(ErrorCode.InternalError, "Elicitation cannot be processed");
29
+ }
23
30
  /**
24
31
  * Wire the elicitation/create handler onto a fresh client. Async because the
25
32
  * request schema comes from the lazily loaded SDK; the caller (`buildMcpClient`
26
33
  * in `transport-sdk.ts`) awaits it before any connect, so a mid-call
27
34
  * elicitation still cannot race registration.
28
35
  */
29
- export async function configureMcpElicitation(client, owner = legacyUiProvider, timeoutMs = MCP_ELICITATION_TIMEOUT_MS) {
36
+ export async function configureMcpElicitation(client, owner = legacyUiProvider, timeoutMs = MCP_ELICITATION_TIMEOUT_MS, elicitationEnabled = true) {
37
+ if (!elicitationEnabled) {
38
+ // The SDK requires an advertised capability for setRequestHandler.
39
+ // Its public fallback handles unsolicited requests without claiming UI support.
40
+ const { ErrorCode, McpError } = await loadMcpSdkTypes();
41
+ client.fallbackRequestHandler = async (request) => {
42
+ if (request.method === "elicitation/create")
43
+ return rejectElicitation();
44
+ throw new McpError(ErrorCode.MethodNotFound, "Method not found");
45
+ };
46
+ return;
47
+ }
30
48
  const { ElicitRequestSchema } = await loadMcpSdkTypes();
31
49
  client.setRequestHandler(ElicitRequestSchema, async (request) => {
32
50
  const params = request.params;
33
51
  // URL mode is deliberately unsupported in v1 (form mode only).
34
52
  if (!("requestedSchema" in params))
35
- return { action: "decline" };
53
+ return rejectElicitation();
36
54
  const ui = typeof owner === "function" ? owner() : owner?.getMcpElicitationUi();
37
55
  if (ui === undefined)
38
- return { action: "decline" };
56
+ return rejectElicitation();
39
57
  return await runElicitationForm(ui, params.message, params.requestedSchema, timeoutMs);
40
58
  });
41
59
  }
@@ -61,31 +79,59 @@ async function collectForm(ui, message, requestedSchema) {
61
79
  for (const [name, property] of Object.entries(requestedSchema.properties ?? {})) {
62
80
  const title = `${message} - ${property.title ?? name}`;
63
81
  if (property.type === "boolean") {
64
- content[name] = await ui.confirm(title, property.description ?? name);
82
+ // confirm collapses cancellation into false, including automated RPC replies.
83
+ // A selection preserves dismissal separately from a human's explicit No.
84
+ const picked = await ui.select(property.description ? `${title}\n${property.description}` : title, [
85
+ "Yes",
86
+ "No",
87
+ ]);
88
+ if (picked === undefined)
89
+ return { action: "cancel" };
90
+ // The thread gateway also accepts y/n and true/false, ignoring case and whitespace.
91
+ switch (picked.trim().toLowerCase()) {
92
+ case "yes":
93
+ case "y":
94
+ case "true":
95
+ content[name] = true;
96
+ break;
97
+ case "no":
98
+ case "n":
99
+ case "false":
100
+ content[name] = false;
101
+ break;
102
+ default:
103
+ return rejectElicitation();
104
+ }
65
105
  continue;
66
106
  }
67
107
  if (Array.isArray(property.enum) && property.enum.length > 0) {
68
108
  const picked = await ui.select(title, property.enum.map(String));
69
109
  if (picked === undefined)
70
- return { action: "decline" };
110
+ return { action: "cancel" };
71
111
  content[name] = picked;
72
112
  continue;
73
113
  }
74
114
  const answer = await ui.input(title, property.description);
75
- if (answer === undefined || answer.length === 0) {
115
+ if (answer === undefined)
116
+ return { action: "cancel" };
117
+ if (answer.length === 0) {
76
118
  if (required.has(name))
77
- return { action: "decline" };
119
+ return rejectElicitation();
78
120
  continue;
79
121
  }
80
122
  if (property.type === "number" || property.type === "integer") {
81
123
  const parsed = Number(answer);
82
124
  if (!Number.isFinite(parsed))
83
- return { action: "decline" };
125
+ return rejectElicitation();
84
126
  content[name] = parsed;
85
127
  continue;
86
128
  }
87
129
  content[name] = answer;
88
130
  }
131
+ // Even a fieldless form needs an explicit submission rather than an automatic accept.
132
+ if (Object.keys(content).length === 0 && (await ui.select(message, ["Submit"])) !== "Submit") {
133
+ return { action: "cancel" };
134
+ }
89
135
  return { action: "accept", content };
90
136
  }
91
137
  //# sourceMappingURL=elicitation.js.map
@@ -123,7 +123,7 @@ export function createMcpExtension(service, sessionOwned = true) {
123
123
  }
124
124
  else {
125
125
  // Elicitation (todo 41): point mid-call forms at this session's UI.
126
- service.setMcpElicitationUiProvider(() => ctx.ui);
126
+ service.setMcpElicitationUiProvider(() => (ctx.hasUI ? ctx.ui : undefined));
127
127
  await (attachPromise ?? attach({ type: "session_start", reason: "startup" }, ctx));
128
128
  }
129
129
  const skills = preview ? [] : (event.systemPromptOptions.skills ?? []);
@@ -1,4 +1,4 @@
1
- import { configureMcpElicitation, MCP_CLIENT_ELICITATION_CAPABILITY } from "../elicitation.js";
1
+ import { configureMcpElicitation, mcpClientCapabilities } from "../elicitation.js";
2
2
  import { ConnectError } from "../errors.js";
3
3
  import { loadMcpSdkClient } from "../sdk.lazy.js";
4
4
  import { materializeMcpTransport, } from "../transport-sdk.js";
@@ -9,13 +9,10 @@ import { probeStdioTransport } from "./stdio-probe.js";
9
9
  import { ModernHttpTransport } from "./transport.js";
10
10
  import { MODERN_PROTOCOL_VERSION } from "./wire.js";
11
11
  const clientInfo = { name: "senpi-mcp-client", version: "0.0.0" };
12
- const probeOptions = {
13
- clientInfo,
14
- clientCapabilities: MCP_CLIENT_ELICITATION_CAPABILITY,
15
- supportedVersions: [MODERN_PROTOCOL_VERSION],
16
- };
17
12
  export async function materializeNegotiatedTransport(options, serverName, timeoutMs, signal) {
18
13
  signal.throwIfAborted();
14
+ const clientCapabilities = mcpClientCapabilities(options.elicitationEnabled);
15
+ const probeOptions = { clientInfo, clientCapabilities, supportedVersions: [MODERN_PROTOCOL_VERSION] };
19
16
  const { spec, logger } = options;
20
17
  let outcome = { kind: "legacy-fallback" };
21
18
  if (spec.kind === "http") {
@@ -32,13 +29,13 @@ export async function materializeNegotiatedTransport(options, serverName, timeou
32
29
  });
33
30
  }
34
31
  const { Client } = await loadMcpSdkClient();
35
- const client = new Client(clientInfo, { capabilities: MCP_CLIENT_ELICITATION_CAPABILITY });
36
- await configureMcpElicitation(client, options.elicitationUiProvider);
32
+ const client = new Client(clientInfo, { capabilities: clientCapabilities });
33
+ await configureMcpElicitation(client, options.elicitationUiProvider, undefined, options.elicitationEnabled);
37
34
  const transport = new ModernSdkBridge(new ModernHttpTransport({
38
35
  url: spec.url,
39
36
  fetch,
40
37
  clientInfo,
41
- clientCapabilities: MCP_CLIENT_ELICITATION_CAPABILITY,
38
+ clientCapabilities,
42
39
  protocolVersion: outcome.version,
43
40
  }), outcome.discovery, outcome.version);
44
41
  const version = outcome.version;
@@ -14,6 +14,7 @@ interface SessionConnectionOptions {
14
14
  readonly generation: McpConfigGeneration;
15
15
  readonly declared: McpServerDeclaredConfig;
16
16
  readonly session: McpSessionOptions;
17
+ readonly elicitationEnabled: boolean;
17
18
  readonly cwd: string;
18
19
  readonly ui: McpElicitationUiProvider;
19
20
  readonly artifacts: McpOutputArtifacts;
@@ -38,6 +38,7 @@ export function createMcpSessionConnection(options) {
38
38
  config,
39
39
  env: projectPolicy?.initial.env ?? session.env,
40
40
  elicitationUiProvider: options.ui,
41
+ elicitationEnabled: options.elicitationEnabled,
41
42
  logger,
42
43
  serverName: name,
43
44
  ...(renewal === undefined ? {} : { resolveConfig: renewal.resolveConfig, onGenerationChanged: rekey }),
@@ -709,6 +709,7 @@ export class McpService {
709
709
  await this.#resyncToLiveSessions(owner);
710
710
  },
711
711
  session: options,
712
+ elicitationEnabled: declarer.context.mode !== "print" && declarer.context.mode !== "json",
712
713
  cwd: declarer.context.cwd,
713
714
  ui: () => this.getMcpElicitationUi(),
714
715
  artifacts: this.#outputArtifacts,
@@ -44,6 +44,7 @@ export interface MaterializeMcpTransportOptions {
44
44
  readonly logger: McpLogger;
45
45
  readonly sink: McpAsyncErrorSink;
46
46
  readonly elicitationUiProvider: McpElicitationUiProvider | undefined;
47
+ readonly elicitationEnabled?: boolean;
47
48
  /** Invoked once the stdio child has started, so the caller can capture its pid. */
48
49
  readonly onStart: () => void;
49
50
  }
@@ -1,9 +1,9 @@
1
- import { configureMcpElicitation, MCP_CLIENT_ELICITATION_CAPABILITY, } from "./elicitation.js";
1
+ import { configureMcpElicitation, mcpClientCapabilities } from "./elicitation.js";
2
2
  import { ProjectStdioTransport } from "./project-stdio-transport.js";
3
3
  import { loadMcpSdkClient, loadMcpSdkStdioTransport, loadMcpSdkStreamableHttpTransport } from "./sdk.lazy.js";
4
4
  import { safeOn } from "./wrap.js";
5
5
  export async function materializeMcpTransport(options) {
6
- const client = await buildMcpClient(options.elicitationUiProvider);
6
+ const client = await buildMcpClient(options.elicitationUiProvider, options.elicitationEnabled);
7
7
  if (options.spec.kind === "stdio")
8
8
  return materializeStdio(options.spec, options, client);
9
9
  return materializeHttp(options.spec, client);
@@ -48,13 +48,13 @@ async function materializeHttp(spec, client) {
48
48
  transport,
49
49
  };
50
50
  }
51
- async function buildMcpClient(elicitationUiProvider) {
51
+ async function buildMcpClient(elicitationUiProvider, elicitationEnabled = true) {
52
52
  // Elicitation capability is declared EMPTY on purpose (form mode only;
53
53
  // Spring-AI servers reject richer shapes) and the create-handler is wired
54
54
  // before any connect so mid-call requests never race registration.
55
55
  const { Client: McpClient } = await loadMcpSdkClient();
56
- const client = new McpClient({ name: "senpi-mcp-client", version: "0.0.0" }, { capabilities: MCP_CLIENT_ELICITATION_CAPABILITY });
57
- await configureMcpElicitation(client, elicitationUiProvider);
56
+ const client = new McpClient({ name: "senpi-mcp-client", version: "0.0.0" }, { capabilities: mcpClientCapabilities(elicitationEnabled) });
57
+ await configureMcpElicitation(client, elicitationUiProvider, undefined, elicitationEnabled);
58
58
  return client;
59
59
  }
60
60
  // OMP pattern: hand stdio OAuth servers the current access token via env.
@@ -32,6 +32,7 @@ export type CreateMcpTransportOptions = {
32
32
  readonly logger: McpLogger;
33
33
  readonly env?: Record<string, string | undefined>;
34
34
  readonly elicitationUiProvider?: McpElicitationUiProvider;
35
+ readonly elicitationEnabled?: boolean;
35
36
  readonly authProvider?: McpOAuthProvider;
36
37
  /** Re-reads a declaring server's references for this transport (senpi#3858). */
37
38
  readonly resolveConfig?: () => Promise<McpServerConfig>;
@@ -91,6 +91,7 @@ function createConnection(options, spec, connectTimeoutMs) {
91
91
  materialize: async () => {
92
92
  const materializeWith = (current) => import("./modern/dispatch.js").then(({ materializeNegotiatedTransport }) => materializeNegotiatedTransport({
93
93
  elicitationUiProvider: options.elicitationUiProvider,
94
+ elicitationEnabled: options.elicitationEnabled,
94
95
  logger: options.logger,
95
96
  onStart: captureRootPid,
96
97
  sink: asyncErrorSink,
@@ -60,6 +60,8 @@ export declare class ExternalAdmission {
60
60
  }): InputHold;
61
61
  onInputsSettled(listener: () => void): () => void;
62
62
  onEmitted(listener: (deliveryId: string) => void): () => void;
63
+ /** Durable custody will replay this delivery: reserve its id before asynchronous recovery or gateway admission. */
64
+ restoreQueued(deliveryId: string, isOwned: () => boolean): void;
63
65
  /**
64
66
  * From here on every `admit` throws `reason` and changes nothing: the runtime is being handed to
65
67
  * another writer, so the delivery stays with its sender and is delivered there instead.
@@ -82,8 +84,9 @@ export declare class ExternalAdmission {
82
84
  * deliveries may be admitted again - unless the file's last write was refused too.
83
85
  */
84
86
  observeRunSettled(): void;
85
- /** The runtime's queues were cleared: queued deliveries are no longer held. */
87
+ /** Cleared carriers are no longer held; custody not yet enqueued for recovery still owns its delivery. */
86
88
  dropQueued(): void;
89
+ private releaseRestored;
87
90
  private endHold;
88
91
  private isBusy;
89
92
  }