@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
|
@@ -24,6 +24,7 @@ export interface HostStatusReport {
|
|
|
24
24
|
readonly lifecycle: "ready" | "stopping" | null;
|
|
25
25
|
readonly socket: string;
|
|
26
26
|
readonly pid: number | null;
|
|
27
|
+
readonly processStartTime: string | null;
|
|
27
28
|
readonly instanceId: string | null;
|
|
28
29
|
readonly generation: number | null;
|
|
29
30
|
readonly engineVersion: string | null;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { isHostCrash, readHostCrashRecords } from "./host-crash-record.js";
|
|
14
14
|
import { readDaemonEnvKeys } from "./host-daemon-env.js";
|
|
15
15
|
import { createHostDaemonPaths, parseShardSocket } from "./host-daemon-paths.js";
|
|
16
|
+
import { readHostRegistration } from "./host-daemon-registration.js";
|
|
16
17
|
import { pruneDeadGenerations, readGenerationRows } from "./host-generations.js";
|
|
17
18
|
import { OBSERVE_REQUEST_FIELD } from "./host-observe-request.js";
|
|
18
19
|
import { observeProtocolInfo, requestOnSocket } from "./host-probe.js";
|
|
@@ -44,6 +45,7 @@ export async function probeHostStatus(options, read = {}) {
|
|
|
44
45
|
await pruneDeadGenerations(paths);
|
|
45
46
|
const generations = await readGenerationRows(paths, { includeDead: !prune });
|
|
46
47
|
const current = generations.find((row) => row.current && row.alive);
|
|
48
|
+
const registered = await readHostRegistration(paths);
|
|
47
49
|
const metrics = current ? await readHostProcessMetrics(current.pid) : UNOBSERVED_METRICS;
|
|
48
50
|
// A socket that did not answer who it is will not answer what it holds: asking again would only
|
|
49
51
|
// spend a second budget on a hung endpoint.
|
|
@@ -56,6 +58,9 @@ export async function probeHostStatus(options, read = {}) {
|
|
|
56
58
|
lifecycle: host?.lifecycle ?? null,
|
|
57
59
|
socket: options.socket,
|
|
58
60
|
pid: current?.pid ?? null,
|
|
61
|
+
processStartTime: registered?.instanceId === current?.instanceId && registered?.record.pid === current?.pid
|
|
62
|
+
? (registered?.record.processStartTime ?? null)
|
|
63
|
+
: null,
|
|
59
64
|
instanceId: host?.instanceId ?? current?.instanceId ?? null,
|
|
60
65
|
generation: host?.generation ?? current?.generation ?? null,
|
|
61
66
|
engineVersion: host?.engineVersion ?? current?.engineVersion ?? null,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RegisteredHost } from "./host-daemon-registration.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The registration the operator saw and authorized, never a newly resolved target. `processStartTime`
|
|
4
|
+
* is null where the platform cannot read it (Windows); the key itself is always present.
|
|
5
|
+
*/
|
|
6
|
+
export interface HostStopGeneration {
|
|
7
|
+
readonly instanceId: string;
|
|
8
|
+
readonly generation: number;
|
|
9
|
+
readonly pid: number;
|
|
10
|
+
readonly processStartTime: string | null;
|
|
11
|
+
}
|
|
12
|
+
export declare function parseHostStopGeneration(text: string): HostStopGeneration | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* How the current registration compares with the confirmed one:
|
|
15
|
+
* - `proven`: same generation, and the start times agree; the live process is still checked before signalling.
|
|
16
|
+
* - `bound_without_start_time`: same pid, instance and generation on a platform that records no start time.
|
|
17
|
+
* - `stale`: a different generation, pid or start time holds the endpoint now.
|
|
18
|
+
* - `unprovable`: same generation, but the start time cannot prove the process (`unknown_owner`).
|
|
19
|
+
*/
|
|
20
|
+
export type StopGenerationMatch = "proven" | "bound_without_start_time" | "stale" | "unprovable";
|
|
21
|
+
export declare function compareStopGeneration(registered: RegisteredHost | undefined, expected: HostStopGeneration): StopGenerationMatch;
|
|
22
|
+
//# sourceMappingURL=host-stop-generation.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { parseJson } from "./host-daemon-state.js";
|
|
2
|
+
import { processStartTimeIsUnreadable } from "./host-stop-start-time.js";
|
|
3
|
+
export function parseHostStopGeneration(text) {
|
|
4
|
+
const value = parseJson(text);
|
|
5
|
+
if (!value ||
|
|
6
|
+
typeof value.instanceId !== "string" ||
|
|
7
|
+
value.instanceId.length === 0 ||
|
|
8
|
+
typeof value.generation !== "number" ||
|
|
9
|
+
!Number.isSafeInteger(value.generation) ||
|
|
10
|
+
value.generation < 0 ||
|
|
11
|
+
typeof value.pid !== "number" ||
|
|
12
|
+
!Number.isSafeInteger(value.pid) ||
|
|
13
|
+
value.pid <= 0 ||
|
|
14
|
+
!(value.processStartTime === null ||
|
|
15
|
+
(typeof value.processStartTime === "string" && value.processStartTime.length > 0)))
|
|
16
|
+
return undefined;
|
|
17
|
+
return {
|
|
18
|
+
instanceId: value.instanceId,
|
|
19
|
+
generation: value.generation,
|
|
20
|
+
pid: value.pid,
|
|
21
|
+
processStartTime: value.processStartTime,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function compareStopGeneration(registered, expected) {
|
|
25
|
+
if (registered?.instanceId !== expected.instanceId ||
|
|
26
|
+
registered.generation !== expected.generation ||
|
|
27
|
+
registered.record.pid !== expected.pid)
|
|
28
|
+
return "stale";
|
|
29
|
+
const recorded = registered.record.processStartTime;
|
|
30
|
+
if (recorded !== null && expected.processStartTime !== null)
|
|
31
|
+
return recorded === expected.processStartTime ? "proven" : "stale";
|
|
32
|
+
if (recorded === null && expected.processStartTime === null && processStartTimeIsUnreadable())
|
|
33
|
+
return "bound_without_start_time";
|
|
34
|
+
return "unprovable";
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=host-stop-generation.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether this platform can record a process start time at registration. On Windows the CIM probe often
|
|
3
|
+
* outlives its 1 s budget while a host starts, so engine hosts record `processStartTime: null` (measured
|
|
4
|
+
* on a Windows runner) and a generation-bound stop cannot compare start times (omo-desktop-app#3561).
|
|
5
|
+
*/
|
|
6
|
+
export declare function processStartTimeIsUnreadable(): boolean;
|
|
7
|
+
/** The live start time of `pid` now, or undefined when the process is gone. Throws when unreadable. */
|
|
8
|
+
export declare function readLiveStartTime(pid: number): Promise<string | undefined>;
|
|
9
|
+
/**
|
|
10
|
+
* Proves, without a recorded start time, that `pid` is still the process that wrote `registrationFile`:
|
|
11
|
+
* that process started before it wrote the file, so a live start time LATER than the file's mtime is a
|
|
12
|
+
* reused pid. Gone, unreadable or unparseable is not proven.
|
|
13
|
+
*/
|
|
14
|
+
export declare function startedBeforeRegistration(pid: number, registrationFile: string): Promise<boolean>;
|
|
15
|
+
//# sourceMappingURL=host-stop-start-time.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { processStartTimeMs, readProcessStartTime } from "../../utils/process-identity.js";
|
|
3
|
+
/**
|
|
4
|
+
* Whether this platform can record a process start time at registration. On Windows the CIM probe often
|
|
5
|
+
* outlives its 1 s budget while a host starts, so engine hosts record `processStartTime: null` (measured
|
|
6
|
+
* on a Windows runner) and a generation-bound stop cannot compare start times (omo-desktop-app#3561).
|
|
7
|
+
*/
|
|
8
|
+
export function processStartTimeIsUnreadable() {
|
|
9
|
+
return process.platform === "win32";
|
|
10
|
+
}
|
|
11
|
+
/** A stop is an operator action: give the start-time probe the budget a loaded Windows host needs. */
|
|
12
|
+
const STOP_START_TIME_BUDGET_MS = 10_000;
|
|
13
|
+
/** The live start time of `pid` now, or undefined when the process is gone. Throws when unreadable. */
|
|
14
|
+
export function readLiveStartTime(pid) {
|
|
15
|
+
return readProcessStartTime(pid, process.platform, STOP_START_TIME_BUDGET_MS);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Proves, without a recorded start time, that `pid` is still the process that wrote `registrationFile`:
|
|
19
|
+
* that process started before it wrote the file, so a live start time LATER than the file's mtime is a
|
|
20
|
+
* reused pid. Gone, unreadable or unparseable is not proven.
|
|
21
|
+
*/
|
|
22
|
+
export async function startedBeforeRegistration(pid, registrationFile) {
|
|
23
|
+
const live = await readLiveStartTime(pid).catch(() => undefined);
|
|
24
|
+
const startedMs = live === undefined ? undefined : processStartTimeMs(live);
|
|
25
|
+
if (startedMs === undefined)
|
|
26
|
+
return false;
|
|
27
|
+
const written = await stat(registrationFile).catch(() => undefined);
|
|
28
|
+
return written !== undefined && startedMs <= written.mtimeMs;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=host-stop-start-time.js.map
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* error to a caller that asked for exactly that outcome.
|
|
20
20
|
*/
|
|
21
21
|
import { type DrainHostResult } from "./host-stop-drain.ts";
|
|
22
|
+
import { type HostStopGeneration } from "./host-stop-generation.ts";
|
|
22
23
|
export interface StopHostOptions {
|
|
23
24
|
readonly socket: string;
|
|
24
25
|
readonly agentDir?: string;
|
|
@@ -33,13 +34,15 @@ export interface StopHostOptions {
|
|
|
33
34
|
readonly timeoutMs?: number;
|
|
34
35
|
/** Refuse unless the host answers both reads: a caller that judged it idle must not stop it unseen. */
|
|
35
36
|
readonly requireAnswer?: boolean;
|
|
37
|
+
/** Refuse rather than signal a generation other than the one the operator confirmed. */
|
|
38
|
+
readonly expectedGeneration?: HostStopGeneration;
|
|
36
39
|
}
|
|
37
40
|
export type StopHostResult = DrainHostResult | {
|
|
38
41
|
readonly action: "stopped";
|
|
39
42
|
readonly pid: number;
|
|
40
43
|
} | {
|
|
41
44
|
readonly action: "refuse";
|
|
42
|
-
readonly reason: "unknown_owner" | "drain_unsupported" | "sessions_live";
|
|
45
|
+
readonly reason: "unknown_owner" | "drain_unsupported" | "sessions_live" | "stale_generation";
|
|
43
46
|
};
|
|
44
47
|
/**
|
|
45
48
|
* Ends a running generation. `drain` is always permitted - it ends no work, it only stops the
|
|
@@ -47,6 +50,14 @@ export type StopHostResult = DrainHostResult | {
|
|
|
47
50
|
* operator never silently kills another client's sessions.
|
|
48
51
|
*/
|
|
49
52
|
export declare function stopHost(options: StopHostOptions): Promise<StopHostResult>;
|
|
53
|
+
type StopRefusal = Extract<StopHostResult, {
|
|
54
|
+
action: "refuse";
|
|
55
|
+
}>;
|
|
56
|
+
/**
|
|
57
|
+
* Refuses a binding the endpoint no longer matches, before any probe or census (desktop#3561 review): a
|
|
58
|
+
* caller learns its confirmation is stale rather than that the host it no longer names is busy.
|
|
59
|
+
*/
|
|
60
|
+
export declare function staleStopGeneration(socket: string, agentDir: string | undefined, expected: HostStopGeneration): Promise<StopRefusal | undefined>;
|
|
50
61
|
/**
|
|
51
62
|
* Signals a generation whose owner was just proven, and reports whether the signal was delivered.
|
|
52
63
|
*
|
|
@@ -56,4 +67,5 @@ export declare function stopHost(options: StopHostOptions): Promise<StopHostResu
|
|
|
56
67
|
* the request. Any OTHER signalling failure (EPERM against somebody else's process) still throws.
|
|
57
68
|
*/
|
|
58
69
|
export declare function signalGeneration(pid: number, signal: NodeJS.Signals): boolean;
|
|
70
|
+
export {};
|
|
59
71
|
//# sourceMappingURL=host-stop.d.ts.map
|
|
@@ -18,21 +18,46 @@
|
|
|
18
18
|
* failure ESRCH means "already gone", not "something went wrong", and must never surface as a raw
|
|
19
19
|
* error to a caller that asked for exactly that outcome.
|
|
20
20
|
*/
|
|
21
|
-
import { createHostDaemonPaths, generationPaths } from "./host-daemon-paths.js";
|
|
21
|
+
import { createHostDaemonPaths, generationPaths, sameEndpoint } from "./host-daemon-paths.js";
|
|
22
22
|
import { provenOwner, readHostRegistration, releaseGeneration } from "./host-daemon-registration.js";
|
|
23
|
+
import { acquireHostEnsureLock } from "./host-ensure-lock.js";
|
|
23
24
|
import { probeProtocolInfo, probeSessionCount } from "./host-probe.js";
|
|
24
25
|
import { drainHost } from "./host-stop-drain.js";
|
|
26
|
+
import { compareStopGeneration } from "./host-stop-generation.js";
|
|
25
27
|
import { writeStopIntent } from "./host-stop-intent.js";
|
|
28
|
+
import { startedBeforeRegistration } from "./host-stop-start-time.js";
|
|
26
29
|
/**
|
|
27
30
|
* Ends a running generation. `drain` is always permitted - it ends no work, it only stops the
|
|
28
31
|
* host from taking new work - while a hard stop requires either an empty host or `force`, so an
|
|
29
32
|
* operator never silently kills another client's sessions.
|
|
30
33
|
*/
|
|
31
34
|
export async function stopHost(options) {
|
|
35
|
+
// A binding names the generation a HARD stop may signal; a drain targets its own instance and must not hold the lock.
|
|
36
|
+
if (options.expectedGeneration !== undefined && options.drain === true)
|
|
37
|
+
throw new TypeError("stopHost: expectedGeneration binds a hard stop and cannot be combined with drain");
|
|
38
|
+
// GC already holds this lock and uses an unqualified stop. Conditional operator stops acquire it
|
|
39
|
+
// here so ensure/handoff cannot publish a different generation between the check and signal.
|
|
40
|
+
if (options.expectedGeneration === undefined)
|
|
41
|
+
return stopHostLocked(options);
|
|
42
|
+
const release = await acquireHostEnsureLock(options.socket, 30_000);
|
|
43
|
+
try {
|
|
44
|
+
return await stopHostLocked(options);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
await release();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function stopHostLocked(options) {
|
|
32
51
|
const paths = createHostDaemonPaths({
|
|
33
52
|
socket: options.socket,
|
|
34
53
|
...(options.agentDir ? { agentDir: options.agentDir } : {}),
|
|
35
54
|
});
|
|
55
|
+
const expected = options.expectedGeneration;
|
|
56
|
+
if (expected !== undefined) {
|
|
57
|
+
const refusal = generationRefusal(compareStopGeneration(await readHostRegistration(paths), expected));
|
|
58
|
+
if (refusal !== undefined)
|
|
59
|
+
return refusal;
|
|
60
|
+
}
|
|
36
61
|
if (options.drain === true) {
|
|
37
62
|
return drainHost(paths, {
|
|
38
63
|
socket: options.socket,
|
|
@@ -41,9 +66,8 @@ export async function stopHost(options) {
|
|
|
41
66
|
...(options.timeoutMs !== undefined && { timeoutMs: options.timeoutMs }),
|
|
42
67
|
});
|
|
43
68
|
}
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
if (!owner)
|
|
69
|
+
const owner = expected === undefined ? await provenOwner(await readHostRegistration(paths), options.socket) : undefined;
|
|
70
|
+
if (expected === undefined && !owner)
|
|
47
71
|
return { action: "refuse", reason: "unknown_owner" };
|
|
48
72
|
const host = await probeProtocolInfo(options.socket, options.timeoutMs ?? 10_000);
|
|
49
73
|
// A hard stop ends whatever the host is doing, including work that belongs to other clients:
|
|
@@ -55,19 +79,64 @@ export async function stopHost(options) {
|
|
|
55
79
|
if (options.requireAnswer === true && (host === undefined || sessions === undefined)) {
|
|
56
80
|
return { action: "refuse", reason: "sessions_live" };
|
|
57
81
|
}
|
|
58
|
-
await
|
|
82
|
+
const target = expected === undefined ? owner : await recheckedTarget(paths, options.socket, expected, host);
|
|
83
|
+
if (target === undefined)
|
|
84
|
+
return { action: "refuse", reason: "unknown_owner" };
|
|
85
|
+
if ("action" in target)
|
|
86
|
+
return target;
|
|
87
|
+
await writeStopIntent(generationPaths(paths, target.instanceId), {
|
|
59
88
|
sender: { pid: process.pid, kind: "stop" },
|
|
60
|
-
targetPid:
|
|
89
|
+
targetPid: target.pid,
|
|
61
90
|
reason: "operator_stop",
|
|
62
91
|
signal: "SIGTERM",
|
|
63
92
|
at: new Date().toISOString(),
|
|
64
93
|
});
|
|
65
|
-
const delivered = signalGeneration(
|
|
94
|
+
const delivered = signalGeneration(target.pid, "SIGTERM");
|
|
66
95
|
// A signal is not an exit: only the supervisor may release ownership after reaping its host.
|
|
67
96
|
// Its circuit breaker deliberately leaves these records intact while the child might be alive.
|
|
68
97
|
if (!delivered)
|
|
69
|
-
await releaseGeneration(paths, { instanceId:
|
|
70
|
-
return { action: "stopped", pid:
|
|
98
|
+
await releaseGeneration(paths, { instanceId: target.instanceId, pid: target.pid });
|
|
99
|
+
return { action: "stopped", pid: target.pid };
|
|
100
|
+
}
|
|
101
|
+
function generationRefusal(match) {
|
|
102
|
+
if (match === "stale")
|
|
103
|
+
return { action: "refuse", reason: "stale_generation" };
|
|
104
|
+
if (match === "unprovable")
|
|
105
|
+
return { action: "refuse", reason: "unknown_owner" };
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Refuses a binding the endpoint no longer matches, before any probe or census (desktop#3561 review): a
|
|
110
|
+
* caller learns its confirmation is stale rather than that the host it no longer names is busy.
|
|
111
|
+
*/
|
|
112
|
+
export async function staleStopGeneration(socket, agentDir, expected) {
|
|
113
|
+
const paths = createHostDaemonPaths({ socket, ...(agentDir ? { agentDir } : {}) });
|
|
114
|
+
return generationRefusal(compareStopGeneration(await readHostRegistration(paths), expected));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The confirmed generation re-read after the probes, and the only pid/instance the stop may signal: the
|
|
118
|
+
* expected identity itself, never a registration read that was not compared with it.
|
|
119
|
+
*/
|
|
120
|
+
async function recheckedTarget(paths, socket, expected, host) {
|
|
121
|
+
const current = await readHostRegistration(paths);
|
|
122
|
+
const match = compareStopGeneration(current, expected);
|
|
123
|
+
const refusal = generationRefusal(match);
|
|
124
|
+
if (refusal !== undefined)
|
|
125
|
+
return refusal;
|
|
126
|
+
if (match === "proven" && !(await provenOwner(current, socket)))
|
|
127
|
+
return undefined;
|
|
128
|
+
if (match === "bound_without_start_time") {
|
|
129
|
+
// No start time was recorded (Windows), so prove the same process another way before signalling: this
|
|
130
|
+
// endpoint, a live process that started before it wrote this registration (a later start is a reused pid),
|
|
131
|
+
// and, when the host answered, the confirmed instance.
|
|
132
|
+
if (current?.socket !== undefined && !sameEndpoint(current.socket, socket))
|
|
133
|
+
return undefined;
|
|
134
|
+
if (!(await startedBeforeRegistration(expected.pid, generationPaths(paths, expected.instanceId).pidFile)))
|
|
135
|
+
return undefined;
|
|
136
|
+
if (host?.instanceId !== undefined && host.instanceId !== expected.instanceId)
|
|
137
|
+
return { action: "refuse", reason: "stale_generation" };
|
|
138
|
+
}
|
|
139
|
+
return { pid: expected.pid, instanceId: expected.instanceId };
|
|
71
140
|
}
|
|
72
141
|
/**
|
|
73
142
|
* Signals a generation whose owner was just proven, and reports whether the signal was delivered.
|
|
@@ -179,6 +179,11 @@ export declare class RpcClient {
|
|
|
179
179
|
}>, "type" | "id" | "sessionId"> & {
|
|
180
180
|
readonly sessionId?: string;
|
|
181
181
|
}): Promise<RpcUnloadAnswer>;
|
|
182
|
+
/**
|
|
183
|
+
* Patch the context the attached session publishes (`set_session_context`); `null` deletes a key.
|
|
184
|
+
* Rejects with {@link RpcCommandError} carrying the host's code (`session_not_attached`, `invalid_session_context`).
|
|
185
|
+
*/
|
|
186
|
+
setSessionContext(patch: Record<string, string | number | boolean | null>, sessionId?: string | undefined): Promise<void>;
|
|
182
187
|
closeSession(sessionId?: string | undefined): Promise<void>;
|
|
183
188
|
/**
|
|
184
189
|
* Drop this connection's session attachments and wait until the host confirms they are gone (senpi#3238).
|
|
@@ -523,6 +528,10 @@ export declare class RpcClient {
|
|
|
523
528
|
* Set the session display name.
|
|
524
529
|
*/
|
|
525
530
|
setSessionName(name: string): Promise<void>;
|
|
531
|
+
/**
|
|
532
|
+
* Report that the user started typing a draft, so a first-activity prompt-cache prewarm can run (senpi#3859).
|
|
533
|
+
*/
|
|
534
|
+
recordDraftActivity(): Promise<void>;
|
|
526
535
|
importJsonl(inputPath: string, cwdOverride?: string): Promise<{
|
|
527
536
|
cancelled: boolean;
|
|
528
537
|
}>;
|
|
@@ -341,6 +341,15 @@ export class RpcClient {
|
|
|
341
341
|
throw new Error("unload_session needs a session");
|
|
342
342
|
return this.getData(await this.send({ ...options, type: "unload_session", sessionId }, false));
|
|
343
343
|
}
|
|
344
|
+
/**
|
|
345
|
+
* Patch the context the attached session publishes (`set_session_context`); `null` deletes a key.
|
|
346
|
+
* Rejects with {@link RpcCommandError} carrying the host's code (`session_not_attached`, `invalid_session_context`).
|
|
347
|
+
*/
|
|
348
|
+
async setSessionContext(patch, sessionId = this.sessionId) {
|
|
349
|
+
if (!sessionId)
|
|
350
|
+
throw new Error("set_session_context needs a session");
|
|
351
|
+
this.getData(await this.send({ type: "set_session_context", sessionId, patch }, false));
|
|
352
|
+
}
|
|
344
353
|
async closeSession(sessionId = this.sessionId) {
|
|
345
354
|
if (!sessionId)
|
|
346
355
|
return;
|
|
@@ -807,6 +816,12 @@ export class RpcClient {
|
|
|
807
816
|
async setSessionName(name) {
|
|
808
817
|
await this.send({ type: "set_session_name", name });
|
|
809
818
|
}
|
|
819
|
+
/**
|
|
820
|
+
* Report that the user started typing a draft, so a first-activity prompt-cache prewarm can run (senpi#3859).
|
|
821
|
+
*/
|
|
822
|
+
async recordDraftActivity() {
|
|
823
|
+
await this.send({ type: "record_user_activity", kind: "draft" });
|
|
824
|
+
}
|
|
810
825
|
async importJsonl(inputPath, cwdOverride) {
|
|
811
826
|
const response = await this.send({ type: "import_jsonl", inputPath, cwdOverride });
|
|
812
827
|
return this.getData(response);
|
|
@@ -372,6 +372,12 @@ type RpcSessionCommand = {
|
|
|
372
372
|
id?: string;
|
|
373
373
|
type: "set_session_name";
|
|
374
374
|
name: string;
|
|
375
|
+
}
|
|
376
|
+
/** The user started typing a draft in this session's composer (senpi#3859). Only `draft` is accepted. */
|
|
377
|
+
| {
|
|
378
|
+
id?: string;
|
|
379
|
+
type: "record_user_activity";
|
|
380
|
+
kind: "draft";
|
|
375
381
|
} | {
|
|
376
382
|
id?: string;
|
|
377
383
|
type: "import_jsonl";
|
|
@@ -572,6 +578,8 @@ export interface RpcAbortQuiesce {
|
|
|
572
578
|
}
|
|
573
579
|
/** Capability: `steer`/`follow_up` accept `whenIdle: "start"`. */
|
|
574
580
|
export declare const STEER_WHEN_IDLE_CAPABILITY = "steer_when_idle";
|
|
581
|
+
/** Capability: `record_user_activity` reports composer draft activity (senpi#3859). */
|
|
582
|
+
export declare const USER_ACTIVITY_CAPABILITY = "user_activity";
|
|
575
583
|
/** What `steer`/`follow_up` do on an idle session: queue (default) or start a turn. */
|
|
576
584
|
export type RpcWhenIdle = "queue" | "start";
|
|
577
585
|
/** What `dequeue_message` / `edit_queued_message` did; anything but removed/edited carries reason + action. */
|
|
@@ -1509,6 +1517,11 @@ export type RpcResponse = {
|
|
|
1509
1517
|
type: "response";
|
|
1510
1518
|
command: "set_session_name";
|
|
1511
1519
|
success: true;
|
|
1520
|
+
} | {
|
|
1521
|
+
id?: string;
|
|
1522
|
+
type: "response";
|
|
1523
|
+
command: "record_user_activity";
|
|
1524
|
+
success: true;
|
|
1512
1525
|
} | {
|
|
1513
1526
|
id?: string;
|
|
1514
1527
|
type: "response";
|
|
@@ -1593,6 +1606,11 @@ export type RpcResponse = {
|
|
|
1593
1606
|
type: "response";
|
|
1594
1607
|
command: "login_api_key";
|
|
1595
1608
|
success: true;
|
|
1609
|
+
/** Present when the key replaced a login of the other kind; `backupPath` holds the previous file (senpi#3448). */
|
|
1610
|
+
data?: {
|
|
1611
|
+
replaced: "oauth" | "api_key";
|
|
1612
|
+
backupPath?: string;
|
|
1613
|
+
};
|
|
1596
1614
|
} | {
|
|
1597
1615
|
id?: string;
|
|
1598
1616
|
type: "response";
|
|
@@ -105,4 +105,6 @@ export const RPC_ERROR_HOST_DRAINING = "host_draining";
|
|
|
105
105
|
export const RPC_ERROR_HOST_STOPPING = "host_stopping";
|
|
106
106
|
/** Capability: `steer`/`follow_up` accept `whenIdle: "start"`. */
|
|
107
107
|
export const STEER_WHEN_IDLE_CAPABILITY = "steer_when_idle";
|
|
108
|
+
/** Capability: `record_user_activity` reports composer draft activity (senpi#3859). */
|
|
109
|
+
export const USER_ACTIVITY_CAPABILITY = "user_activity";
|
|
108
110
|
//# sourceMappingURL=rpc-types.js.map
|
|
@@ -13,5 +13,5 @@ export interface RpcSessionBinding {
|
|
|
13
13
|
* Creates the extension UI bridge and subscriptions within the entry's provider
|
|
14
14
|
* scope. The classic handler remains the single source of command semantics.
|
|
15
15
|
*/
|
|
16
|
-
export declare function createRpcSessionBinding(sessionId: string, entry: RpcSessionEntry, writer: SessionEventWriter, requestClose: () => void, options?: Pick<RpcConnectionOptions, "capabilities" | "clientInfo">): Promise<RpcSessionBinding>;
|
|
16
|
+
export declare function createRpcSessionBinding(sessionId: string, entry: RpcSessionEntry, writer: SessionEventWriter, requestClose: () => void, options?: Pick<RpcConnectionOptions, "capabilities" | "clientInfo" | "connectionRoles">): Promise<RpcSessionBinding>;
|
|
17
17
|
//# sourceMappingURL=session-binding.d.ts.map
|
|
@@ -61,6 +61,7 @@ export declare class SessionCommandRouter {
|
|
|
61
61
|
private readonly createBinding;
|
|
62
62
|
private readonly connectionOptions;
|
|
63
63
|
private readonly pendingCapabilities;
|
|
64
|
+
private readonly connectionRoles;
|
|
64
65
|
private readonly finalizations;
|
|
65
66
|
private readonly idleNow;
|
|
66
67
|
private readonly idleEvictionMs;
|
|
@@ -2,6 +2,7 @@ import { VERSION } from "../../config.js";
|
|
|
2
2
|
import { subscriptionLimits } from "../../core/subscription-limits/limit-store.js";
|
|
3
3
|
import { detachConnection } from "./connection-detach.js";
|
|
4
4
|
import { buildRpcSessionState } from "./connection-handler.js";
|
|
5
|
+
import { CONNECTION_ROLE_FIXED, ConnectionRoles, roleConnection } from "./connection-roles.js";
|
|
5
6
|
import { answerEffectFromTranscript } from "./effect-commands.js";
|
|
6
7
|
import { latestSessionEffect } from "./effect-ledger.js";
|
|
7
8
|
import { multiSessionHostCapabilities } from "./host-capabilities.js";
|
|
@@ -49,6 +50,7 @@ export class SessionCommandRouter {
|
|
|
49
50
|
/** Connections whose socket has already been detached. */
|
|
50
51
|
this.releasedConnections = new Set();
|
|
51
52
|
this.pendingCapabilities = new Map();
|
|
53
|
+
this.connectionRoles = new ConnectionRoles();
|
|
52
54
|
this.finalizations = new Map();
|
|
53
55
|
this.draining = false;
|
|
54
56
|
this.handoffParks = 0;
|
|
@@ -320,6 +322,15 @@ export class SessionCommandRouter {
|
|
|
320
322
|
}
|
|
321
323
|
if (command.type === "set_client_info" && !command.sessionId) {
|
|
322
324
|
const connection = this.writer.currentConnection();
|
|
325
|
+
if (command.capabilities !== undefined &&
|
|
326
|
+
!this.connectionRoles.declare(roleConnection(connection), command.capabilities))
|
|
327
|
+
return {
|
|
328
|
+
id: command.id,
|
|
329
|
+
type: "response",
|
|
330
|
+
command: "set_client_info",
|
|
331
|
+
success: false,
|
|
332
|
+
error: CONNECTION_ROLE_FIXED,
|
|
333
|
+
};
|
|
323
334
|
if (connection !== undefined) {
|
|
324
335
|
this.pendingCapabilities.set(connection, command.capabilities ?? []);
|
|
325
336
|
this.writer.setConnectionCapabilities(connection, command.capabilities ?? []);
|
|
@@ -766,6 +777,7 @@ export class SessionCommandRouter {
|
|
|
766
777
|
capabilities: owner !== undefined
|
|
767
778
|
? (this.pendingCapabilities.get(owner) ?? this.connectionOptions?.capabilities ?? [])
|
|
768
779
|
: this.connectionOptions?.capabilities,
|
|
780
|
+
connectionRoles: this.connectionRoles,
|
|
769
781
|
clientInfo: {
|
|
770
782
|
setCapabilities: (connectionId, capabilities) => {
|
|
771
783
|
if (connectionId !== undefined) {
|
|
@@ -872,6 +884,7 @@ export class SessionCommandRouter {
|
|
|
872
884
|
async releaseConnection(connectionId) {
|
|
873
885
|
this.releasedConnections.add(connectionId);
|
|
874
886
|
this.writer.clearConnectionCapabilities(connectionId);
|
|
887
|
+
this.connectionRoles.release(connectionId);
|
|
875
888
|
for (const sessionId of this.sessionsByConnection.get(connectionId)?.keys() ?? [])
|
|
876
889
|
this.writer.detachConnectionFromSession(connectionId, sessionId);
|
|
877
890
|
const opens = this.opensByConnection.get(connectionId);
|
|
@@ -43,7 +43,7 @@ export declare class SessionWorkerClient {
|
|
|
43
43
|
/** Moves the worker's live session to another permission preset (a later `open_session.permissionPreset`). */
|
|
44
44
|
setPermissionPreset(preset: string): Promise<void>;
|
|
45
45
|
commit(): Promise<WorkerSnapshot>;
|
|
46
|
-
bind(sessionId: string, writer: SessionEventWriter, requestClose: () => void, options: Pick<RpcConnectionOptions, "capabilities" | "clientInfo">): Promise<RpcSessionBinding>;
|
|
46
|
+
bind(sessionId: string, writer: SessionEventWriter, requestClose: () => void, options: Pick<RpcConnectionOptions, "capabilities" | "clientInfo" | "connectionRoles">): Promise<RpcSessionBinding>;
|
|
47
47
|
get busy(): boolean;
|
|
48
48
|
get handoffBusy(): boolean;
|
|
49
49
|
subscribeSettled(listener: () => void): () => void;
|
|
@@ -3,6 +3,7 @@ import { Worker } from "node:worker_threads";
|
|
|
3
3
|
import { isBunBinary } from "../../config.js";
|
|
4
4
|
import { ownProcessStartedAtMs } from "../../core/extensions/builtin/terminal/process-identity.js";
|
|
5
5
|
import { createWebViewBroker } from "../../core/webview/webview-broker.js";
|
|
6
|
+
import { roleConnection } from "./connection-roles.js";
|
|
6
7
|
import { SessionWorkerRequests } from "./session-worker-requests.js";
|
|
7
8
|
import { acknowledge, acknowledgeGrant } from "./session-worker-signals.js";
|
|
8
9
|
const compiledWorkerEntry = typeof SENPI_RPC_SESSION_WORKER_ENTRY === "string"
|
|
@@ -95,10 +96,17 @@ export class SessionWorkerClient {
|
|
|
95
96
|
sessionId,
|
|
96
97
|
capabilities: options.capabilities ?? [],
|
|
97
98
|
connection: writer.currentConnection(),
|
|
99
|
+
roles: options.connectionRoles?.rolesOf(roleConnection(writer.currentConnection())),
|
|
98
100
|
});
|
|
99
101
|
return {
|
|
100
102
|
handle: async (command) => {
|
|
101
|
-
|
|
103
|
+
const connection = writer.currentConnection();
|
|
104
|
+
await this.request({
|
|
105
|
+
type: "command",
|
|
106
|
+
command,
|
|
107
|
+
connection,
|
|
108
|
+
roles: options.connectionRoles?.rolesOf(roleConnection(connection)),
|
|
109
|
+
});
|
|
102
110
|
},
|
|
103
111
|
cancelPendingExtensionUiRequests: () => this.post({ type: "cancel_ui" }),
|
|
104
112
|
dispose: async () => {
|
|
@@ -208,6 +216,9 @@ export class SessionWorkerClient {
|
|
|
208
216
|
return;
|
|
209
217
|
}
|
|
210
218
|
case "capabilities":
|
|
219
|
+
// The isolate already refused a changed role; a refusal here means the two registries drifted.
|
|
220
|
+
if (this.options.connectionRoles?.declare(roleConnection(message.connection), message.capabilities) === false)
|
|
221
|
+
process.stderr.write(`senpi rpc worker role drift: connection ${roleConnection(message.connection)} declared a changed role\n`);
|
|
211
222
|
this.options.clientInfo?.setCapabilities(message.connection, message.capabilities);
|
|
212
223
|
acknowledge(message.signal, true);
|
|
213
224
|
return;
|
|
@@ -47,11 +47,14 @@ export type HostToSessionWorker = {
|
|
|
47
47
|
sessionId: string;
|
|
48
48
|
capabilities: readonly string[];
|
|
49
49
|
connection?: string;
|
|
50
|
+
/** Roles the connection explicitly declared on the host (senpi#3811); the isolate adopts them. */
|
|
51
|
+
roles?: readonly string[];
|
|
50
52
|
} | {
|
|
51
53
|
type: "command";
|
|
52
54
|
request: number;
|
|
53
55
|
command: object;
|
|
54
56
|
connection?: string;
|
|
57
|
+
roles?: readonly string[];
|
|
55
58
|
} | {
|
|
56
59
|
type: "prompt_surface";
|
|
57
60
|
request: number;
|
|
@@ -12,6 +12,7 @@ import { registerWebViewBroker } from "../../core/webview/webview-broker.js";
|
|
|
12
12
|
import { createCliRuntimeFactory } from "../../main.js";
|
|
13
13
|
import { initTheme } from "../interactive/theme/theme.js";
|
|
14
14
|
import { buildRpcSessionState } from "./connection-handler.js";
|
|
15
|
+
import { ConnectionRoles, roleConnection } from "./connection-roles.js";
|
|
15
16
|
import { isHandoffBusy } from "./handoff-activity.js";
|
|
16
17
|
import { createRpcSessionBinding } from "./session-binding.js";
|
|
17
18
|
import { SessionEventWriter } from "./session-event-writer.js";
|
|
@@ -84,6 +85,8 @@ class WorkerEventWriter extends SessionEventWriter {
|
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
const writer = new WorkerEventWriter();
|
|
88
|
+
// One session per isolate: the roles of the connections it serves, adopted from the host before each command.
|
|
89
|
+
const connectionRoles = new ConnectionRoles();
|
|
87
90
|
let prepared;
|
|
88
91
|
let registry;
|
|
89
92
|
let entry;
|
|
@@ -150,9 +153,12 @@ async function handle(message) {
|
|
|
150
153
|
case "bind": {
|
|
151
154
|
if (!entry || binding)
|
|
152
155
|
throw new Error("Invalid session binding");
|
|
156
|
+
if (message.roles !== undefined)
|
|
157
|
+
connectionRoles.adopt(roleConnection(message.connection), message.roles);
|
|
153
158
|
const bindingEntry = entry;
|
|
154
159
|
const createBinding = () => createRpcSessionBinding(message.sessionId, bindingEntry, writer, () => send({ type: "request_close" }), {
|
|
155
160
|
capabilities: message.capabilities,
|
|
161
|
+
connectionRoles,
|
|
156
162
|
clientInfo: {
|
|
157
163
|
connectionId: () => writer.currentConnection(),
|
|
158
164
|
setCapabilities: (connection, capabilities) => exchange((signal) => ({ type: "capabilities", connection, capabilities, signal }), "session_worker_capabilities_denied"),
|
|
@@ -177,6 +183,8 @@ async function handle(message) {
|
|
|
177
183
|
["abort", "abort_bash", "extension_ui_response", "extension_ui_progress"].includes(String(message.command.type));
|
|
178
184
|
if (!binding || (closing && !privileged))
|
|
179
185
|
throw new Error("session_closing");
|
|
186
|
+
if (message.roles !== undefined)
|
|
187
|
+
connectionRoles.adopt(roleConnection(message.connection), message.roles);
|
|
180
188
|
const activeBinding = binding;
|
|
181
189
|
await (message.connection === undefined
|
|
182
190
|
? activeBinding.handle(message.command)
|
|
@@ -212,6 +220,7 @@ async function handle(message) {
|
|
|
212
220
|
unsubscribe?.();
|
|
213
221
|
unsubscribeWake?.();
|
|
214
222
|
await binding?.dispose();
|
|
223
|
+
connectionRoles.clear();
|
|
215
224
|
if (entry?.runtime) {
|
|
216
225
|
const current = entry;
|
|
217
226
|
await runWithProviderScope(current.scope, async () => {
|