@code-yeongyu/senpi 2026.10.21-1 → 2026.10.21-2

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 (204) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/bundle/chunks/{anthropic-messages-S5BTP7U2.js → anthropic-messages-2IQD75YN.js} +1 -1
  3. package/dist/bundle/chunks/args-SV2KM4KA.js +2 -0
  4. package/dist/bundle/chunks/azure-openai-responses-HFI6SDH5.js +2 -0
  5. package/dist/bundle/chunks/{chunk-WQLMQGOW.js → chunk-2AJB5S4C.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-XSRM367N.js → chunk-3AK3WUN4.js} +2 -2
  7. package/dist/bundle/chunks/{chunk-6TL72TUD.js → chunk-4HJTPWZY.js} +1 -1
  8. package/dist/bundle/chunks/chunk-4NPBDGC4.js +26 -0
  9. package/dist/bundle/chunks/{chunk-E4WOCULF.js → chunk-4XR5HDWC.js} +72 -69
  10. package/dist/bundle/chunks/{chunk-6IWSMXBA.js → chunk-5GC4IQ3A.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-7I7ZMSBF.js → chunk-A4B4CMOR.js} +1 -1
  12. package/dist/bundle/chunks/chunk-B4N4N7BR.js +8 -0
  13. package/dist/bundle/chunks/{chunk-X23XXKMH.js → chunk-BVFJAXWO.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-25B6JQC4.js → chunk-C3BJRXEV.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-DKBJM22C.js → chunk-CQDU6S6C.js} +2 -2
  16. package/dist/bundle/chunks/{chunk-MF57DS3V.js → chunk-EV4TWAJI.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-JJXDWX46.js → chunk-F3JGTOLH.js} +1 -1
  18. package/dist/bundle/chunks/{chunk-CRY5CP3B.js → chunk-G42JHX5Q.js} +3 -3
  19. package/dist/bundle/chunks/{chunk-5VRI3CME.js → chunk-G7VYQRE7.js} +2 -2
  20. package/dist/bundle/chunks/chunk-GVSBCJFI.js +2 -0
  21. package/dist/bundle/chunks/{chunk-KUZPBZUK.js → chunk-HKDGQE6D.js} +2 -2
  22. package/dist/bundle/chunks/{chunk-HD67U37N.js → chunk-HRV7NEOJ.js} +1 -1
  23. package/dist/bundle/chunks/chunk-JDETIKL7.js +7 -0
  24. package/dist/bundle/chunks/{chunk-MVNVDGYF.js → chunk-JGMG227A.js} +1 -1
  25. package/dist/bundle/chunks/chunk-MHD7SSHF.js +2 -0
  26. package/dist/bundle/chunks/{chunk-CEQMTVYS.js → chunk-MXLEWTWC.js} +2 -2
  27. package/dist/bundle/chunks/{chunk-C5W3D56I.js → chunk-N6UN2SDS.js} +3 -3
  28. package/dist/bundle/chunks/chunk-NTK2MHJ5.js +2 -0
  29. package/dist/bundle/chunks/chunk-OMN4QWFA.js +3 -0
  30. package/dist/bundle/chunks/chunk-PLQ5WRXN.js +2 -0
  31. package/dist/bundle/chunks/{chunk-2MOGCBPT.js → chunk-PUMCQ5HS.js} +1 -1
  32. package/dist/bundle/chunks/{chunk-55YTTFNY.js → chunk-PUQFKAB4.js} +1 -1
  33. package/dist/bundle/chunks/{chunk-IJT3GSB2.js → chunk-SN7PTZTC.js} +1 -1
  34. package/dist/bundle/chunks/{chunk-FWHSGCWR.js → chunk-T2IIUI3J.js} +3 -3
  35. package/dist/bundle/chunks/{chunk-5AOKDC6G.js → chunk-TSETMVJP.js} +1 -1
  36. package/dist/bundle/chunks/{chunk-SR7DBNWN.js → chunk-V6AYNGMX.js} +1 -1
  37. package/dist/bundle/chunks/chunk-VDPJ7W52.js +11 -0
  38. package/dist/bundle/chunks/{chunk-EPQK674G.js → chunk-VLKULV6F.js} +1 -1
  39. package/dist/bundle/chunks/{chunk-SL7TSFR5.js → chunk-VXACHCLB.js} +1 -1
  40. package/dist/bundle/chunks/{chunk-FM7NDZR5.js → chunk-VYSRUDEU.js} +1 -1
  41. package/dist/bundle/chunks/chunk-YMFGBSO3.js +2 -0
  42. package/dist/bundle/chunks/cli-main-RA6EBOOU.js +3 -0
  43. package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-4QEQ7MGK.js → cloudflare-workers-ai-system-one-BVENEDT6.js} +1 -1
  44. package/dist/bundle/chunks/cursor-agent.js +3 -3
  45. package/dist/bundle/chunks/google-generative-ai-ONS2KSGN.js +2 -0
  46. package/dist/bundle/chunks/google-vertex-5VS7ZESH.js +2 -0
  47. package/dist/bundle/chunks/help-fast-path-FZY53UXB.js +2 -0
  48. package/dist/bundle/chunks/host-command-TNUIHEFP.js +2 -0
  49. package/dist/bundle/chunks/{host-lifecycle-EZQ5VCQV.js → host-lifecycle-LDOHAYLK.js} +1 -1
  50. package/dist/bundle/chunks/host-session-command-OKASUQPA.js +20 -0
  51. package/dist/bundle/chunks/interactive-mode-PSGTSURY.js +2 -0
  52. package/dist/bundle/chunks/{mistral-conversations-5EHLE7PZ.js → mistral-conversations-RLDMF2BI.js} +1 -1
  53. package/dist/bundle/chunks/{multi-session-host-OMJZ5VXM.js → multi-session-host-J5BVVUG3.js} +7 -6
  54. package/dist/bundle/chunks/openai-codex-responses-VRIJIZJG.js +10 -0
  55. package/dist/bundle/chunks/{openai-completions-NHDMDAS7.js → openai-completions-EZBOIXHP.js} +2 -2
  56. package/dist/bundle/chunks/{openai-images-PNXQDBSP.js → openai-images-KKM343F2.js} +1 -1
  57. package/dist/bundle/chunks/openai-responses-CMBH3DCY.js +3 -0
  58. package/dist/bundle/chunks/{package-manager-cli-7GQDB5DX.js → package-manager-cli-JHVRG35I.js} +1 -1
  59. package/dist/bundle/chunks/{role-WQWYL4ZD.js → role-LNF5MFGC.js} +1 -1
  60. package/dist/bundle/chunks/rotation-stream-JK3Q4IWI.js +2 -0
  61. package/dist/bundle/chunks/rpc-mode-HVSPPRTT.js +2 -0
  62. package/dist/bundle/chunks/{rpc-monitor-source-FI7H7LGY.js → rpc-monitor-source-2DQZLFFJ.js} +1 -1
  63. package/dist/bundle/chunks/{runtime-session-IQAKNRTV.js → runtime-session-CLNODAYE.js} +1 -1
  64. package/dist/bundle/chunks/{schedule-command-XXJC4QQN.js → schedule-command-OMI4VWUV.js} +1 -1
  65. package/dist/bundle/chunks/session-control-endpoint-UPYF4OFP.js +3 -0
  66. package/dist/bundle/chunks/session-picker-NR7CRVNQ.js +2 -0
  67. package/dist/bundle/chunks/session-worker.js +143 -139
  68. package/dist/bundle/chunks/{typesafe-system-one-W2ZTYF7X.js → typesafe-system-one-PJIWA5SP.js} +1 -1
  69. package/dist/bundle/cli.js +2 -2
  70. package/dist/bundle/index.js +1 -1
  71. package/dist/bundle/rpc-entry.js +1 -1
  72. package/dist/bundle/runtime-manifest.json +1 -1
  73. package/dist/cli/host-command.d.ts +2 -0
  74. package/dist/cli/host-command.js +14 -19
  75. package/dist/cli/host-handover-args.d.ts +4 -0
  76. package/dist/cli/host-handover-args.js +20 -0
  77. package/dist/core/agent-session.d.ts +29 -1
  78. package/dist/core/agent-session.js +74 -19
  79. package/dist/core/auth-storage.d.ts +44 -1
  80. package/dist/core/auth-storage.js +120 -3
  81. package/dist/core/client-message-identity.d.ts +3 -0
  82. package/dist/core/credential-accounts.d.ts +2 -0
  83. package/dist/core/credential-accounts.js +8 -1
  84. package/dist/core/credential-pool/rotation-slots.js +3 -2
  85. package/dist/core/extensions/builtin/account/index.js +6 -1
  86. package/dist/core/extensions/builtin/compaction/openai-remote.d.ts +2 -1
  87. package/dist/core/extensions/builtin/compaction/openai-remote.js +6 -5
  88. package/dist/core/extensions/builtin/goal/provider-retry.d.ts +2 -2
  89. package/dist/core/extensions/builtin/goal/provider-retry.js +18 -6
  90. package/dist/core/extensions/builtin/terminal/command-monitor-source.d.ts +17 -4
  91. package/dist/core/extensions/builtin/terminal/command-monitor-source.js +33 -6
  92. package/dist/core/extensions/builtin/terminal/durable-command.js +1 -0
  93. package/dist/core/extensions/builtin/terminal/extension.js +28 -0
  94. package/dist/core/extensions/builtin/terminal/monitor-notify-details.d.ts +6 -0
  95. package/dist/core/extensions/builtin/terminal/monitor-notify-details.js +19 -0
  96. package/dist/core/extensions/builtin/terminal/monitor-notify-format.d.ts +18 -0
  97. package/dist/core/extensions/builtin/terminal/monitor-notify-format.js +52 -0
  98. package/dist/core/extensions/builtin/terminal/monitor-notify.d.ts +8 -1
  99. package/dist/core/extensions/builtin/terminal/monitor-notify.js +65 -119
  100. package/dist/core/extensions/builtin/terminal/monitor-overflow-file.d.ts +7 -0
  101. package/dist/core/extensions/builtin/terminal/monitor-overflow-file.js +67 -0
  102. package/dist/core/extensions/builtin/terminal/monitor-overflow.d.ts +37 -0
  103. package/dist/core/extensions/builtin/terminal/monitor-overflow.js +75 -0
  104. package/dist/core/extensions/builtin/terminal/monitor-registry.d.ts +10 -0
  105. package/dist/core/extensions/builtin/terminal/monitor-registry.js +24 -1
  106. package/dist/core/extensions/builtin/terminal/monitor-types.d.ts +5 -0
  107. package/dist/core/extensions/builtin/terminal/monitor-wake-fit.d.ts +9 -0
  108. package/dist/core/extensions/builtin/terminal/monitor-wake-fit.js +34 -0
  109. package/dist/core/extensions/builtin/terminal/monitor-wake-pacing.d.ts +10 -0
  110. package/dist/core/extensions/builtin/terminal/monitor-wake-pacing.js +41 -0
  111. package/dist/core/extensions/builtin/terminal/restore-session.js +2 -0
  112. package/dist/core/extensions/builtin/terminal/rpc-monitor-observer.js +5 -1
  113. package/dist/core/extensions/builtin/terminal/settings.d.ts +10 -0
  114. package/dist/core/extensions/builtin/terminal/settings.js +13 -1
  115. package/dist/core/extensions/builtin/terminal/shared.d.ts +17 -10
  116. package/dist/core/extensions/builtin/terminal/shared.js +19 -10
  117. package/dist/core/extensions/builtin/terminal/terminal-manifest-model.d.ts +3 -0
  118. package/dist/core/extensions/builtin/terminal/terminal-manifest-parse.js +7 -0
  119. package/dist/core/extensions/builtin/terminal/terminal-manifest.js +1 -0
  120. package/dist/core/extensions/builtin/terminal/tools/monitor-schema.d.ts +1 -0
  121. package/dist/core/extensions/builtin/terminal/tools/monitor-schema.js +6 -1
  122. package/dist/core/extensions/builtin/terminal/tools/monitor.d.ts +1 -0
  123. package/dist/core/extensions/builtin/terminal/tools/monitor.js +9 -1
  124. package/dist/core/extensions/builtin/tps.js +1 -8
  125. package/dist/core/extensions/loader.js +3 -1
  126. package/dist/core/extensions/runner.d.ts +1 -0
  127. package/dist/core/extensions/runner.js +12 -1
  128. package/dist/core/extensions/types.d.ts +3 -1
  129. package/dist/core/session-manager.d.ts +10 -1
  130. package/dist/core/session-manager.js +4 -3
  131. package/dist/core/terminal-settings.d.ts +3 -0
  132. package/dist/index.d.ts +1 -1
  133. package/dist/main.d.ts +14 -0
  134. package/dist/main.js +25 -8
  135. package/dist/modes/index.d.ts +1 -0
  136. package/dist/modes/index.js +1 -0
  137. package/dist/modes/interactive/components/footer.js +4 -2
  138. package/dist/modes/interactive/interactive-mode.d.ts +5 -0
  139. package/dist/modes/interactive/interactive-mode.js +13 -1
  140. package/dist/modes/rpc/client-admission-record.d.ts +5 -0
  141. package/dist/modes/rpc/client-admission-record.js +2 -0
  142. package/dist/modes/rpc/connection-handler.d.ts +3 -0
  143. package/dist/modes/rpc/connection-handler.js +33 -5
  144. package/dist/modes/rpc/connection-roles.d.ts +30 -0
  145. package/dist/modes/rpc/connection-roles.js +53 -0
  146. package/dist/modes/rpc/custom-capability.d.ts +6 -0
  147. package/dist/modes/rpc/custom-capability.js +6 -0
  148. package/dist/modes/rpc/host-capabilities.js +2 -1
  149. package/dist/modes/rpc/host-decision.d.ts +4 -1
  150. package/dist/modes/rpc/host-decision.js +6 -3
  151. package/dist/modes/rpc/host-ensure-liveness.d.ts +11 -0
  152. package/dist/modes/rpc/host-ensure-liveness.js +26 -0
  153. package/dist/modes/rpc/host-ensure.js +2 -3
  154. package/dist/modes/rpc/host-holder.d.ts +27 -0
  155. package/dist/modes/rpc/host-holder.js +18 -0
  156. package/dist/modes/rpc/host-runner.d.ts +2 -0
  157. package/dist/modes/rpc/host-runner.js +8 -1
  158. package/dist/modes/rpc/host-status-all.js +1 -0
  159. package/dist/modes/rpc/host-status.d.ts +1 -0
  160. package/dist/modes/rpc/host-status.js +5 -0
  161. package/dist/modes/rpc/host-stop-generation.d.ts +22 -0
  162. package/dist/modes/rpc/host-stop-generation.js +36 -0
  163. package/dist/modes/rpc/host-stop-start-time.d.ts +15 -0
  164. package/dist/modes/rpc/host-stop-start-time.js +30 -0
  165. package/dist/modes/rpc/host-stop.d.ts +13 -1
  166. package/dist/modes/rpc/host-stop.js +78 -9
  167. package/dist/modes/rpc/rpc-client.d.ts +9 -0
  168. package/dist/modes/rpc/rpc-client.js +15 -0
  169. package/dist/modes/rpc/rpc-types.d.ts +18 -0
  170. package/dist/modes/rpc/rpc-types.js +2 -0
  171. package/dist/modes/rpc/session-binding.d.ts +1 -1
  172. package/dist/modes/rpc/session-command-router.d.ts +1 -0
  173. package/dist/modes/rpc/session-command-router.js +13 -0
  174. package/dist/modes/rpc/session-worker-client.d.ts +1 -1
  175. package/dist/modes/rpc/session-worker-client.js +12 -1
  176. package/dist/modes/rpc/session-worker-protocol.d.ts +3 -0
  177. package/dist/modes/rpc/session-worker.js +9 -0
  178. package/docs/rpc.md +54 -2
  179. package/docs/session-format.md +5 -0
  180. package/package.json +6 -6
  181. package/dist/bundle/chunks/args-CRXNC7IS.js +0 -2
  182. package/dist/bundle/chunks/azure-openai-responses-2DCY6QKO.js +0 -2
  183. package/dist/bundle/chunks/chunk-5ZBDN3LB.js +0 -26
  184. package/dist/bundle/chunks/chunk-AEJUOQ7U.js +0 -2
  185. package/dist/bundle/chunks/chunk-LAWS3FYQ.js +0 -11
  186. package/dist/bundle/chunks/chunk-LM76EMEJ.js +0 -7
  187. package/dist/bundle/chunks/chunk-M237EDHB.js +0 -2
  188. package/dist/bundle/chunks/chunk-V6RY5A6T.js +0 -2
  189. package/dist/bundle/chunks/chunk-WIAXG52U.js +0 -2
  190. package/dist/bundle/chunks/chunk-WTFD4TII.js +0 -8
  191. package/dist/bundle/chunks/chunk-Z33PKFXM.js +0 -3
  192. package/dist/bundle/chunks/cli-main-RKC5Z25H.js +0 -3
  193. package/dist/bundle/chunks/google-generative-ai-3SKUSCIL.js +0 -2
  194. package/dist/bundle/chunks/google-vertex-YHSIIE4G.js +0 -2
  195. package/dist/bundle/chunks/help-fast-path-FWS5ARO5.js +0 -2
  196. package/dist/bundle/chunks/host-command-L66Y7ZL5.js +0 -2
  197. package/dist/bundle/chunks/host-session-command-2YNQNTEH.js +0 -20
  198. package/dist/bundle/chunks/interactive-mode-XOKTNBLA.js +0 -2
  199. package/dist/bundle/chunks/openai-codex-responses-4BIPACKV.js +0 -10
  200. package/dist/bundle/chunks/openai-responses-2UOPEJLP.js +0 -3
  201. package/dist/bundle/chunks/rotation-stream-J2NUPRMG.js +0 -2
  202. package/dist/bundle/chunks/rpc-mode-WM5WH7TH.js +0 -2
  203. package/dist/bundle/chunks/session-control-endpoint-MTOAYRRY.js +0 -3
  204. package/dist/bundle/chunks/session-picker-LU7BPLNI.js +0 -2
