@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
package/dist/modes/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export { type HostEndpointEntry, type HostEndpointIdentitySource, listHostEndpoi
|
|
|
11
11
|
export { createHostDaemonPaths, type EnsuredHost, type EnsureHostOptions, ensureHost, type HostDaemonPaths, type HostUpgradePolicy, PINNED_HOST_CLIENT_CAPABILITIES, } from "./rpc/host-ensure.ts";
|
|
12
12
|
export { gcHostEndpoints, type HostGcOptions, type HostGcResult } from "./rpc/host-gc.ts";
|
|
13
13
|
export { type HandoffHostOptions, type HandoffRefusal, type HandoffResult, handoffHost, } from "./rpc/host-handoff.ts";
|
|
14
|
+
export { describeHostHolder, type HostHolder } from "./rpc/host-holder.ts";
|
|
14
15
|
export { DEFAULT_HOST_LAUNCH_SPEC, type HostLaunchSpec, type HostLaunchSpecCore, HostLaunchSpecError, type HostLaunchSpecRefusal, loadHostLaunchSpec, parseHostLaunchSpec, type ResolvedHostLaunchSpec, } from "./rpc/host-launch-spec.ts";
|
|
15
16
|
export { type ProbeHostOptions, probeHost } from "./rpc/host-probe.ts";
|
|
16
17
|
export { HOST_EXIT_ERROR, HOST_EXIT_FALLBACK, HOST_EXIT_OK, HOST_EXIT_REFUSED, HOST_EXIT_USAGE, type HostOutcome, type HostRequest, type HostTarget, runHostRequest, } from "./rpc/host-runner.ts";
|
package/dist/modes/index.js
CHANGED
|
@@ -16,6 +16,7 @@ export { gcHostEndpoints } from "./rpc/host-gc.js";
|
|
|
16
16
|
// Replacing a running daemon without ending its work: the drain-based generation handoff,
|
|
17
17
|
// the identity probe every decision starts from, and the two ways a generation is ended.
|
|
18
18
|
export { handoffHost, } from "./rpc/host-handoff.js";
|
|
19
|
+
export { describeHostHolder } from "./rpc/host-holder.js";
|
|
19
20
|
// What a client hands `senpi host` to describe the daemon it wants, and the trust boundary it crosses.
|
|
20
21
|
export { DEFAULT_HOST_LAUNCH_SPEC, HostLaunchSpecError, loadHostLaunchSpec, parseHostLaunchSpec, } from "./rpc/host-launch-spec.js";
|
|
21
22
|
export { probeHost } from "./rpc/host-probe.js";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { slotsOfOwnKind } from "@earendil-works/pi-ai/auth/pool/kinds";
|
|
3
4
|
import { applyAccountOrder, rendezvousOrder } from "@earendil-works/pi-ai/auth/pool/select";
|
|
4
|
-
import { accountLabel
|
|
5
|
+
import { accountLabel } from "@earendil-works/pi-ai/auth/pool/slots";
|
|
5
6
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
6
7
|
import { theme } from "../theme/theme.js";
|
|
7
8
|
import { planFooterLayout } from "./footer-layout.js";
|
|
@@ -47,7 +48,8 @@ export function formatTokens(count) {
|
|
|
47
48
|
*/
|
|
48
49
|
export function accountFooterSuffix(credential, sessionId) {
|
|
49
50
|
const pooled = credential;
|
|
50
|
-
|
|
51
|
+
// Only slots the credential can serve with; a mixed pool's other-kind slot never does (senpi#3486).
|
|
52
|
+
const slots = credential ? slotsOfOwnKind(credential) : [];
|
|
51
53
|
if (slots.length < 2)
|
|
52
54
|
return "";
|
|
53
55
|
const pinned = Object.entries(credential ?? {}).find(([key]) => key === "pinned")?.[1];
|
|
@@ -67,6 +67,11 @@ export interface InteractiveModeOptions {
|
|
|
67
67
|
type: "info" | "warning" | "error";
|
|
68
68
|
message: string;
|
|
69
69
|
}>;
|
|
70
|
+
/** Credential notices to show once at startup; `markSeen` runs after they are shown (senpi#3486). */
|
|
71
|
+
authNotices?: {
|
|
72
|
+
readonly messages: readonly string[];
|
|
73
|
+
readonly markSeen: () => void;
|
|
74
|
+
};
|
|
70
75
|
/** Warning message if session model couldn't be restored */
|
|
71
76
|
modelFallbackMessage?: string;
|
|
72
77
|
/** Cwd to trust after reload if it gained a .pi directory during this implicitly trusted session. */
|
|
@@ -15,6 +15,7 @@ import { parseSkillBlock, } from "../../core/agent-session.js";
|
|
|
15
15
|
import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
|
|
16
16
|
import { formatNoModelsAvailableMessage } from "../../core/auth-guidance.js";
|
|
17
17
|
import { isApiKeyLoginProvider, loginRowStatus } from "../../core/auth-providers.js";
|
|
18
|
+
import { describeCredentialReplacement } from "../../core/auth-storage.js";
|
|
18
19
|
import { markModeCrashHandler } from "../../core/bootstrap-crash-listeners.js";
|
|
19
20
|
import { envValue } from "../../core/brand.js";
|
|
20
21
|
import { CACHE_TTL_MS, collectCacheMisses, computeCacheWaste, detectCacheMiss, } from "../../core/cache-stats.js";
|
|
@@ -1327,7 +1328,7 @@ export class InteractiveMode {
|
|
|
1327
1328
|
}
|
|
1328
1329
|
});
|
|
1329
1330
|
// Show startup warnings
|
|
1330
|
-
const { migratedProviders, legacyPiEditNotice, startupDiagnostics, modelFallbackMessage, initialMessage, initialImages, initialMessages, initialTitlePrompt, } = this.options;
|
|
1331
|
+
const { migratedProviders, legacyPiEditNotice, startupDiagnostics, authNotices, modelFallbackMessage, initialMessage, initialImages, initialMessages, initialTitlePrompt, } = this.options;
|
|
1331
1332
|
if (migratedProviders && migratedProviders.length > 0) {
|
|
1332
1333
|
this.showWarning(`Migrated credentials to auth.json: ${migratedProviders.join(", ")}`);
|
|
1333
1334
|
}
|
|
@@ -1340,6 +1341,11 @@ export class InteractiveMode {
|
|
|
1340
1341
|
else if (diagnostic.type === "error")
|
|
1341
1342
|
this.showError(diagnostic.message);
|
|
1342
1343
|
}
|
|
1344
|
+
if (authNotices && authNotices.messages.length > 0) {
|
|
1345
|
+
for (const message of authNotices.messages)
|
|
1346
|
+
this.showWarning(message);
|
|
1347
|
+
authNotices.markSeen();
|
|
1348
|
+
}
|
|
1343
1349
|
const modelsJsonError = this.session.modelRuntime.getError();
|
|
1344
1350
|
if (modelsJsonError) {
|
|
1345
1351
|
this.showError(`models.json error: ${modelsJsonError}`);
|
|
@@ -7842,11 +7848,17 @@ export class InteractiveMode {
|
|
|
7842
7848
|
}
|
|
7843
7849
|
}
|
|
7844
7850
|
async loginProvider(dialog, providerId, method) {
|
|
7851
|
+
// A replacement recorded by another login path must not be reported as this one's.
|
|
7852
|
+
this.session.modelRegistry.authStorage.takeReplacement(providerId);
|
|
7845
7853
|
await this.session.modelRuntime.login(providerId, method, {
|
|
7846
7854
|
signal: dialog.signal,
|
|
7847
7855
|
prompt: (prompt) => this.showAuthPrompt(dialog, prompt),
|
|
7848
7856
|
notify: (event) => this.notifyAuthDialog(dialog, event),
|
|
7849
7857
|
}, { getDeviceId: () => this.settingsManager.getOrCreateDeviceId() });
|
|
7858
|
+
// A login of the other kind replaced the stored one: say where it went (senpi#3448).
|
|
7859
|
+
const replacement = this.session.modelRegistry.authStorage.takeReplacement(providerId);
|
|
7860
|
+
if (replacement)
|
|
7861
|
+
this.showWarning(describeCredentialReplacement(providerId, replacement));
|
|
7850
7862
|
}
|
|
7851
7863
|
async showLoginDialog(providerId, providerName) {
|
|
7852
7864
|
const previousModel = this.session.model;
|
|
@@ -38,6 +38,11 @@ export declare const clientAdmissionRecordSchema: z.ZodDiscriminatedUnion<[z.Zod
|
|
|
38
38
|
steer: "steer";
|
|
39
39
|
}>;
|
|
40
40
|
enqueueOrder: z.ZodNumber;
|
|
41
|
+
source: z.ZodOptional<z.ZodEnum<{
|
|
42
|
+
extension: "extension";
|
|
43
|
+
interactive: "interactive";
|
|
44
|
+
rpc: "rpc";
|
|
45
|
+
}>>;
|
|
41
46
|
}, z.core.$strip>;
|
|
42
47
|
}, z.core.$strip>, z.ZodObject<{
|
|
43
48
|
clientTurnId: z.ZodOptional<z.ZodString>;
|
|
@@ -29,6 +29,8 @@ const preparedInputSchema = clientMessageIdentitySchema.extend({
|
|
|
29
29
|
images: z.array(z.object({ type: z.literal("image"), data: z.string(), mimeType: z.string() })).optional(),
|
|
30
30
|
mode: z.enum(["steer", "followUp"]),
|
|
31
31
|
enqueueOrder: z.number(),
|
|
32
|
+
// Additive: a record written before this field restores with no source (unknown origin).
|
|
33
|
+
source: z.enum(["interactive", "rpc", "extension"]).optional(),
|
|
32
34
|
});
|
|
33
35
|
export const clientAdmissionRecordSchema = z.discriminatedUnion("state", [
|
|
34
36
|
admissionIdentitySchema.extend({ state: z.literal("queued"), prepared: preparedInputSchema }),
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { type ToolRuntimeKind } from "../../cli/tool-runtime.ts";
|
|
2
2
|
import type { AgentSessionRuntime } from "../../core/agent-session-runtime.ts";
|
|
3
|
+
import { ConnectionRoles } from "./connection-roles.ts";
|
|
3
4
|
export { buildRpcSessionState } from "./rpc-session-state.ts";
|
|
4
5
|
/** Additive per-connection options. Absent = classic default (byte-identical). */
|
|
5
6
|
export interface RpcConnectionOptions {
|
|
6
7
|
/** Client capability flags from the handshake (e.g. custom_unsupported opt-in). */
|
|
7
8
|
capabilities?: readonly string[];
|
|
9
|
+
/** The host's per-connection role registry; a single-connection process keeps its own. */
|
|
10
|
+
connectionRoles?: ConnectionRoles;
|
|
8
11
|
/** Called instead of requesting process shutdown for a session-owned binding. */
|
|
9
12
|
shutdownHandler?: () => void;
|
|
10
13
|
/** Session registries own runtime disposal themselves. */
|
|
@@ -39,6 +39,7 @@ import { DURABLE_CLIENT_MESSAGE_ID_CAPABILITY } from "./client-admission-record.
|
|
|
39
39
|
import { ClientAdmissions } from "./client-admissions.js";
|
|
40
40
|
import { handleClientInput } from "./client-input-handler.js";
|
|
41
41
|
import { ConnectionQuestionBridge, degradeQuestion, sessionQuestionBridges } from "./connection-question-bridge.js";
|
|
42
|
+
import { CONNECTION_ROLE_FIXED, ConnectionRoles, roleConnection } from "./connection-roles.js";
|
|
42
43
|
import { AUTH_KEY_VERIFY_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, buildCustomUnsupportedRequest, CONTINUE_FROM_LEAF_CAPABILITY, DEFAULT_CUSTOM_EXTENSION_LABEL, EVAL_CELLS_CAPABILITY, EXTENSION_EVENTS_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, QUESTION_CAPABILITY, QUEUED_MESSAGE_REVISION_CAPABILITY, RESUME_SESSION_CAPABILITY, RETRY_FALLBACK_COMMAND_CAPABILITY, SESSION_HELD_CAPABILITY, } from "./custom-capability.js";
|
|
43
44
|
import { runAbort, runGetEffectStatus, runQueuedInputChange, runSetModel, runSetThinkingLevel, } from "./effect-commands.js";
|
|
44
45
|
import { EffectLedger } from "./effect-ledger.js";
|
|
@@ -53,7 +54,7 @@ import { protocolIdentity } from "./protocol-identity.js";
|
|
|
53
54
|
import { buildRpcCommandsForSession, createCommandsChangedEvent, rpcCommandListDigest } from "./rpc-command-surface.js";
|
|
54
55
|
import { rpcCommandPayloadError, rpcCommandShapeError, rpcMessageLengthError, sessionRetryFallbackError, } from "./rpc-input-validation.js";
|
|
55
56
|
import { buildRpcSessionState } from "./rpc-session-state.js";
|
|
56
|
-
import { RPC_ERROR_ENTRY_NOT_FOUND, RPC_ERROR_EVAL_CELL_NOT_FOUND, RPC_ERROR_EVAL_CELLS_UNAVAILABLE, RPC_ERROR_INVALID_ARGUMENT, RPC_ERROR_INVALID_EVAL_CELL_REQUEST, RPC_ERROR_MEDIA_NOT_FOUND, RPC_ERROR_RESPONSE_TOO_LARGE, STEER_WHEN_IDLE_CAPABILITY, } from "./rpc-types.js";
|
|
57
|
+
import { RPC_ERROR_ENTRY_NOT_FOUND, RPC_ERROR_EVAL_CELL_NOT_FOUND, RPC_ERROR_EVAL_CELLS_UNAVAILABLE, RPC_ERROR_INVALID_ARGUMENT, RPC_ERROR_INVALID_EVAL_CELL_REQUEST, RPC_ERROR_MEDIA_NOT_FOUND, RPC_ERROR_RESPONSE_TOO_LARGE, STEER_WHEN_IDLE_CAPABILITY, USER_ACTIVITY_CAPABILITY, } from "./rpc-types.js";
|
|
57
58
|
import { availableModelsData, interruptRunningTurn } from "./session-control-actions-data.js";
|
|
58
59
|
import { SessionExtensionUiRequests, sessionDialogRequests } from "./session-extension-ui-requests.js";
|
|
59
60
|
import { subscribeSessionRecords } from "./session-record-stream.js";
|
|
@@ -162,6 +163,7 @@ function findToolResultMedia(session, toolCallId, contentIndex) {
|
|
|
162
163
|
*/
|
|
163
164
|
export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
164
165
|
let clientCapabilities = options.capabilities;
|
|
166
|
+
const connectionRoles = options.connectionRoles ?? new ConnectionRoles();
|
|
165
167
|
const routingSessionId = options.sessionId;
|
|
166
168
|
// True only while THIS connection's own command drives a replacement; the issuer
|
|
167
169
|
// already learns the new identity from its command response.
|
|
@@ -676,8 +678,11 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
676
678
|
const settled = new AbortController();
|
|
677
679
|
const loginSignal = AbortSignal.any([controller.signal, settled.signal]);
|
|
678
680
|
activeLogins.set(provider, controller);
|
|
681
|
+
const authStorage = session.modelRegistry.authStorage;
|
|
682
|
+
// A replacement left by an earlier login is not this login's; drop it so it is never reported here.
|
|
683
|
+
authStorage.takeReplacement(provider);
|
|
679
684
|
try {
|
|
680
|
-
await
|
|
685
|
+
await authStorage.login(provider, {
|
|
681
686
|
onAuth: (info) => {
|
|
682
687
|
outputEvent({ type: "auth_login_url", provider, url: info.url });
|
|
683
688
|
},
|
|
@@ -691,7 +696,9 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
691
696
|
session.modelRegistry.refresh();
|
|
692
697
|
if (provider === ANTHROPIC_SUBSCRIPTION_PROVIDER_ID)
|
|
693
698
|
emitProviderAccountsChanged(provider);
|
|
694
|
-
|
|
699
|
+
// The client shows what this login replaced and where the previous credential is (senpi#3486).
|
|
700
|
+
const replacement = authStorage.takeReplacement(provider);
|
|
701
|
+
outputEvent({ type: "auth_login_end", provider, success: true, ...replacement });
|
|
695
702
|
}
|
|
696
703
|
catch (loginError) {
|
|
697
704
|
const message = loginError instanceof Error ? loginError.message : String(loginError);
|
|
@@ -751,6 +758,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
751
758
|
EVAL_CELLS_CAPABILITY,
|
|
752
759
|
AUTH_KEY_VERIFY_CAPABILITY,
|
|
753
760
|
QUEUED_MESSAGE_REVISION_CAPABILITY,
|
|
761
|
+
USER_ACTIVITY_CAPABILITY,
|
|
754
762
|
RESUME_SESSION_CAPABILITY,
|
|
755
763
|
...(options.retryFallbackCommand ? [RETRY_FALLBACK_COMMAND_CAPABILITY] : []),
|
|
756
764
|
...(options.toolRuntime ? toolRuntimeCapabilities(options.toolRuntime) : []),
|
|
@@ -903,6 +911,10 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
903
911
|
// State
|
|
904
912
|
// =================================================================
|
|
905
913
|
case "set_client_info":
|
|
914
|
+
if (command.capabilities !== undefined) {
|
|
915
|
+
if (!connectionRoles.declare(roleConnection(options.clientInfo?.connectionId()), command.capabilities))
|
|
916
|
+
return error(id, "set_client_info", CONNECTION_ROLE_FIXED);
|
|
917
|
+
}
|
|
906
918
|
if (options.clientInfo && command.capabilities !== undefined) {
|
|
907
919
|
clientCapabilities = command.capabilities;
|
|
908
920
|
options.clientInfo.setCapabilities(options.clientInfo.connectionId(), command.capabilities);
|
|
@@ -1339,6 +1351,16 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
1339
1351
|
session.setSessionName(name);
|
|
1340
1352
|
return success(id, "set_session_name");
|
|
1341
1353
|
}
|
|
1354
|
+
case "record_user_activity": {
|
|
1355
|
+
// `prompt` activity is emitted by prompt admission itself; a client may only report a draft.
|
|
1356
|
+
// Read as unknown: the wire value is unchecked, whatever the declared command type says.
|
|
1357
|
+
const kind = command.kind;
|
|
1358
|
+
if (kind !== "draft") {
|
|
1359
|
+
return error(id, "record_user_activity", `Unsupported activity kind: ${String(kind)}`, RPC_ERROR_INVALID_ARGUMENT);
|
|
1360
|
+
}
|
|
1361
|
+
session.recordUserActivity("draft");
|
|
1362
|
+
return success(id, "record_user_activity");
|
|
1363
|
+
}
|
|
1342
1364
|
// =================================================================
|
|
1343
1365
|
// Messages
|
|
1344
1366
|
// =================================================================
|
|
@@ -1427,11 +1449,17 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
1427
1449
|
return success(id, "login_cancel");
|
|
1428
1450
|
}
|
|
1429
1451
|
case "login_api_key": {
|
|
1430
|
-
session.modelRegistry.authStorage.set(command.provider, {
|
|
1452
|
+
const replacement = session.modelRegistry.authStorage.set(command.provider, {
|
|
1453
|
+
type: "api_key",
|
|
1454
|
+
key: command.key,
|
|
1455
|
+
});
|
|
1456
|
+
// The response carries what this key replaced (senpi#3448). Consume the record before the
|
|
1457
|
+
// await below, so no record is pending across it and a concurrent OAuth login keeps its own (#3952).
|
|
1458
|
+
session.modelRegistry.authStorage.takeReplacement(command.provider);
|
|
1431
1459
|
// Answer after the registry sees the key: a client re-reading auth status on this
|
|
1432
1460
|
// response must not get the pre-login snapshot (#2384).
|
|
1433
1461
|
await session.modelRegistry.refresh();
|
|
1434
|
-
return success(id, "login_api_key");
|
|
1462
|
+
return success(id, "login_api_key", replacement);
|
|
1435
1463
|
}
|
|
1436
1464
|
case "verify_api_key": {
|
|
1437
1465
|
const verification = await verifyApiKey({
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection roles (senpi#3811): the capabilities that say who a connection speaks for, which is what
|
|
3
|
+
* authority checks read. A role belongs to one connection, never to a session: a shared host serves every
|
|
4
|
+
* connection attached to a session through one binding. Only the client's own `set_client_info` declares a
|
|
5
|
+
* role; launch defaults (`SENPI_RPC_CLIENT_CAPABILITIES`, a host's connection options) never do, so a client
|
|
6
|
+
* can still declare after it attached. Once a connection declared, a different declaration is refused.
|
|
7
|
+
*/
|
|
8
|
+
export declare const CONNECTION_ROLE_CAPABILITIES: readonly string[];
|
|
9
|
+
/** The one connection of a single-session `--mode rpc` process. */
|
|
10
|
+
export declare const SOLE_CONNECTION = "sole-connection";
|
|
11
|
+
/**
|
|
12
|
+
* The key a connection's roles are kept under. A stdio host (and a worker isolate serving it) reads lines
|
|
13
|
+
* with no connection id; its one client is the sole connection, so the role is still declared once and fixed.
|
|
14
|
+
*/
|
|
15
|
+
export declare function roleConnection(connectionId: string | undefined): string;
|
|
16
|
+
export declare const CONNECTION_ROLE_FIXED = "connection_role_fixed: interactive_user cannot change after it is declared";
|
|
17
|
+
export declare class ConnectionRoles {
|
|
18
|
+
private readonly declared;
|
|
19
|
+
/** Records the roles a `set_client_info` carries; false when this connection already declared other roles. */
|
|
20
|
+
declare(connectionId: string, capabilities: readonly string[]): boolean;
|
|
21
|
+
/** The roles this connection explicitly declared, or undefined while it has declared none (no id = a stdio client). */
|
|
22
|
+
rolesOf(connectionId: string | undefined): readonly string[] | undefined;
|
|
23
|
+
/** Takes a declaration made elsewhere (the host, for a worker isolate) unless this connection already declared. */
|
|
24
|
+
adopt(connectionId: string, roles: readonly string[]): void;
|
|
25
|
+
has(connectionId: string | undefined, role: string): boolean;
|
|
26
|
+
release(connectionId: string): void;
|
|
27
|
+
/** Forgets every connection, for a worker isolate whose session ends. */
|
|
28
|
+
clear(): void;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=connection-roles.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { INTERACTIVE_USER_CAPABILITY } from "./custom-capability.js";
|
|
2
|
+
/**
|
|
3
|
+
* Connection roles (senpi#3811): the capabilities that say who a connection speaks for, which is what
|
|
4
|
+
* authority checks read. A role belongs to one connection, never to a session: a shared host serves every
|
|
5
|
+
* connection attached to a session through one binding. Only the client's own `set_client_info` declares a
|
|
6
|
+
* role; launch defaults (`SENPI_RPC_CLIENT_CAPABILITIES`, a host's connection options) never do, so a client
|
|
7
|
+
* can still declare after it attached. Once a connection declared, a different declaration is refused.
|
|
8
|
+
*/
|
|
9
|
+
export const CONNECTION_ROLE_CAPABILITIES = [INTERACTIVE_USER_CAPABILITY];
|
|
10
|
+
/** The one connection of a single-session `--mode rpc` process. */
|
|
11
|
+
export const SOLE_CONNECTION = "sole-connection";
|
|
12
|
+
/**
|
|
13
|
+
* The key a connection's roles are kept under. A stdio host (and a worker isolate serving it) reads lines
|
|
14
|
+
* with no connection id; its one client is the sole connection, so the role is still declared once and fixed.
|
|
15
|
+
*/
|
|
16
|
+
export function roleConnection(connectionId) {
|
|
17
|
+
return connectionId ?? SOLE_CONNECTION;
|
|
18
|
+
}
|
|
19
|
+
export const CONNECTION_ROLE_FIXED = "connection_role_fixed: interactive_user cannot change after it is declared";
|
|
20
|
+
export class ConnectionRoles {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.declared = new Map();
|
|
23
|
+
}
|
|
24
|
+
/** Records the roles a `set_client_info` carries; false when this connection already declared other roles. */
|
|
25
|
+
declare(connectionId, capabilities) {
|
|
26
|
+
const roles = CONNECTION_ROLE_CAPABILITIES.filter((role) => capabilities.includes(role));
|
|
27
|
+
const prior = this.declared.get(connectionId);
|
|
28
|
+
if (prior !== undefined)
|
|
29
|
+
return prior.length === roles.length && prior.every((role) => roles.includes(role));
|
|
30
|
+
this.declared.set(connectionId, roles);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
/** The roles this connection explicitly declared, or undefined while it has declared none (no id = a stdio client). */
|
|
34
|
+
rolesOf(connectionId) {
|
|
35
|
+
return this.declared.get(roleConnection(connectionId));
|
|
36
|
+
}
|
|
37
|
+
/** Takes a declaration made elsewhere (the host, for a worker isolate) unless this connection already declared. */
|
|
38
|
+
adopt(connectionId, roles) {
|
|
39
|
+
if (!this.declared.has(connectionId))
|
|
40
|
+
this.declared.set(connectionId, [...roles]);
|
|
41
|
+
}
|
|
42
|
+
has(connectionId, role) {
|
|
43
|
+
return this.declared.get(roleConnection(connectionId))?.includes(role) ?? false;
|
|
44
|
+
}
|
|
45
|
+
release(connectionId) {
|
|
46
|
+
this.declared.delete(connectionId);
|
|
47
|
+
}
|
|
48
|
+
/** Forgets every connection, for a worker isolate whose session ends. */
|
|
49
|
+
clear() {
|
|
50
|
+
this.declared.clear();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=connection-roles.js.map
|
|
@@ -47,6 +47,12 @@ export declare const RETAIN_ON_DISCONNECT_CAPABILITY = "retain_on_disconnect";
|
|
|
47
47
|
* no other connection will still see it attached (senpi#3238).
|
|
48
48
|
*/
|
|
49
49
|
export declare const DETACH_CONNECTION_CAPABILITY = "detach_connection";
|
|
50
|
+
/**
|
|
51
|
+
* Connection role (senpi#3811): the connection may exercise a person's authority (Stop with intent, release a
|
|
52
|
+
* pause). It does not decide input origin; that comes from the entry point. Declared per connection by the
|
|
53
|
+
* client's own `set_client_info` and fixed at that first declaration (see `connection-roles.ts`).
|
|
54
|
+
*/
|
|
55
|
+
export declare const INTERACTIVE_USER_CAPABILITY = "interactive_user";
|
|
50
56
|
/**
|
|
51
57
|
* HOST capability: this host accepts `open_session.context`, hands it to that session's
|
|
52
58
|
* extensions as `pi.sessionContext`, and republishes it on `list_sessions { include_workers: true }`.
|
|
@@ -47,6 +47,12 @@ export const RETAIN_ON_DISCONNECT_CAPABILITY = "retain_on_disconnect";
|
|
|
47
47
|
* no other connection will still see it attached (senpi#3238).
|
|
48
48
|
*/
|
|
49
49
|
export const DETACH_CONNECTION_CAPABILITY = "detach_connection";
|
|
50
|
+
/**
|
|
51
|
+
* Connection role (senpi#3811): the connection may exercise a person's authority (Stop with intent, release a
|
|
52
|
+
* pause). It does not decide input origin; that comes from the entry point. Declared per connection by the
|
|
53
|
+
* client's own `set_client_info` and fixed at that first declaration (see `connection-roles.ts`).
|
|
54
|
+
*/
|
|
55
|
+
export const INTERACTIVE_USER_CAPABILITY = "interactive_user";
|
|
50
56
|
/**
|
|
51
57
|
* HOST capability: this host accepts `open_session.context`, hands it to that session's
|
|
52
58
|
* extensions as `pi.sessionContext`, and republishes it on `list_sessions { include_workers: true }`.
|
|
@@ -3,7 +3,7 @@ import { ACCEPT_EDITS_PERMISSION_PRESET_CAPABILITY, AUTO_PERMISSION_PRESET_CAPAB
|
|
|
3
3
|
import { DURABLE_CLIENT_MESSAGE_ID_CAPABILITY } from "./client-admission-record.js";
|
|
4
4
|
import { ATTENDED_CAPABILITY, AUTH_KEY_VERIFY_CAPABILITY, AUTO_TITLE_PER_SESSION_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, BROWSER_ENGINE_CAPABILITY, CONTINUE_FROM_LEAF_CAPABILITY, DETACH_CONNECTION_CAPABILITY, DURABLE_SESSION_ID_CAPABILITY, EVAL_CELLS_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, MOVED_PATH_GUARD_CAPABILITY, PROMPT_SURFACE_CAPABILITY, PROMPT_SURFACE_CHAT_CAPABILITY, QUEUED_MESSAGE_REVISION_CAPABILITY, RESUME_SESSION_CAPABILITY, RETAIN_ON_DISCONNECT_CAPABILITY, RETRY_FALLBACK_PROFILE_CAPABILITY, SESSION_CONTEXT_CAPABILITY, SESSION_HELD_CAPABILITY, SESSION_KIND_CAPABILITY, SESSION_STATUS_CAPABILITY, WARM_CAPABILITY, } from "./custom-capability.js";
|
|
5
5
|
import { EFFECT_LEDGER_CAPABILITY } from "./effect-record.js";
|
|
6
|
-
import { STEER_WHEN_IDLE_CAPABILITY } from "./rpc-types.js";
|
|
6
|
+
import { STEER_WHEN_IDLE_CAPABILITY, USER_ACTIVITY_CAPABILITY } from "./rpc-types.js";
|
|
7
7
|
/** `unload_session` and `session_parked.reason` (senpi#3225). */
|
|
8
8
|
export const UNLOAD_SESSION_CAPABILITY = "unload_session";
|
|
9
9
|
/** `get_subscription_limits` and the `subscription_limits` host record (senpi#3370). */
|
|
@@ -31,6 +31,7 @@ export function multiSessionHostCapabilities(options) {
|
|
|
31
31
|
EVAL_CELLS_CAPABILITY,
|
|
32
32
|
AUTH_KEY_VERIFY_CAPABILITY,
|
|
33
33
|
QUEUED_MESSAGE_REVISION_CAPABILITY,
|
|
34
|
+
USER_ACTIVITY_CAPABILITY,
|
|
34
35
|
// Both shared runtimes dispatch through the active session-writing holder guard.
|
|
35
36
|
SESSION_HELD_CAPABILITY,
|
|
36
37
|
RESUME_SESSION_CAPABILITY,
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* and says so instead.
|
|
20
20
|
*/
|
|
21
21
|
import { type EngineBuildIdentity } from "../../core/engine-build-identity.ts";
|
|
22
|
+
import { type HostHolder } from "./host-holder.ts";
|
|
22
23
|
import type { HostProtocolInfo } from "./host-protocol-info.ts";
|
|
23
24
|
import type { RpcLaunchProfile } from "./rpc-types.ts";
|
|
24
25
|
export { type HostProtocolInfo, parseHostProtocolInfo } from "./host-protocol-info.ts";
|
|
@@ -96,7 +97,9 @@ export declare class HostEnsureRefusedError extends Error {
|
|
|
96
97
|
readonly reason: HostRefusalReason;
|
|
97
98
|
/** What only the caller knows: which process is in the way, and how to clear it. */
|
|
98
99
|
readonly detail: string | undefined;
|
|
99
|
-
|
|
100
|
+
/** The generation in the way, when its registration names one (`foreign_writer`). */
|
|
101
|
+
readonly holder: HostHolder | undefined;
|
|
102
|
+
constructor(socket: string, reason: HostRefusalReason, host: HostProtocolInfo | undefined, detail?: string, holder?: HostHolder);
|
|
100
103
|
}
|
|
101
104
|
export declare function decideHostAction(client: HostDecisionClient, host: HostProtocolInfo | undefined, policy: "never"): AttachOnlyDecision;
|
|
102
105
|
export declare function decideHostAction(client: HostDecisionClient, host: HostProtocolInfo | undefined, policy: HostDecisionPolicy): HostDecision;
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { compareEngineOrdinal, } from "../../core/engine-build-identity.js";
|
|
22
22
|
import { EXTENSION_EVENTS_CAPABILITY, SESSION_CONTEXT_CAPABILITY, SESSION_KIND_CAPABILITY, } from "./custom-capability.js";
|
|
23
|
+
import { describeHostHolder } from "./host-holder.js";
|
|
23
24
|
export { parseHostProtocolInfo } from "./host-protocol-info.js";
|
|
24
25
|
/** The wire protocol this build speaks. A host answering with any other number is refused, not replaced. */
|
|
25
26
|
export const HOST_PROTOCOL_VERSION = 1;
|
|
@@ -41,12 +42,14 @@ export const REQUIRED_HOST_CAPABILITIES = [
|
|
|
41
42
|
* endpoint another process already owns is exactly the failure the refusal exists to prevent.
|
|
42
43
|
*/
|
|
43
44
|
export class HostEnsureRefusedError extends Error {
|
|
44
|
-
constructor(socket, reason, host, detail) {
|
|
45
|
-
|
|
45
|
+
constructor(socket, reason, host, detail, holder) {
|
|
46
|
+
const heldBy = holder ? ` (held by ${describeHostHolder(holder)})` : "";
|
|
47
|
+
super(`RPC socket ${socket} refused: ${reason} - ${refusalDetail(reason, host)}${detail ? `: ${detail}` : ""}${heldBy}`);
|
|
46
48
|
this.name = "HostEnsureRefusedError";
|
|
47
49
|
this.socket = socket;
|
|
48
50
|
this.reason = reason;
|
|
49
51
|
this.detail = detail;
|
|
52
|
+
this.holder = holder;
|
|
50
53
|
}
|
|
51
54
|
}
|
|
52
55
|
function refusalDetail(reason, host) {
|
|
@@ -58,7 +61,7 @@ function refusalDetail(reason, host) {
|
|
|
58
61
|
return `the running host is missing ${JSON.stringify(missing)}; it owns the socket, so no second host is started`;
|
|
59
62
|
}
|
|
60
63
|
case "foreign_writer":
|
|
61
|
-
return "
|
|
64
|
+
return "another process registered the host on this socket; it accepts connections but did not answer as a host, and this one may not signal it - stop that host explicitly (host stop --force), then ensure again";
|
|
62
65
|
case "legacy_host":
|
|
63
66
|
return "a host from before the per-socket daemon directory is still running; it is drained and replaced only while it holds no session, and no second host is started beside it";
|
|
64
67
|
case "host_busy":
|
|
@@ -4,12 +4,23 @@
|
|
|
4
4
|
* out of `host-ensure.ts`; neither check ever signals anything.
|
|
5
5
|
*/
|
|
6
6
|
import { type HostDaemonPaths } from "./host-daemon-paths.ts";
|
|
7
|
+
import type { RegisteredHost } from "./host-daemon-registration.ts";
|
|
7
8
|
import { type HostProtocolInfo } from "./host-decision.ts";
|
|
9
|
+
import type { HostHolder } from "./host-holder.ts";
|
|
8
10
|
/**
|
|
9
11
|
* IS-5: a generation nothing can reach whose loop is measurably stalled is still serving its sessions, so
|
|
10
12
|
* it is neither stopped nor stranded beside a new generation: no signal, no successor, registration intact.
|
|
11
13
|
*/
|
|
12
14
|
export declare function refuseIfStalled(paths: HostDaemonPaths, instanceId: string, socket: string, protocol: HostProtocolInfo | undefined): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* I1 for a record another process wrote: while something still accepts at the public path, the generation
|
|
17
|
+
* it names may own the endpoint, so the ensure refuses `foreign_writer` - and the refusal names that
|
|
18
|
+
* generation, so every client can say WHO is in the way instead of only that something is
|
|
19
|
+
* (omo-desktop-app#3561). An endpoint nothing accepts at is left to the caller's stranded start (#1936).
|
|
20
|
+
*/
|
|
21
|
+
export declare function refuseIfForeignHeld(paths: HostDaemonPaths, registered: RegisteredHost, socket: string, protocol: HostProtocolInfo | undefined): Promise<void>;
|
|
22
|
+
/** The registration's own account of the generation: its record also stores the build and launch profile. */
|
|
23
|
+
export declare function readHostHolder(paths: HostDaemonPaths, registered: RegisteredHost): Promise<HostHolder>;
|
|
13
24
|
/**
|
|
14
25
|
* Whether SOMETHING still accepts connections at the public path - the one fact that says a
|
|
15
26
|
* registered process may still own the endpoint. A missing entry and an entry nobody listens
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* out of `host-ensure.ts`; neither check ever signals anything.
|
|
5
5
|
*/
|
|
6
6
|
import { generationPaths } from "./host-daemon-paths.js";
|
|
7
|
+
import { parseJson, readFileOrUndefined } from "./host-daemon-state.js";
|
|
7
8
|
import { HostEnsureRefusedError } from "./host-decision.js";
|
|
8
9
|
import { probeSocketReachable } from "./host-probe.js";
|
|
9
10
|
import { hostChildAlive, hostLoopStalled, stallRefusalWindowMs } from "./host-stalled-evidence.js";
|
|
@@ -24,6 +25,31 @@ export async function refuseIfStalled(paths, instanceId, socket, protocol) {
|
|
|
24
25
|
throw new HostEnsureRefusedError(socket, "host_stalled", protocol);
|
|
25
26
|
}
|
|
26
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* I1 for a record another process wrote: while something still accepts at the public path, the generation
|
|
30
|
+
* it names may own the endpoint, so the ensure refuses `foreign_writer` - and the refusal names that
|
|
31
|
+
* generation, so every client can say WHO is in the way instead of only that something is
|
|
32
|
+
* (omo-desktop-app#3561). An endpoint nothing accepts at is left to the caller's stranded start (#1936).
|
|
33
|
+
*/
|
|
34
|
+
export async function refuseIfForeignHeld(paths, registered, socket, protocol) {
|
|
35
|
+
if (!(await publicEndpointAccepts(socket)))
|
|
36
|
+
return;
|
|
37
|
+
const holder = await readHostHolder(paths, registered);
|
|
38
|
+
throw new HostEnsureRefusedError(socket, "foreign_writer", protocol, undefined, holder);
|
|
39
|
+
}
|
|
40
|
+
/** The registration's own account of the generation: its record also stores the build and launch profile. */
|
|
41
|
+
export async function readHostHolder(paths, registered) {
|
|
42
|
+
const record = parseJson(await readFileOrUndefined(generationPaths(paths, registered.instanceId).pidFile)) ?? {};
|
|
43
|
+
return {
|
|
44
|
+
pid: registered.record.pid,
|
|
45
|
+
instanceId: registered.instanceId,
|
|
46
|
+
generation: registered.generation,
|
|
47
|
+
processStartTime: registered.record.processStartTime,
|
|
48
|
+
engineVersion: typeof record.engineVersion === "string" ? record.engineVersion : null,
|
|
49
|
+
launchProfileId: typeof record.launchProfileId === "string" ? record.launchProfileId : null,
|
|
50
|
+
writerPid: registered.writer?.pid ?? null,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
27
53
|
/**
|
|
28
54
|
* Only the connect matters here, never an answer: the kernel completes it from the listen backlog
|
|
29
55
|
* without the host's event loop, so a live owner under load still accepts within this budget.
|
|
@@ -7,7 +7,7 @@ import { clearHostRegistration, readHostRegistration, writtenByThisProcess, } fr
|
|
|
7
7
|
import { claimHostOwner } from "./host-daemon-state.js";
|
|
8
8
|
import { decideHostAction, HostEnsureRefusedError } from "./host-decision.js";
|
|
9
9
|
import { ensureClient } from "./host-ensure-client.js";
|
|
10
|
-
import {
|
|
10
|
+
import { refuseIfForeignHeld, refuseIfStalled, unregisteredEndpointHeld } from "./host-ensure-liveness.js";
|
|
11
11
|
import { hostEnsureLockOptions, hostEnsureLockTarget } from "./host-ensure-lock.js";
|
|
12
12
|
import { appendStderr, DEFAULT_READINESS_TIMEOUT_MS, DEFAULT_STOP_TIMEOUT_MS, startHost } from "./host-ensure-start.js";
|
|
13
13
|
import { delay, ensureSender, matchesPidFileOrUnknown, resolvePidFileOwnership, STOP_WAIT_BUDGET_MS, stopManagedHost, } from "./host-ensure-stop.js";
|
|
@@ -148,8 +148,7 @@ async function ensureHostLocked(paths, socket, options, stopDeadline = Date.now(
|
|
|
148
148
|
if (!startedByUs) {
|
|
149
149
|
// I1: the socket is silent, but the process behind it is alive. Only the process that WROTE
|
|
150
150
|
// this record may end it - anyone else refuses rather than signalling somebody else's host.
|
|
151
|
-
|
|
152
|
-
throw new HostEnsureRefusedError(socket, "foreign_writer", protocol);
|
|
151
|
+
await refuseIfForeignHeld(paths, registered, socket, protocol);
|
|
153
152
|
await refuseIfStalled(paths, registered.instanceId, socket, protocol);
|
|
154
153
|
// A foreign record whose public endpoint accepts NOTHING names a generation nobody can reach:
|
|
155
154
|
// its entry was replaced (so it is already draining, #1893) or removed, or a dead listener
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHO holds an endpoint an ensure refused to touch (omo-desktop-app#3561).
|
|
3
|
+
*
|
|
4
|
+
* A `foreign_writer` refusal is final by I1: the registered generation was written by another process,
|
|
5
|
+
* it still accepts at the public path, and it did not answer as a host. The refusal used to carry only
|
|
6
|
+
* its reason, so every client - TUI, CLI, desktop - could say that something was in the way but not
|
|
7
|
+
* what. The generation's own record already names it; this is that record as the refusal reports it.
|
|
8
|
+
*
|
|
9
|
+
* A leaf on purpose: `host-decision.ts` words refusals with it and sits in the supervisor's import graph.
|
|
10
|
+
*/
|
|
11
|
+
/** The generation a refusal left standing, as its registration records it. */
|
|
12
|
+
export interface HostHolder {
|
|
13
|
+
/** The process serving the endpoint (the generation's supervisor). */
|
|
14
|
+
readonly pid: number;
|
|
15
|
+
readonly instanceId: string;
|
|
16
|
+
readonly generation: number;
|
|
17
|
+
readonly processStartTime: string | null;
|
|
18
|
+
/** The engine build the generation recorded at start; `null` for a record that predates the field. */
|
|
19
|
+
readonly engineVersion: string | null;
|
|
20
|
+
/** The launch profile the generation was started with; `null` when it recorded none. */
|
|
21
|
+
readonly launchProfileId: string | null;
|
|
22
|
+
/** The process that registered the generation (the client whose ensure started it). */
|
|
23
|
+
readonly writerPid: number | null;
|
|
24
|
+
}
|
|
25
|
+
/** The holder in one clause: `pid 4242, engine 2026.10.16, launch profile cli-profile`. */
|
|
26
|
+
export declare function describeHostHolder(holder: HostHolder): string;
|
|
27
|
+
//# sourceMappingURL=host-holder.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHO holds an endpoint an ensure refused to touch (omo-desktop-app#3561).
|
|
3
|
+
*
|
|
4
|
+
* A `foreign_writer` refusal is final by I1: the registered generation was written by another process,
|
|
5
|
+
* it still accepts at the public path, and it did not answer as a host. The refusal used to carry only
|
|
6
|
+
* its reason, so every client - TUI, CLI, desktop - could say that something was in the way but not
|
|
7
|
+
* what. The generation's own record already names it; this is that record as the refusal reports it.
|
|
8
|
+
*
|
|
9
|
+
* A leaf on purpose: `host-decision.ts` words refusals with it and sits in the supervisor's import graph.
|
|
10
|
+
*/
|
|
11
|
+
/** The holder in one clause: `pid 4242, engine 2026.10.16, launch profile cli-profile`. */
|
|
12
|
+
export function describeHostHolder(holder) {
|
|
13
|
+
const engine = holder.engineVersion === null ? "an unrecorded engine" : `engine ${holder.engineVersion}`;
|
|
14
|
+
const profile = holder.launchProfileId === null ? "" : `, launch profile ${holder.launchProfileId}`;
|
|
15
|
+
const writer = holder.writerPid === null || holder.writerPid === holder.pid ? "" : `, started by pid ${holder.writerPid}`;
|
|
16
|
+
return `pid ${holder.pid}, ${engine}${profile}${writer}`;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=host-holder.js.map
|
|
@@ -3,6 +3,7 @@ import { type HostDecisionPolicy } from "./host-decision.ts";
|
|
|
3
3
|
import { type IdleHandoverTerms } from "./host-handover-request.ts";
|
|
4
4
|
import type { ResolvedHostLaunchSpec } from "./host-launch-spec.ts";
|
|
5
5
|
import { type HostOutcome } from "./host-outcome.ts";
|
|
6
|
+
import type { HostStopGeneration } from "./host-stop-generation.ts";
|
|
6
7
|
export { HOST_EXIT_ERROR, HOST_EXIT_FALLBACK, HOST_EXIT_OK, HOST_EXIT_REFUSED, HOST_EXIT_USAGE, type HostOutcome, } from "./host-outcome.ts";
|
|
7
8
|
/** Which daemon a request is about: the endpoint, and the agent directory holding its state. */
|
|
8
9
|
export interface HostTarget {
|
|
@@ -25,6 +26,7 @@ export type HostRequest = {
|
|
|
25
26
|
readonly target: HostTarget;
|
|
26
27
|
readonly drain: boolean;
|
|
27
28
|
readonly force: boolean;
|
|
29
|
+
readonly expectedGeneration?: HostStopGeneration;
|
|
28
30
|
/** With `drain`: the generation to drain, by instance id (senpi#3653). */
|
|
29
31
|
readonly instanceId?: string;
|
|
30
32
|
/** With `drain`: wait up to this long for that generation to exit. */
|
|
@@ -31,7 +31,7 @@ import { clientRuntimeBuildId, decisionClient, HOST_EXIT_OK, HOST_EXIT_REFUSED,
|
|
|
31
31
|
import { probeProtocolInfo } from "./host-probe.js";
|
|
32
32
|
import { readHostStatus, readSessionCounts } from "./host-status.js";
|
|
33
33
|
import { readAllHostStatus } from "./host-status-all.js";
|
|
34
|
-
import { stopHost } from "./host-stop.js";
|
|
34
|
+
import { staleStopGeneration, stopHost } from "./host-stop.js";
|
|
35
35
|
export { HOST_EXIT_ERROR, HOST_EXIT_FALLBACK, HOST_EXIT_OK, HOST_EXIT_REFUSED, HOST_EXIT_USAGE, } from "./host-outcome.js";
|
|
36
36
|
const PROBE_TIMEOUT_MS = 10_000;
|
|
37
37
|
export async function runHostRequest(request) {
|
|
@@ -93,6 +93,7 @@ async function ensureOutcome(target, spec, policy) {
|
|
|
93
93
|
reason: error.reason,
|
|
94
94
|
socket: target.socket,
|
|
95
95
|
...(error.detail !== undefined && { detail: error.detail }),
|
|
96
|
+
...(error.holder !== undefined && { holder: error.holder }),
|
|
96
97
|
});
|
|
97
98
|
}
|
|
98
99
|
const host = await probeProtocolInfo(target.socket, PROBE_TIMEOUT_MS).finally(ensured.release);
|
|
@@ -140,6 +141,11 @@ function shardPathOutcome(kind, owner, root) {
|
|
|
140
141
|
*/
|
|
141
142
|
async function stopOutcome(request) {
|
|
142
143
|
const { target, drain, force, instanceId, waitMs } = request;
|
|
144
|
+
if (request.expectedGeneration !== undefined) {
|
|
145
|
+
const stale = await staleStopGeneration(target.socket, target.agentDir, request.expectedGeneration);
|
|
146
|
+
if (stale !== undefined)
|
|
147
|
+
return refusal("refuse", undefined, { reason: stale.reason, socket: target.socket });
|
|
148
|
+
}
|
|
143
149
|
const host = await probeProtocolInfo(target.socket, PROBE_TIMEOUT_MS);
|
|
144
150
|
const sessions = await readSessionCounts(target.socket, true);
|
|
145
151
|
const occupied = sessions.foreign_attached + sessions.foreign_retained > 0;
|
|
@@ -151,6 +157,7 @@ async function stopOutcome(request) {
|
|
|
151
157
|
agentDir: target.agentDir,
|
|
152
158
|
drain,
|
|
153
159
|
force,
|
|
160
|
+
...(request.expectedGeneration !== undefined && { expectedGeneration: request.expectedGeneration }),
|
|
154
161
|
...(instanceId !== undefined && { instanceId }),
|
|
155
162
|
...(waitMs !== undefined && { waitMs }),
|
|
156
163
|
});
|