@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.
- package/CHANGELOG.md +33 -0
- package/dist/bundle/chunks/{anthropic-messages-S5BTP7U2.js → anthropic-messages-2IQD75YN.js} +1 -1
- package/dist/bundle/chunks/args-SV2KM4KA.js +2 -0
- package/dist/bundle/chunks/azure-openai-responses-HFI6SDH5.js +2 -0
- package/dist/bundle/chunks/{chunk-WQLMQGOW.js → chunk-2AJB5S4C.js} +1 -1
- package/dist/bundle/chunks/{chunk-XSRM367N.js → chunk-3AK3WUN4.js} +2 -2
- package/dist/bundle/chunks/{chunk-6TL72TUD.js → chunk-4HJTPWZY.js} +1 -1
- package/dist/bundle/chunks/chunk-4NPBDGC4.js +26 -0
- package/dist/bundle/chunks/{chunk-E4WOCULF.js → chunk-4XR5HDWC.js} +72 -69
- package/dist/bundle/chunks/{chunk-6IWSMXBA.js → chunk-5GC4IQ3A.js} +1 -1
- package/dist/bundle/chunks/{chunk-7I7ZMSBF.js → chunk-A4B4CMOR.js} +1 -1
- package/dist/bundle/chunks/chunk-B4N4N7BR.js +8 -0
- package/dist/bundle/chunks/{chunk-X23XXKMH.js → chunk-BVFJAXWO.js} +1 -1
- package/dist/bundle/chunks/{chunk-25B6JQC4.js → chunk-C3BJRXEV.js} +1 -1
- package/dist/bundle/chunks/{chunk-DKBJM22C.js → chunk-CQDU6S6C.js} +2 -2
- package/dist/bundle/chunks/{chunk-MF57DS3V.js → chunk-EV4TWAJI.js} +1 -1
- package/dist/bundle/chunks/{chunk-JJXDWX46.js → chunk-F3JGTOLH.js} +1 -1
- package/dist/bundle/chunks/{chunk-CRY5CP3B.js → chunk-G42JHX5Q.js} +3 -3
- package/dist/bundle/chunks/{chunk-5VRI3CME.js → chunk-G7VYQRE7.js} +2 -2
- package/dist/bundle/chunks/chunk-GVSBCJFI.js +2 -0
- package/dist/bundle/chunks/{chunk-KUZPBZUK.js → chunk-HKDGQE6D.js} +2 -2
- package/dist/bundle/chunks/{chunk-HD67U37N.js → chunk-HRV7NEOJ.js} +1 -1
- package/dist/bundle/chunks/chunk-JDETIKL7.js +7 -0
- package/dist/bundle/chunks/{chunk-MVNVDGYF.js → chunk-JGMG227A.js} +1 -1
- package/dist/bundle/chunks/chunk-MHD7SSHF.js +2 -0
- package/dist/bundle/chunks/{chunk-CEQMTVYS.js → chunk-MXLEWTWC.js} +2 -2
- package/dist/bundle/chunks/{chunk-C5W3D56I.js → chunk-N6UN2SDS.js} +3 -3
- package/dist/bundle/chunks/chunk-NTK2MHJ5.js +2 -0
- package/dist/bundle/chunks/chunk-OMN4QWFA.js +3 -0
- package/dist/bundle/chunks/chunk-PLQ5WRXN.js +2 -0
- package/dist/bundle/chunks/{chunk-2MOGCBPT.js → chunk-PUMCQ5HS.js} +1 -1
- package/dist/bundle/chunks/{chunk-55YTTFNY.js → chunk-PUQFKAB4.js} +1 -1
- package/dist/bundle/chunks/{chunk-IJT3GSB2.js → chunk-SN7PTZTC.js} +1 -1
- package/dist/bundle/chunks/{chunk-FWHSGCWR.js → chunk-T2IIUI3J.js} +3 -3
- package/dist/bundle/chunks/{chunk-5AOKDC6G.js → chunk-TSETMVJP.js} +1 -1
- package/dist/bundle/chunks/{chunk-SR7DBNWN.js → chunk-V6AYNGMX.js} +1 -1
- package/dist/bundle/chunks/chunk-VDPJ7W52.js +11 -0
- package/dist/bundle/chunks/{chunk-EPQK674G.js → chunk-VLKULV6F.js} +1 -1
- package/dist/bundle/chunks/{chunk-SL7TSFR5.js → chunk-VXACHCLB.js} +1 -1
- package/dist/bundle/chunks/{chunk-FM7NDZR5.js → chunk-VYSRUDEU.js} +1 -1
- package/dist/bundle/chunks/chunk-YMFGBSO3.js +2 -0
- package/dist/bundle/chunks/cli-main-RA6EBOOU.js +3 -0
- package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-4QEQ7MGK.js → cloudflare-workers-ai-system-one-BVENEDT6.js} +1 -1
- package/dist/bundle/chunks/cursor-agent.js +3 -3
- package/dist/bundle/chunks/google-generative-ai-ONS2KSGN.js +2 -0
- package/dist/bundle/chunks/google-vertex-5VS7ZESH.js +2 -0
- package/dist/bundle/chunks/help-fast-path-FZY53UXB.js +2 -0
- package/dist/bundle/chunks/host-command-TNUIHEFP.js +2 -0
- package/dist/bundle/chunks/{host-lifecycle-EZQ5VCQV.js → host-lifecycle-LDOHAYLK.js} +1 -1
- package/dist/bundle/chunks/host-session-command-OKASUQPA.js +20 -0
- package/dist/bundle/chunks/interactive-mode-PSGTSURY.js +2 -0
- package/dist/bundle/chunks/{mistral-conversations-5EHLE7PZ.js → mistral-conversations-RLDMF2BI.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-OMJZ5VXM.js → multi-session-host-J5BVVUG3.js} +7 -6
- package/dist/bundle/chunks/openai-codex-responses-VRIJIZJG.js +10 -0
- package/dist/bundle/chunks/{openai-completions-NHDMDAS7.js → openai-completions-EZBOIXHP.js} +2 -2
- package/dist/bundle/chunks/{openai-images-PNXQDBSP.js → openai-images-KKM343F2.js} +1 -1
- package/dist/bundle/chunks/openai-responses-CMBH3DCY.js +3 -0
- package/dist/bundle/chunks/{package-manager-cli-7GQDB5DX.js → package-manager-cli-JHVRG35I.js} +1 -1
- package/dist/bundle/chunks/{role-WQWYL4ZD.js → role-LNF5MFGC.js} +1 -1
- package/dist/bundle/chunks/rotation-stream-JK3Q4IWI.js +2 -0
- package/dist/bundle/chunks/rpc-mode-HVSPPRTT.js +2 -0
- package/dist/bundle/chunks/{rpc-monitor-source-FI7H7LGY.js → rpc-monitor-source-2DQZLFFJ.js} +1 -1
- package/dist/bundle/chunks/{runtime-session-IQAKNRTV.js → runtime-session-CLNODAYE.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-XXJC4QQN.js → schedule-command-OMI4VWUV.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-UPYF4OFP.js +3 -0
- package/dist/bundle/chunks/session-picker-NR7CRVNQ.js +2 -0
- package/dist/bundle/chunks/session-worker.js +143 -139
- package/dist/bundle/chunks/{typesafe-system-one-W2ZTYF7X.js → typesafe-system-one-PJIWA5SP.js} +1 -1
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/cli/host-command.d.ts +2 -0
- package/dist/cli/host-command.js +14 -19
- package/dist/cli/host-handover-args.d.ts +4 -0
- package/dist/cli/host-handover-args.js +20 -0
- package/dist/core/agent-session.d.ts +29 -1
- package/dist/core/agent-session.js +74 -19
- package/dist/core/auth-storage.d.ts +44 -1
- package/dist/core/auth-storage.js +120 -3
- package/dist/core/client-message-identity.d.ts +3 -0
- package/dist/core/credential-accounts.d.ts +2 -0
- package/dist/core/credential-accounts.js +8 -1
- package/dist/core/credential-pool/rotation-slots.js +3 -2
- package/dist/core/extensions/builtin/account/index.js +6 -1
- package/dist/core/extensions/builtin/compaction/openai-remote.d.ts +2 -1
- package/dist/core/extensions/builtin/compaction/openai-remote.js +6 -5
- package/dist/core/extensions/builtin/goal/provider-retry.d.ts +2 -2
- package/dist/core/extensions/builtin/goal/provider-retry.js +18 -6
- package/dist/core/extensions/builtin/terminal/command-monitor-source.d.ts +17 -4
- package/dist/core/extensions/builtin/terminal/command-monitor-source.js +33 -6
- package/dist/core/extensions/builtin/terminal/durable-command.js +1 -0
- package/dist/core/extensions/builtin/terminal/extension.js +28 -0
- package/dist/core/extensions/builtin/terminal/monitor-notify-details.d.ts +6 -0
- package/dist/core/extensions/builtin/terminal/monitor-notify-details.js +19 -0
- package/dist/core/extensions/builtin/terminal/monitor-notify-format.d.ts +18 -0
- package/dist/core/extensions/builtin/terminal/monitor-notify-format.js +52 -0
- package/dist/core/extensions/builtin/terminal/monitor-notify.d.ts +8 -1
- package/dist/core/extensions/builtin/terminal/monitor-notify.js +65 -119
- package/dist/core/extensions/builtin/terminal/monitor-overflow-file.d.ts +7 -0
- package/dist/core/extensions/builtin/terminal/monitor-overflow-file.js +67 -0
- package/dist/core/extensions/builtin/terminal/monitor-overflow.d.ts +37 -0
- package/dist/core/extensions/builtin/terminal/monitor-overflow.js +75 -0
- package/dist/core/extensions/builtin/terminal/monitor-registry.d.ts +10 -0
- package/dist/core/extensions/builtin/terminal/monitor-registry.js +24 -1
- package/dist/core/extensions/builtin/terminal/monitor-types.d.ts +5 -0
- package/dist/core/extensions/builtin/terminal/monitor-wake-fit.d.ts +9 -0
- package/dist/core/extensions/builtin/terminal/monitor-wake-fit.js +34 -0
- package/dist/core/extensions/builtin/terminal/monitor-wake-pacing.d.ts +10 -0
- package/dist/core/extensions/builtin/terminal/monitor-wake-pacing.js +41 -0
- package/dist/core/extensions/builtin/terminal/restore-session.js +2 -0
- package/dist/core/extensions/builtin/terminal/rpc-monitor-observer.js +5 -1
- package/dist/core/extensions/builtin/terminal/settings.d.ts +10 -0
- package/dist/core/extensions/builtin/terminal/settings.js +13 -1
- package/dist/core/extensions/builtin/terminal/shared.d.ts +17 -10
- package/dist/core/extensions/builtin/terminal/shared.js +19 -10
- package/dist/core/extensions/builtin/terminal/terminal-manifest-model.d.ts +3 -0
- package/dist/core/extensions/builtin/terminal/terminal-manifest-parse.js +7 -0
- package/dist/core/extensions/builtin/terminal/terminal-manifest.js +1 -0
- package/dist/core/extensions/builtin/terminal/tools/monitor-schema.d.ts +1 -0
- package/dist/core/extensions/builtin/terminal/tools/monitor-schema.js +6 -1
- package/dist/core/extensions/builtin/terminal/tools/monitor.d.ts +1 -0
- package/dist/core/extensions/builtin/terminal/tools/monitor.js +9 -1
- package/dist/core/extensions/builtin/tps.js +1 -8
- package/dist/core/extensions/loader.js +3 -1
- package/dist/core/extensions/runner.d.ts +1 -0
- package/dist/core/extensions/runner.js +12 -1
- package/dist/core/extensions/types.d.ts +3 -1
- package/dist/core/session-manager.d.ts +10 -1
- package/dist/core/session-manager.js +4 -3
- package/dist/core/terminal-settings.d.ts +3 -0
- package/dist/index.d.ts +1 -1
- package/dist/main.d.ts +14 -0
- package/dist/main.js +25 -8
- package/dist/modes/index.d.ts +1 -0
- package/dist/modes/index.js +1 -0
- package/dist/modes/interactive/components/footer.js +4 -2
- package/dist/modes/interactive/interactive-mode.d.ts +5 -0
- package/dist/modes/interactive/interactive-mode.js +13 -1
- package/dist/modes/rpc/client-admission-record.d.ts +5 -0
- package/dist/modes/rpc/client-admission-record.js +2 -0
- package/dist/modes/rpc/connection-handler.d.ts +3 -0
- package/dist/modes/rpc/connection-handler.js +33 -5
- package/dist/modes/rpc/connection-roles.d.ts +30 -0
- package/dist/modes/rpc/connection-roles.js +53 -0
- package/dist/modes/rpc/custom-capability.d.ts +6 -0
- package/dist/modes/rpc/custom-capability.js +6 -0
- package/dist/modes/rpc/host-capabilities.js +2 -1
- package/dist/modes/rpc/host-decision.d.ts +4 -1
- package/dist/modes/rpc/host-decision.js +6 -3
- package/dist/modes/rpc/host-ensure-liveness.d.ts +11 -0
- package/dist/modes/rpc/host-ensure-liveness.js +26 -0
- package/dist/modes/rpc/host-ensure.js +2 -3
- package/dist/modes/rpc/host-holder.d.ts +27 -0
- package/dist/modes/rpc/host-holder.js +18 -0
- package/dist/modes/rpc/host-runner.d.ts +2 -0
- package/dist/modes/rpc/host-runner.js +8 -1
- package/dist/modes/rpc/host-status-all.js +1 -0
- package/dist/modes/rpc/host-status.d.ts +1 -0
- package/dist/modes/rpc/host-status.js +5 -0
- package/dist/modes/rpc/host-stop-generation.d.ts +22 -0
- package/dist/modes/rpc/host-stop-generation.js +36 -0
- package/dist/modes/rpc/host-stop-start-time.d.ts +15 -0
- package/dist/modes/rpc/host-stop-start-time.js +30 -0
- package/dist/modes/rpc/host-stop.d.ts +13 -1
- package/dist/modes/rpc/host-stop.js +78 -9
- package/dist/modes/rpc/rpc-client.d.ts +9 -0
- package/dist/modes/rpc/rpc-client.js +15 -0
- package/dist/modes/rpc/rpc-types.d.ts +18 -0
- package/dist/modes/rpc/rpc-types.js +2 -0
- package/dist/modes/rpc/session-binding.d.ts +1 -1
- package/dist/modes/rpc/session-command-router.d.ts +1 -0
- package/dist/modes/rpc/session-command-router.js +13 -0
- package/dist/modes/rpc/session-worker-client.d.ts +1 -1
- package/dist/modes/rpc/session-worker-client.js +12 -1
- package/dist/modes/rpc/session-worker-protocol.d.ts +3 -0
- package/dist/modes/rpc/session-worker.js +9 -0
- package/docs/rpc.md +54 -2
- package/docs/session-format.md +5 -0
- package/package.json +6 -6
- package/dist/bundle/chunks/args-CRXNC7IS.js +0 -2
- package/dist/bundle/chunks/azure-openai-responses-2DCY6QKO.js +0 -2
- package/dist/bundle/chunks/chunk-5ZBDN3LB.js +0 -26
- package/dist/bundle/chunks/chunk-AEJUOQ7U.js +0 -2
- package/dist/bundle/chunks/chunk-LAWS3FYQ.js +0 -11
- package/dist/bundle/chunks/chunk-LM76EMEJ.js +0 -7
- package/dist/bundle/chunks/chunk-M237EDHB.js +0 -2
- package/dist/bundle/chunks/chunk-V6RY5A6T.js +0 -2
- package/dist/bundle/chunks/chunk-WIAXG52U.js +0 -2
- package/dist/bundle/chunks/chunk-WTFD4TII.js +0 -8
- package/dist/bundle/chunks/chunk-Z33PKFXM.js +0 -3
- package/dist/bundle/chunks/cli-main-RKC5Z25H.js +0 -3
- package/dist/bundle/chunks/google-generative-ai-3SKUSCIL.js +0 -2
- package/dist/bundle/chunks/google-vertex-YHSIIE4G.js +0 -2
- package/dist/bundle/chunks/help-fast-path-FWS5ARO5.js +0 -2
- package/dist/bundle/chunks/host-command-L66Y7ZL5.js +0 -2
- package/dist/bundle/chunks/host-session-command-2YNQNTEH.js +0 -20
- package/dist/bundle/chunks/interactive-mode-XOKTNBLA.js +0 -2
- package/dist/bundle/chunks/openai-codex-responses-4BIPACKV.js +0 -10
- package/dist/bundle/chunks/openai-responses-2UOPEJLP.js +0 -3
- package/dist/bundle/chunks/rotation-stream-J2NUPRMG.js +0 -2
- package/dist/bundle/chunks/rpc-mode-WM5WH7TH.js +0 -2
- package/dist/bundle/chunks/session-control-endpoint-MTOAYRRY.js +0 -3
- 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
|
-
|
|
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(
|
|
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({ ...
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
103
|
+
sessionId?: string;
|
|
103
104
|
serviceTier?: ServiceTier;
|
|
104
105
|
}): OpenAiRemoteCompactionRequest | undefined;
|
|
105
106
|
export declare function buildOpenAiRemoteCompactionResult(options: {
|