@@ -306,6 +306,16 @@ export function providerRetryWatchdogAbortMessage(retryTimeoutMs, streamStartTim
306
306
  ? " (Its stream-start guard is off; to bound or extend this wait, set retry.provider.streamStartTimeoutMs, 0 disables.)"
307
307
  : ` (Stream-start guard: ${Math.round(streamStartTimeoutMs / 1000)}s; to allow longer, raise retry.provider.streamStartTimeoutMs, 0 disables.)`));
308
308
  }
309
+ function isHumanInput(options) {
310
+ return inputOriginOf(options?.source) === "person";
311
+ }
312
+ function promptKind(options) {
313
+ if (options?.streamingBehavior === "steer")
314
+ return "steer";
315
+ if (options?.streamingBehavior === "followUp")
316
+ return "follow_up";
317
+ return "prompt";
318
+ }
309
319
  export class AgentSession {
310
320
  /**
311
321
  * Resolve the effective retry profile for the current model's provider.
@@ -331,7 +341,6 @@ export class AgentSession {
331
341
  this._messageEndPersistence = new WeakMap();
332
342
  this._isAgentRunActive = false;
333
343
  this._promptsInFlight = 0;
334
- this._personAnswerPending = false;
335
344
  /** senpi#3650: the core may continue a run after agent_end; the settle must not wait on that continuation. */
336
345
  this._runContinuations = new AgentRunContinuations();
337
346
  /** senpi#3650: the turn that owns extension handler waits started now; the turn deadline releases only its own. */
@@ -347,6 +356,7 @@ export class AgentSession {
347
356
  this._admissionIdleWaiters = [];
348
357
  this._engineTurnStarts = [];
349
358
  this._engineTurnRelease = new EngineTurnRelease();
359
+ this._humanInputListeners = new Set();
350
360
  this._engineTurnPauseRecorded = false;
351
361
  // The pause warning is shown once per pause: a stream of refused events would otherwise add one line each (senpi#2537).
352
362
  this._engineTurnPauseNotified = false;
@@ -365,6 +375,11 @@ export class AgentSession {
365
375
  this._queueClearRecorded = Promise.resolve();
366
376
  /** The exact message object queued for each `queuedId`, so removal and edits hit the native queue by identity. */
367
377
  this._queuedMessages = new Map();
378
+ /**
379
+ * Input source of each user message this session admitted, written onto its session entry when the
380
+ * message is persisted. Kept off the message itself so it can never reach a provider request.
381
+ */
382
+ this._userInputSources = new WeakMap();
368
383
  /** Recently delivered `queuedId`s (bounded), so a late dequeue answers `already_delivered`, not `not_found`. */
369
384
  this._deliveredQueuedIds = new Set();
370
385
  this._supersededCompactionLogAttemptIds = new Set();
@@ -1966,8 +1981,9 @@ export class AgentSession {
1966
1981
  "Agent is already processing a prompt. Use steer() or followUp() to queue messages, or wait for completion.") {
1967
1982
  const queuedMessages = Array.isArray(messages) ? messages : [messages];
1968
1983
  const userMessage = queuedMessages.find((message) => message.role === "user");
1984
+ const source = userMessage === undefined ? undefined : this._userInputSources.get(userMessage);
1969
1985
  // The caller was already told this input started; it now waits in the queue, so persist
1970
- // it as queued input before it is enqueued, exactly like the regular queue path.
1986
+ // it as queued input before it is enqueued, exactly like the regular queue path (source included).
1971
1987
  const prepared = userMessage?.role === "user"
1972
1988
  ? {
1973
1989
  ...readClientMessageIdentity(userMessage),
@@ -1977,6 +1993,7 @@ export class AgentSession {
1977
1993
  : userMessage.content.filter((part) => part.type === "image"),
1978
1994
  mode: "steer",
1979
1995
  enqueueOrder: this.reserveQueuedInputOrder(),
1996
+ ...(source === undefined ? {} : { source }),
1980
1997
  }
1981
1998
  : undefined;
1982
1999
  if (prepared)
@@ -2339,7 +2356,9 @@ export class AgentSession {
2339
2356
  event.message.role === "assistant" ||
2340
2357
  event.message.role === "toolResult") {
2341
2358
  // Regular LLM message - persist as SessionMessageEntry
2342
- entryId = this.sessionManager.appendOwnedMessage(event.message);
2359
+ entryId = this.sessionManager.appendOwnedMessage(event.message, {
2360
+ inputSource: this._userInputSources.get(event.message),
2361
+ });
2343
2362
  this._emitEntryAppended(entryId);
2344
2363
  this._incrementMessageRevision();
2345
2364
  this.externalAdmission.observePersisted(event.message);
@@ -3767,6 +3786,8 @@ export class AgentSession {
3767
3786
  * @throws Error if no model selected or no API key available (when not streaming)
3768
3787
  */
3769
3788
  async prompt(text, options) {
3789
+ if (isHumanInput(options) && options?.humanInputReported !== true)
3790
+ this._humanInputAdmitted(promptKind(options));
3770
3791
  // Held synchronously, before any await: an external delivery never overtakes this input.
3771
3792
  const hold = this.externalAdmission.beginInput({
3772
3793
  command: this._isExtensionCommandText(text, options),
@@ -4229,12 +4250,14 @@ export class AgentSession {
4229
4250
  if (currentImages) {
4230
4251
  userContent.push(...currentImages);
4231
4252
  }
4232
- messages.push({
4253
+ const userMessage = {
4233
4254
  role: "user",
4234
4255
  content: userContent,
4235
4256
  timestamp: Date.now(),
4236
4257
  ...clientMessageIdentity(options),
4237
- });
4258
+ };
4259
+ this._userInputSources.set(userMessage, options?.source ?? "interactive");
4260
+ messages.push(userMessage);
4238
4261
  // Consume next-turn messages transactionally: a final admission rejection
4239
4262
  // restores them in their original order rather than dropping or duplicating
4240
4263
  // one-shot extension state.
@@ -4469,7 +4492,7 @@ export class AgentSession {
4469
4492
  async _queueUserInput(text, images, behavior, options) {
4470
4493
  const origin = inputOriginOf(options?.source);
4471
4494
  if (origin === "person" && this._engineTurnHold.held) {
4472
- await this.prompt(text, { ...options, images, streamingBehavior: behavior });
4495
+ await this.prompt(text, { ...options, images, streamingBehavior: behavior, humanInputReported: true });
4473
4496
  return "queued";
4474
4497
  }
4475
4498
  if (origin === "person")
@@ -4516,6 +4539,8 @@ export class AgentSession {
4516
4539
  * @throws Error if text is an extension command
4517
4540
  */
4518
4541
  async steer(text, images, options) {
4542
+ if (isHumanInput(options))
4543
+ this._humanInputAdmitted("steer");
4519
4544
  return this._queueUserInput(text, images, "steer", options);
4520
4545
  }
4521
4546
  /**
@@ -4528,6 +4553,8 @@ export class AgentSession {
4528
4553
  * @throws Error if text is an extension command
4529
4554
  */
4530
4555
  async followUp(text, images, options) {
4556
+ if (isHumanInput(options))
4557
+ this._humanInputAdmitted("follow_up");
4531
4558
  return this._queueUserInput(text, images, "followUp", options);
4532
4559
  }
4533
4560
  _startSessionTitleGeneration(firstPrompt) {
@@ -4614,11 +4641,12 @@ export class AgentSession {
4614
4641
  /** Identified input is admitted into durable custody and enqueued in one gated step; other input as before. */
4615
4642
  async _enqueueQueuedInput(input) {
4616
4643
  const clientMessageId = input.clientMessageId;
4644
+ const sourced = { ...input, source: input.source ?? "interactive" };
4617
4645
  if (clientMessageId === undefined) {
4618
- this._enqueuePreparedInput(input);
4646
+ this._enqueuePreparedInput(sourced);
4619
4647
  return;
4620
4648
  }
4621
- await this._pendingInputs.admit({ clientMessageId, text: input.text, ...(input.images ? { images: input.images } : {}), mode: input.mode }, (queuedId) => this._enqueuePreparedInput({ ...input, queuedId }));
4649
+ await this._pendingInputs.admit({ clientMessageId, text: input.text, ...(input.images ? { images: input.images } : {}), mode: input.mode }, (queuedId) => this._enqueuePreparedInput({ ...sourced, queuedId }));
4622
4650
  }
4623
4651
  /** Restore accepted input without re-running input transforms or changing its enqueue order. */
4624
4652
  async restoreQueuedInput(input) {
@@ -4638,6 +4666,7 @@ export class AgentSession {
4638
4666
  images: input.images,
4639
4667
  mode: input.mode,
4640
4668
  enqueueOrder,
4669
+ ...(input.source === undefined ? {} : { source: input.source }),
4641
4670
  };
4642
4671
  input.onQueuedInput?.(prepared);
4643
4672
  const queue = input.mode === "steer" ? this._steeringMessages : this._followUpMessages;
@@ -4657,6 +4686,11 @@ export class AgentSession {
4657
4686
  timestamp: Date.now(),
4658
4687
  ...clientMessageIdentity(input),
4659
4688
  };
4689
+ // New input always arrives with a source: `_enqueueQueuedInput` applies the same "interactive" default
4690
+ // `prompt()` does. Only restored input can lack one (a record written before sources were kept), and it
4691
+ // stays unstamped: unknown origin, never defaulted to a trusted source.
4692
+ if (input.source !== undefined)
4693
+ this._userInputSources.set(message, input.source);
4660
4694
  this._queuedMessages.set(queuedId, message);
4661
4695
  if (this._promptStartPending && this._skipNextPostCompactionAssistantCheck) {
4662
4696
  const deferred = input.mode === "steer"
@@ -4843,8 +4877,33 @@ export class AgentSession {
4843
4877
  finishSessionWork?.();
4844
4878
  }
4845
4879
  }
4880
+ /**
4881
+ * Subscribe to human input as it enters the session: called synchronously for every input whose origin is a
4882
+ * person (TUI input, RPC prompt/steer/follow_up, human question answers, `resumeSession`). Origin comes from
4883
+ * the entry point, never from the connection's `interactive_user` role, which only decides authority. Never
4884
+ * called for automatic deliveries (extension messages, wakes, continuations). A throwing listener does not block
4885
+ * the input. Timing: a call means a person sent it, not that it took effect: `prompt` reports before checks that
4886
+ * can still throw, `resumeSession` reports even when nothing is paused, and TUI messages queued during
4887
+ * compaction report when they are delivered.
4888
+ */
4889
+ onHumanInputAdmitted(listener) {
4890
+ this._humanInputListeners.add(listener);
4891
+ return () => this._humanInputListeners.delete(listener);
4892
+ }
4893
+ _humanInputAdmitted(kind) {
4894
+ const event = { kind, at: Date.now() };
4895
+ for (const listener of this._humanInputListeners) {
4896
+ try {
4897
+ listener(event);
4898
+ }
4899
+ catch (error) {
4900
+ this._sessionLogger.warn("human_input_listener_failed", { error: String(error) });
4901
+ }
4902
+ }
4903
+ }
4846
4904
  /** Explicit user Resume; attachment alone must never call this. */
4847
4905
  resumeSession() {
4906
+ this._humanInputAdmitted("resume");
4848
4907
  return this._engineTurnRelease.resume(this._engineTurnHold, async (message, admit) => {
4849
4908
  const generation = this._userAbortGeneration;
4850
4909
  await this.waitForIdle();
@@ -5087,10 +5146,10 @@ export class AgentSession {
5087
5146
  // before prompt() accepted the message must not silently drop it.
5088
5147
  if (disposition === undefined) {
5089
5148
  if (options?.deliverAs === "steer") {
5090
- await this._queueSteer(text, images, { textBlocks });
5149
+ await this._queueSteer(text, images, { textBlocks, source: "extension" });
5091
5150
  }
5092
5151
  else {
5093
- await this._queueFollowUp(text, images, { textBlocks });
5152
+ await this._queueFollowUp(text, images, { textBlocks, source: "extension" });
5094
5153
  }
5095
5154
  }
5096
5155
  throw error;
@@ -5546,6 +5605,7 @@ export class AgentSession {
5546
5605
  modelSelectSource: "set",
5547
5606
  invalidateCompaction: true,
5548
5607
  allowDeferral: false,
5608
+ admittedLiveContextTokens: projection.liveContextTokens,
5549
5609
  origin: heldSwitchOrigin(pending.origin),
5550
5610
  });
5551
5611
  }
@@ -5816,9 +5876,8 @@ export class AgentSession {
5816
5876
  this._admitSwitchCompactionRequired(model, deferral, opts.persistDefault, opts.origin);
5817
5877
  return undefined;
5818
5878
  }
5819
- const admittedLiveContextTokens = opts.repairWithSlice
5820
- ? this._reduceForSwitchTarget(model, liveContextTokens)
5821
- : liveContextTokens;
5879
+ const admittedLiveContextTokens = opts.admittedLiveContextTokens ??
5880
+ (opts.repairWithSlice ? this._reduceForSwitchTarget(model, liveContextTokens) : liveContextTokens);
5822
5881
  this._assertModelUsableForSwitch(model, admittedLiveContextTokens);
5823
5882
  if (opts.appendSessionEntry) {
5824
5883
  this.sessionManager.appendModelChange(model.provider, model.id, opts.entryReason, previousModel?.provider, previousModel?.id, { origin: opts.origin, duringTurn });
@@ -7994,7 +8053,7 @@ export class AgentSession {
7994
8053
  return;
7995
8054
  send();
7996
8055
  },
7997
- sendUserMessage: (content, options) => {
8056
+ sendUserMessage: (content, options, inputOrigin = "extension") => {
7998
8057
  const reportError = (err) => {
7999
8058
  if (err instanceof ModelNotReadyError) {
8000
8059
  this._noticeProviderRequired(err.message);
@@ -8006,9 +8065,6 @@ export class AgentSession {
8006
8065
  error: err instanceof Error ? err.message : String(err),
8007
8066
  });
8008
8067
  };
8009
- // A person's answer is classified when it is delivered, not by the phase it later runs in.
8010
- const inputOrigin = this._personAnswerPending ? "person" : "extension";
8011
- this._personAnswerPending = false;
8012
8068
  // sendUserMessage always triggers a turn; register a settlement-deferred
8013
8069
  // turn claim so agent_idle is not emitted before its deferred agent_start.
8014
8070
  if (this._agentSettledDelivery.deferTriggerTurn((claim) => {
@@ -8078,12 +8134,11 @@ export class AgentSession {
8078
8134
  engineTurnsHeld: () => this._engineTurnHold.holdsSource("goal"),
8079
8135
  noteHeldWake: (source) => this._engineTurnHold.note(source),
8080
8136
  questionInput: () => {
8137
+ this._humanInputAdmitted("answer");
8081
8138
  this._engineTurnInput.begin();
8082
8139
  this._engineResumeWindow = false;
8083
- this._personAnswerPending = true;
8084
8140
  // The answer's follow-on (if any) enters prompt() synchronously after this; settle once it has.
8085
8141
  queueMicrotask(() => {
8086
- this._personAnswerPending = false;
8087
8142
  this._settleInputRelease()?.catch((error) => {
8088
8143
  this._sessionLogger.warn("input_release_settle_failed", {
8089
8144
  error: error instanceof Error ? error.message : String(error),
@@ -3,8 +3,21 @@
3
3
  * Provider auth orchestration belongs to ModelRuntime and pi-ai Models.
4
4
  */
5
5
  import type { ApiKeyCredential, AuthOperationOptions, Credential, CredentialInfo, CredentialStore, OAuthAuth, OAuthCredential, OAuthLoginCallbacks } from "@earendil-works/pi-ai";
6
+ import { type CredentialKind } from "@earendil-works/pi-ai/auth/pool/kinds";
6
7
  import { type CredentialSlot } from "@earendil-works/pi-ai/auth/pool/slots";
7
8
  type AuthStorageData = Record<string, Credential>;
9
+ /** A credential problem the user should see once per change: `id` changes when the problem does. */
10
+ export type AuthNotice = {
11
+ readonly id: string;
12
+ readonly provider: string;
13
+ readonly message: string;
14
+ };
15
+ export type CredentialReplacement = {
16
+ readonly replaced: CredentialKind;
17
+ readonly backupPath?: string;
18
+ };
19
+ /** What a login that replaced a credential of the other kind tells the user (senpi#3448, #3486). */
20
+ export declare function describeCredentialReplacement(provider: string, replacement: CredentialReplacement): string;
8
21
  export type AuthCredential = Credential;
9
22
  export type { ApiKeyCredential, OAuthCredential };
10
23
  export type AuthStatus = {
@@ -75,6 +88,10 @@ export declare class AuthStorage implements CredentialStore {
75
88
  private readonly extensionOAuthProviders;
76
89
  private errors;
77
90
  private readonly reportedMigrationSkips;
91
+ private notices;
92
+ private readonly seenInMemory;
93
+ private readonly takenNotices;
94
+ private readonly replacements;
78
95
  private storage;
79
96
  private authPath;
80
97
  private readState;
@@ -127,7 +144,33 @@ export declare class AuthStorage implements CredentialStore {
127
144
  removeRuntimeApiKey(provider: string): void;
128
145
  get(provider: string): Credential | undefined;
129
146
  getProviderEnv(provider: string): Record<string, string> | undefined;
130
- set(provider: string, credential: Credential): void;
147
+ /**
148
+ * Stores a login. A login of the other kind replaces the provider's credential (senpi#3448); the
149
+ * previous bytes are backed up first, and the replacement is returned and kept for
150
+ * `takeReplacement`, so no credential disappears without the user being told where it went.
151
+ */
152
+ set(provider: string, credential: Credential): CredentialReplacement | undefined;
153
+ /** The last replacement a login made for this provider, once; the caller shows it to the user. */
154
+ takeReplacement(provider: string): CredentialReplacement | undefined;
155
+ /** Credential notices not yet marked seen; a changed problem gets a new id and shows again. */
156
+ pendingNotices(): ReadonlyArray<AuthNotice>;
157
+ /**
158
+ * Hands out each pending notice once per process, so a run that reports auth problems at several
159
+ * points shows it once (senpi#3486). With `persistSeen`, the notices are also recorded as seen, so
160
+ * the next launch stays quiet until the problem changes; a run nobody reads (RPC) leaves them pending.
161
+ */
162
+ takePendingNotices(options: {
163
+ persistSeen: boolean;
164
+ }): ReadonlyArray<AuthNotice>;
165
+ /**
166
+ * Records notices as shown, so the next launch stays quiet until the problem changes. Ids of
167
+ * problems that no longer exist are dropped. A failed write never aborts startup: it is reported,
168
+ * and the notice may be shown once more.
169
+ */
170
+ markNoticesSeen(ids: readonly string[]): void;
171
+ private seenNoticesPath;
172
+ private seenNoticeIds;
173
+ private noteReplacement;
131
174
  remove(provider: string): void;
132
175
  listSlots(provider: string): CredentialSlot[];
133
176
  setSlot(provider: string, slot: CredentialSlot): void;
@@ -3,6 +3,7 @@
3
3
  * Provider auth orchestration belongs to ModelRuntime and pi-ai Models.
4
4
  */
5
5
  import { findEnvKeys, getEnvApiKey, readByProviderId } from "@earendil-works/pi-ai";
6
+ import { slotKind, unusedSlotKinds } from "@earendil-works/pi-ai/auth/pool/kinds";
6
7
  import { appendLoginSlot, listSlots, removeSlot, repairManagedSentinelSlots, upsertSlot, } from "@earendil-works/pi-ai/auth/pool/slots";
7
8
  import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
8
9
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
@@ -37,6 +38,36 @@ function repairPoisonedPoolSlots(data) {
37
38
  }
38
39
  return repaired ? { data: repaired, repaired: true } : { data, repaired: false };
39
40
  }
41
+ const KIND_LABEL = { oauth: "OAuth login", api_key: "API key" };
42
+ /**
43
+ * One notice per provider whose stored credential holds slots its type cannot use (senpi#3448).
44
+ * Nothing is changed: which credential pays stays the user's choice, so the notice names the
45
+ * unused login, where it still is, and the action that switches to it.
46
+ */
47
+ function unusedLoginNotices(data) {
48
+ const notices = [];
49
+ for (const [provider, credential] of Object.entries(data)) {
50
+ if (typeof credential !== "object" || credential === null)
51
+ continue;
52
+ for (const kind of unusedSlotKinds(credential)) {
53
+ const slots = listSlots(credential).filter((slot) => slotKind(slot) === kind);
54
+ const inUse = KIND_LABEL[credential.type];
55
+ const names = slots.map((slot) => slot.name);
56
+ notices.push({
57
+ // Slot names, never key material: a new unused login gets a new id and is shown again.
58
+ id: `${provider}:${kind}:${names.join(",")}`,
59
+ provider,
60
+ message: `${provider}: an ${KIND_LABEL[kind]} saved earlier is stored next to your ${inUse} but is never used; the ${inUse} stays in use. It is still in auth.json (${slots.map((slot) => slot.name).join(", ")}); to switch to it, save it again with /login (the ${inUse} is backed up first).`,
61
+ });
62
+ }
63
+ }
64
+ return notices;
65
+ }
66
+ /** What a login that replaced a credential of the other kind tells the user (senpi#3448, #3486). */
67
+ export function describeCredentialReplacement(provider, replacement) {
68
+ const where = replacement.backupPath ? ` It is saved in ${replacement.backupPath}` : " It was not backed up";
69
+ return `Your ${KIND_LABEL[replacement.replaced]} for ${provider} was replaced by this login.${where}; to switch back, log in with it again.`;
70
+ }
40
71
  // Every write stages a fresh 0o600 file and renames it over the store, so the
41
72
  // credential file is never briefly world-readable and never half-written; the
42
73
  // restrictive mode also wins over any wider mode an earlier direct write left.
@@ -317,6 +348,10 @@ export class AuthStorage {
317
348
  this.extensionOAuthProviders = new Map();
318
349
  this.errors = [];
319
350
  this.reportedMigrationSkips = new Set();
351
+ this.notices = [];
352
+ this.seenInMemory = new Set();
353
+ this.takenNotices = new Set();
354
+ this.replacements = new Map();
320
355
  this.busyReads = 0;
321
356
  this.dataLoaded = false;
322
357
  this.dataFromBusyRead = false;
@@ -381,6 +416,7 @@ export class AuthStorage {
381
416
  this.reportedMigrationSkips.add(moved.skipped);
382
417
  this.recordError(new Error(`The Anthropic Claude subscription login in auth.json was not moved to Anthropic Subscription because ${moved.skipped}. It was left unchanged; sign in again with /login and choose "Anthropic Subscription (Claude Pro/Max)".`));
383
418
  }
419
+ this.notices = unusedLoginNotices(moved.data);
384
420
  return {
385
421
  data: moved.data,
386
422
  repaired: repaired.repaired,
@@ -398,13 +434,14 @@ export class AuthStorage {
398
434
  */
399
435
  backupAuthFile(content) {
400
436
  if (!this.authPath || content === undefined)
401
- return;
437
+ return undefined;
402
438
  const stamp = new Date().toISOString().replace(/[:.]/g, "-");
403
439
  let backupPath = `${this.authPath}.backup-${stamp}`;
404
440
  for (let attempt = 1; existsSync(backupPath); attempt++) {
405
441
  backupPath = `${this.authPath}.backup-${stamp}-${attempt}`;
406
442
  }
407
443
  writeFileSync(backupPath, content, AUTH_FILE_WRITE_OPTIONS);
444
+ return backupPath;
408
445
  }
409
446
  updateReadState(data, revision) {
410
447
  this.data = data;
@@ -484,15 +521,94 @@ export class AuthStorage {
484
521
  const credential = readByProviderId(this.data, provider);
485
522
  return credential?.type === "api_key" && credential.env ? { ...credential.env } : undefined;
486
523
  }
524
+ /**
525
+ * Stores a login. A login of the other kind replaces the provider's credential (senpi#3448); the
526
+ * previous bytes are backed up first, and the replacement is returned and kept for
527
+ * `takeReplacement`, so no credential disappears without the user being told where it went.
528
+ */
487
529
  set(provider, credential) {
488
- this.storage.withLock((content) => {
530
+ return this.storage.withLock((content) => {
489
531
  const currentData = this.parseStorageData(content);
532
+ const replacement = this.noteReplacement(provider, currentData[provider], credential, content);
490
533
  const next = appendLoginSlot(currentData[provider], credential);
491
534
  const nextData = { ...currentData, [provider]: next };
492
535
  this.data = nextData;
493
- return { result: undefined, next: JSON.stringify(nextData, null, 2) };
536
+ return { result: replacement, next: JSON.stringify(nextData, null, 2) };
494
537
  });
495
538
  }
539
+ /** The last replacement a login made for this provider, once; the caller shows it to the user. */
540
+ takeReplacement(provider) {
541
+ const replacement = this.replacements.get(provider);
542
+ this.replacements.delete(provider);
543
+ return replacement;
544
+ }
545
+ /** Credential notices not yet marked seen; a changed problem gets a new id and shows again. */
546
+ pendingNotices() {
547
+ const seen = this.seenNoticeIds();
548
+ return this.notices.filter((notice) => !seen.has(notice.id));
549
+ }
550
+ /**
551
+ * Hands out each pending notice once per process, so a run that reports auth problems at several
552
+ * points shows it once (senpi#3486). With `persistSeen`, the notices are also recorded as seen, so
553
+ * the next launch stays quiet until the problem changes; a run nobody reads (RPC) leaves them pending.
554
+ */
555
+ takePendingNotices(options) {
556
+ const taken = this.pendingNotices().filter((notice) => !this.takenNotices.has(notice.id));
557
+ for (const notice of taken)
558
+ this.takenNotices.add(notice.id);
559
+ if (options.persistSeen)
560
+ this.markNoticesSeen(taken.map((notice) => notice.id));
561
+ return taken;
562
+ }
563
+ /**
564
+ * Records notices as shown, so the next launch stays quiet until the problem changes. Ids of
565
+ * problems that no longer exist are dropped. A failed write never aborts startup: it is reported,
566
+ * and the notice may be shown once more.
567
+ */
568
+ markNoticesSeen(ids) {
569
+ if (ids.length === 0)
570
+ return;
571
+ const path = this.seenNoticesPath();
572
+ if (!path) {
573
+ for (const id of ids)
574
+ this.seenInMemory.add(id);
575
+ return;
576
+ }
577
+ const current = new Set(this.notices.map((notice) => notice.id));
578
+ const seen = [...new Set([...this.seenNoticeIds(), ...ids])].filter((id) => current.has(id));
579
+ try {
580
+ writeFileSync(path, JSON.stringify(seen, null, 2), AUTH_FILE_WRITE_OPTIONS);
581
+ }
582
+ catch (error) {
583
+ const reason = error instanceof Error ? error.message : String(error);
584
+ this.recordError(new Error(`Could not record which credential notices were shown (${path}): ${reason}. They may be shown again on the next start; check that the file is writable.`));
585
+ }
586
+ }
587
+ seenNoticesPath() {
588
+ return this.authPath ? join(dirname(this.authPath), "auth-notices-seen.json") : undefined;
589
+ }
590
+ seenNoticeIds() {
591
+ const path = this.seenNoticesPath();
592
+ if (!path)
593
+ return new Set(this.seenInMemory);
594
+ try {
595
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
596
+ return new Set(Array.isArray(parsed) ? parsed.filter((id) => typeof id === "string") : []);
597
+ }
598
+ catch {
599
+ return new Set();
600
+ }
601
+ }
602
+ noteReplacement(provider, previous, next, content) {
603
+ if (previous === undefined || previous.type === next.type)
604
+ return undefined;
605
+ const backupPath = this.backupAuthFile(content);
606
+ const replacement = backupPath
607
+ ? { replaced: previous.type, backupPath }
608
+ : { replaced: previous.type };
609
+ this.replacements.set(provider, replacement);
610
+ return replacement;
611
+ }
496
612
  remove(provider) {
497
613
  this.storage.withLock((content) => {
498
614
  const nextData = { ...this.parseStorageData(content) };
@@ -640,6 +756,7 @@ export class AuthStorage {
640
756
  revision = this.authPath ? getFileContentRevision(this.authPath) : undefined;
641
757
  return { result: currentData[provider] };
642
758
  }
759
+ this.noteReplacement(provider, currentData[provider], next, content);
643
760
  const merged = { ...currentData, [provider]: next };
644
761
  latestData = merged;
645
762
  return { result: next, next: JSON.stringify(merged, null, 2) };
@@ -1,5 +1,6 @@
1
1
  import type { ImageContent } from "@earendil-works/pi-ai";
2
2
  import { z } from "zod";
3
+ import type { InputSource } from "./extensions/types.ts";
3
4
  export declare const clientMessageIdentitySchema: z.ZodObject<{
4
5
  clientMessageId: z.ZodOptional<z.ZodString>;
5
6
  clientTurnId: z.ZodOptional<z.ZodString>;
@@ -10,6 +11,8 @@ export type PreparedClientInput = ClientMessageIdentity & {
10
11
  readonly images?: ImageContent[];
11
12
  readonly mode: "steer" | "followUp";
12
13
  readonly enqueueOrder: number;
14
+ /** The input's `input` event source, kept so restored input is re-stamped with it. Absent: unknown origin. */
15
+ readonly source?: InputSource;
13
16
  };
14
17
  /** Copy only correlation metadata, never the rest of a prompt's options. */
15
18
  export declare function clientMessageIdentity(value?: ClientMessageIdentity): ClientMessageIdentity;
@@ -14,6 +14,8 @@ export type CredentialAccountSummary = {
14
14
  readonly model: string;
15
15
  readonly until: number;
16
16
  }[];
17
+ /** A slot of the other kind than the stored credential: kept, never used, cannot be pinned (senpi#3486). */
18
+ readonly unused?: true;
17
19
  };
18
20
  export declare function assertValidAccountName(name: string): void;
19
21
  export declare function credentialPoolRepository(storage: AuthStorage): CredentialSlotRepository;
@@ -1,5 +1,6 @@
1
1
  import { dirname, join } from "node:path";
2
2
  import { normalizeProviderId } from "@earendil-works/pi-ai";
3
+ import { slotsOfOwnKind } from "@earendil-works/pi-ai/auth/pool/kinds";
3
4
  import { applyAccountOrder, withoutOrderedAccount } from "@earendil-works/pi-ai/auth/pool/select";
4
5
  import { accountDisplayName, listSlots, pinSlot, removeSlot, renameSlotDisplayName, } from "@earendil-works/pi-ai/auth/pool/slots";
5
6
  import { discoverEnvSlots } from "./credential-pool/env-slots.js";
@@ -94,6 +95,7 @@ async function summarizeInStoredOrder(provider, credential, env, repository) {
94
95
  ? listSlots(credential)
95
96
  : []
96
97
  : listSlots(credential);
98
+ const usable = new Set(slotsOfOwnKind(credential).map((slot) => slot.name));
97
99
  for (const slot of storedAccounts) {
98
100
  const displayName = accountDisplayName(slot.displayName);
99
101
  const persisted = state[slot.name];
@@ -111,6 +113,7 @@ async function summarizeInStoredOrder(provider, credential, env, repository) {
111
113
  blocked: slotBlocked(slot, applicable, now),
112
114
  pinned: pinned === slot.name,
113
115
  ...blockedModels(now, applicable?.modelBlocks, storedModelBlocks(slot)),
116
+ ...(usable.has(slot.name) ? {} : { unused: true }),
114
117
  });
115
118
  }
116
119
  if (normalizeProviderId(provider) !== "anthropic-subscription")
@@ -155,9 +158,13 @@ export async function pinCredentialAccount(storage, provider, name, env = proces
155
158
  if (name !== null) {
156
159
  assertValidAccountName(name);
157
160
  const accounts = await getCredentialAccounts(storage, provider, env, repo);
158
- if (!accounts.some((account) => account.name === name)) {
161
+ const account = accounts.find((candidate) => candidate.name === name);
162
+ if (!account) {
159
163
  throw new Error(`Provider account not found: ${name}`);
160
164
  }
165
+ if (account.unused) {
166
+ throw new Error(`Account ${name} holds a login of the other kind and is never used, so it cannot be pinned. To switch ${provider} to it, save it again with /login.`);
167
+ }
161
168
  }
162
169
  await storage.modify(provider, async (current) => {
163
170
  if (current === undefined) {
@@ -1,4 +1,4 @@
1
- import { listSlots as listCredentialSlots } from "@earendil-works/pi-ai/auth/pool/slots";
1
+ import { slotsOfOwnKind } from "@earendil-works/pi-ai/auth/pool/kinds";
2
2
  import { resolveConfigValue } from "../resolve-config-value.js";
3
3
  import { discoverEnvSlots } from "./env-slots.js";
4
4
  import { STORE_BUSY, writeHealthUnlessStoreBusy } from "./health-write.js";
@@ -74,7 +74,8 @@ export async function listRotationSlots(sources, options = {}) {
74
74
  if (credential) {
75
75
  const state = await repository.listSlots(providerId, "stored");
76
76
  const slots = [];
77
- for (const slot of listCredentialSlots(credential)) {
77
+ // A slot of the other kind cannot project, so a session must never be handed it (senpi#3486).
78
+ for (const slot of slotsOfOwnKind(credential)) {
78
79
  const persisted = state[slot.name];
79
80
  const storedRevision = await repository.storedCredentialRevision(providerId, slot.name, {
80
81
  key: slot.key,
@@ -9,7 +9,12 @@ function usage(ctx) {
9
9
  ctx.ui.notify("Usage: /account <provider> [list | pin <id> | unpin | reset <id> | remove <id> | rename <id> <display name...> | clear-name <id>]", "error");
10
10
  }
11
11
  function statusOf(account) {
12
- const states = [accountLabel(account), account.source, account.blocked ? "blocked" : "available"];
12
+ const availability = account.unused
13
+ ? "unused (other login kind; switch with /login)"
14
+ : account.blocked
15
+ ? "blocked"
16
+ : "available";
17
+ const states = [accountLabel(account), account.source, availability];
13
18
  for (const { model, until } of account.blockedModels ?? []) {
14
19
  states.push(`blocked for ${model} until ${new Date(until).toISOString()}`);
15
20
  }
@@ -11,6 +11,7 @@ export { getOpenAiRemoteCompactionDetails, OPENAI_REMOTE_COMPACTION_SCHEMA } fro
11
11
  export declare const SENPI_COMPACTION_EVENT = "senpi:compaction";
12
12
  export type OpenAiRemoteCompactionRequest = {
13
13
  body: OpenAiCompactBody;
14
+ sessionId?: string;
14
15
  inputItemCount: number;
15
16
  tokensBefore: number;
16
17
  };
@@ -99,7 +100,7 @@ export declare function createOpenAiRemoteCompactionRequest(options: {
99
100
  branchEntries: SessionEntry[];
100
101
  messages?: AgentMessage[];
101
102
  tokensBefore: number;
102
- promptCacheKey?: string;
103
+ sessionId?: string;
103
104
  serviceTier?: ServiceTier;
104
105
  }): OpenAiRemoteCompactionRequest | undefined;
105
106
  export declare function buildOpenAiRemoteCompactionResult(options: {