@code-yeongyu/senpi 2026.11.6-2 → 2026.11.6-3
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 +30 -0
- package/dist/bundle/chunks/{bun-extension-importer-RCGPQTI2.js → bun-extension-importer-GQ6IKEXD.js} +2 -2
- package/dist/bundle/chunks/{chunk-QN2BHP3M.js → chunk-44BJPS5I.js} +1 -1
- package/dist/bundle/chunks/{chunk-C7OKUC7L.js → chunk-4BATXPB7.js} +1 -1
- package/dist/bundle/chunks/{chunk-RQ6CHXQ2.js → chunk-5LKB732P.js} +1 -1
- package/dist/bundle/chunks/{chunk-X5ZVYNOL.js → chunk-6CEDL5S2.js} +1 -1
- package/dist/bundle/chunks/{chunk-HLM46XEB.js → chunk-7DRWPZ3N.js} +1 -1
- package/dist/bundle/chunks/{chunk-7PSBD5KS.js → chunk-BASS47UD.js} +1 -1
- package/dist/bundle/chunks/{chunk-ECW7VBMB.js → chunk-CFEHJ4VU.js} +1 -1
- package/dist/bundle/chunks/{chunk-TIPKAMSS.js → chunk-EO2VPZ2K.js} +1 -1
- package/dist/bundle/chunks/{chunk-LNPAT22L.js → chunk-F5JLDCYD.js} +8 -7
- package/dist/bundle/chunks/{chunk-GSKCWXXG.js → chunk-JMQOLOC2.js} +1 -1
- package/dist/bundle/chunks/chunk-L7ZIS3MC.js +106 -0
- package/dist/bundle/chunks/{chunk-47PPZKPO.js → chunk-LM6DOWQH.js} +4 -4
- package/dist/bundle/chunks/chunk-LPYBKMPT.js +2 -0
- package/dist/bundle/chunks/{chunk-GNSMNSL7.js → chunk-LUZVGPQ7.js} +2 -2
- package/dist/bundle/chunks/{chunk-7III44PV.js → chunk-MJTCZUXS.js} +1 -1
- package/dist/bundle/chunks/{chunk-2FDW2LQL.js → chunk-SB6MXNO5.js} +1 -1
- package/dist/bundle/chunks/chunk-UODSYQ2D.js +2 -0
- package/dist/bundle/chunks/{chunk-D5MRJWUK.js → chunk-V2QAYGOK.js} +216 -224
- package/dist/bundle/chunks/{chunk-WGRKM3N6.js → chunk-X53PD4ZC.js} +1 -1
- package/dist/bundle/chunks/{cli-main-T74YJGIF.js → cli-main-RWHDZKTQ.js} +1 -1
- package/dist/bundle/chunks/{dispatch-BAK2YZKD.js → dispatch-VDIYSXQN.js} +1 -1
- package/dist/bundle/chunks/{hand-entry-AYRUGE2A.js → hand-entry-G5LYF62Q.js} +1 -1
- package/dist/bundle/chunks/{host-command-YRXZE4TV.js → host-command-I2JZ5JM7.js} +1 -1
- package/dist/bundle/chunks/{host-session-command-N7IN7ZDV.js → host-session-command-Y52NFPP2.js} +1 -1
- package/dist/bundle/chunks/interactive-mode-PA7DGVS6.js +2 -0
- package/dist/bundle/chunks/main-OBL6375C.js +2 -0
- package/dist/bundle/chunks/{multi-session-host-XSUJDHFZ.js → multi-session-host-NM6I5BWZ.js} +20 -17
- package/dist/bundle/chunks/{package-manager-cli-GLUNPQJU.js → package-manager-cli-GNRQ5MVR.js} +1 -1
- package/dist/bundle/chunks/{role-RRFFHZF5.js → role-F56BS2ZQ.js} +1 -1
- package/dist/bundle/chunks/rpc-mode-G4KIP7XS.js +2 -0
- package/dist/bundle/chunks/{run-attached-tui-W236SEOR.js → run-attached-tui-XTGPAGYM.js} +1 -1
- package/dist/bundle/chunks/{runtime-session-OBXYK4PI.js → runtime-session-VO3X4OE3.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-B2EJTBOE.js → schedule-command-L4M3TSQ3.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-YIMLHGLG.js → session-control-endpoint-V46WMCDF.js} +1 -1
- package/dist/bundle/chunks/{session-picker-E6ZONUJB.js → session-picker-AG6KSR5A.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +87 -83
- 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/core/agent-session-runtime.d.ts +2 -0
- package/dist/core/agent-session-runtime.js +9 -2
- package/dist/core/agent-session.d.ts +24 -3
- package/dist/core/agent-session.js +94 -28
- package/dist/core/credential-pool/rotation-slots.d.ts +3 -3
- package/dist/core/credential-pool/state-store.d.ts +1 -1
- package/dist/core/egress/hpke.d.ts +63 -0
- package/dist/core/egress/hpke.js +188 -0
- package/dist/core/egress/placement-claim.d.ts +69 -0
- package/dist/core/egress/placement-claim.js +115 -0
- package/dist/core/egress/pool-state.d.ts +18 -0
- package/dist/core/egress/pool-state.js +5 -0
- package/dist/core/egress/sealed-format.d.ts +101 -0
- package/dist/core/egress/sealed-format.js +174 -0
- package/dist/core/egress/wire.d.ts +226 -0
- package/dist/core/egress/wire.js +47 -0
- package/dist/core/extensions/builtin/ask-user/escape-residue.d.ts +50 -0
- package/dist/core/extensions/builtin/ask-user/escape-residue.js +113 -0
- package/dist/core/extensions/builtin/ask-user/extension.js +7 -1
- package/dist/core/extensions/builtin/ask-user/resume.js +2 -1
- package/dist/core/extensions/builtin/ask-user/tool.d.ts +3 -0
- package/dist/core/extensions/builtin/ask-user/tool.js +8 -0
- package/dist/core/extensions/builtin/goal/index.js +46 -0
- package/dist/core/extensions/builtin/goal/stop-pause.d.ts +1 -0
- package/dist/core/extensions/builtin/goal/stop-pause.js +4 -2
- package/dist/core/extensions/builtin/permission-system/index.js +10 -4
- package/dist/core/extensions/builtin/permission-system/preset-registry.d.ts +2 -0
- package/dist/core/extensions/builtin/permission-system/preset-registry.js +11 -0
- package/dist/core/extensions/builtin/permission-system/settings.d.ts +2 -0
- package/dist/core/extensions/builtin/permission-system/settings.js +10 -1
- package/dist/core/extensions/builtin/terminal/tools/bash.d.ts +1 -1
- package/dist/core/extensions/builtin/terminal/tools/bash.js +38 -9
- package/dist/core/extensions/builtin/terminal/tools/context.d.ts +3 -0
- package/dist/core/extensions/bun-extension-importer.js +4 -3
- package/dist/core/extensions/bun-extension-load-turns.d.ts +4 -0
- package/dist/core/extensions/bun-extension-load-turns.js +45 -0
- package/dist/core/extensions/bun-extension-registry.d.ts +1 -0
- package/dist/core/extensions/bun-extension-registry.js +19 -1
- package/dist/core/extensions/loader.js +5 -0
- package/dist/core/extensions/session-dialog-api.d.ts +5 -0
- package/dist/core/extensions/session-dialog-api.js +13 -0
- package/dist/core/extensions/types.d.ts +14 -1
- package/dist/core/permission-decision.d.ts +22 -0
- package/dist/core/permission-decision.js +53 -0
- package/dist/core/permission-mode.d.ts +56 -0
- package/dist/core/permission-mode.js +133 -0
- package/dist/core/retry-fallback/controller-types.d.ts +6 -0
- package/dist/core/retry-fallback/controller.d.ts +8 -0
- package/dist/core/retry-fallback/controller.js +31 -3
- package/dist/core/retry-fallback/unusable.d.ts +23 -1
- package/dist/core/retry-fallback/unusable.js +80 -1
- package/dist/core/retry-fallback/usage-limit.d.ts +1 -1
- package/dist/core/retry-fallback/usage-limit.js +6 -3
- package/dist/core/sdk.js +2 -0
- package/dist/core/session-log.js +1 -1
- package/dist/core/stall-warning.d.ts +41 -0
- package/dist/core/stall-warning.js +72 -0
- package/dist/core/turn-origin.d.ts +27 -0
- package/dist/core/turn-origin.js +54 -0
- package/dist/modes/rpc/client-input-handler.d.ts +2 -0
- package/dist/modes/rpc/client-input-handler.js +2 -0
- package/dist/modes/rpc/connection-handler.js +16 -5
- package/dist/modes/rpc/connection-roles.d.ts +6 -0
- package/dist/modes/rpc/connection-roles.js +13 -0
- package/dist/modes/rpc/delegated-drive-origin.d.ts +9 -0
- package/dist/modes/rpc/delegated-drive-origin.js +16 -0
- package/dist/modes/rpc/effect-commands.d.ts +6 -0
- package/dist/modes/rpc/host-bare-endpoint-registration.d.ts +7 -0
- package/dist/modes/rpc/host-bare-endpoint-registration.js +35 -0
- package/dist/modes/rpc/host-capabilities.d.ts +2 -0
- package/dist/modes/rpc/host-capabilities.js +3 -0
- package/dist/modes/rpc/host-handover-parked.d.ts +11 -0
- package/dist/modes/rpc/host-handover-parked.js +17 -0
- package/dist/modes/rpc/host-handover-reopen.js +18 -6
- package/dist/modes/rpc/host-handover-resident-writer.d.ts +16 -0
- package/dist/modes/rpc/host-handover-resident-writer.js +57 -0
- package/dist/modes/rpc/host-warm.d.ts +16 -1
- package/dist/modes/rpc/host-warm.js +33 -6
- package/dist/modes/rpc/method-classes-schema-session.d.ts +9 -0
- package/dist/modes/rpc/method-classes-schema-session.js +1 -0
- package/dist/modes/rpc/method-classes-schema.d.ts +9 -0
- package/dist/modes/rpc/method-classes.d.ts +1 -0
- package/dist/modes/rpc/method-classes.js +2 -0
- package/dist/modes/rpc/multi-session-host.js +6 -1
- package/dist/modes/rpc/permission-preset-command.d.ts +6 -0
- package/dist/modes/rpc/permission-preset-command.js +27 -0
- package/dist/modes/rpc/rpc-mutation-response-types.d.ts +7 -0
- package/dist/modes/rpc/rpc-session-command-types.d.ts +4 -0
- package/dist/modes/rpc/rpc-session-event-types.d.ts +5 -2
- package/dist/modes/rpc/rpc-session-state-types.d.ts +2 -1
- package/dist/modes/rpc/rpc-session-state.js +2 -0
- package/dist/modes/rpc/session-attachment-commands.js +3 -3
- package/dist/modes/rpc/session-event-writer-connections.js +1 -0
- package/dist/modes/rpc/session-event-writer.d.ts +1 -1
- package/dist/modes/rpc/session-registry-attach.d.ts +0 -9
- package/dist/modes/rpc/session-registry-attach.js +16 -3
- package/dist/modes/rpc/session-registry-types.d.ts +2 -0
- package/dist/modes/rpc/session-registry.d.ts +2 -0
- package/dist/modes/rpc/session-registry.js +16 -3
- package/dist/modes/rpc/session-router-close.js +43 -6
- package/dist/modes/rpc/session-router-context.js +2 -0
- package/dist/modes/rpc/session-router-occupancy.js +2 -11
- package/dist/modes/rpc/session-router-open-profile.js +4 -0
- package/dist/modes/rpc/session-router-open.d.ts +1 -1
- package/dist/modes/rpc/session-router-open.js +6 -17
- package/dist/modes/rpc/session-router-residency.d.ts +24 -0
- package/dist/modes/rpc/session-router-residency.js +90 -0
- package/dist/modes/rpc/session-router-routing.js +7 -1
- package/dist/modes/rpc/session-status.js +4 -1
- package/dist/modes/rpc/session-worker.js +4 -0
- package/dist/modes/rpc/worker-session-registry.js +5 -2
- package/docs/engine-socket/fixtures/admission-routes/h-legacy.json +1 -1
- package/docs/engine-socket/fixtures/rpc-inbound-samples.json +12 -2
- package/docs/extensions.md +12 -0
- package/docs/rpc.md +72 -12
- package/native/socket-secret/prebuilds/win32-arm64/senpi_socket_secret.win32-arm64.node +0 -0
- package/native/socket-secret/prebuilds/win32-x64/senpi_socket_secret.win32-x64.node +0 -0
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-C7HEEBFH.js +0 -2
- package/dist/bundle/chunks/chunk-XCMII2VQ.js +0 -98
- package/dist/bundle/chunks/interactive-mode-B5SQDLH7.js +0 -2
- package/dist/bundle/chunks/main-UQFVFCNY.js +0 -2
- package/dist/bundle/chunks/rpc-mode-JYN3E7P7.js +0 -2
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The process that holds a retained session resident (senpi#6190). A host records each retained session it has
|
|
3
|
+
* open, stamped with this writer, so the next host on the endpoint can restore it after a crash; while that
|
|
4
|
+
* writer is still alive (a generation handoff's draining predecessor) the session is not its successor's to
|
|
5
|
+
* reopen - the predecessor parks it, and the unstamped park is what the successor consumes. Liveness follows the
|
|
6
|
+
* session writer claims: same host, same boot, live pid whose start time matches.
|
|
7
|
+
*/
|
|
8
|
+
import { BOOT_INSTANT_TOLERANCE_MS, sameBoot } from "../../core/extensions/builtin/terminal/process-identity.js";
|
|
9
|
+
import { realWriterProbes } from "../../core/session-writer-claims.js";
|
|
10
|
+
export function currentResidentWriter(probes = realWriterProbes) {
|
|
11
|
+
const self = probes.self();
|
|
12
|
+
return {
|
|
13
|
+
host: probes.host(),
|
|
14
|
+
pid: self.pid,
|
|
15
|
+
bootAtMs: probes.bootAtMs(),
|
|
16
|
+
processStartedAtMs: self.processStartedAtMs,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function isResidentWriter(value) {
|
|
20
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
21
|
+
return false;
|
|
22
|
+
const record = value;
|
|
23
|
+
return (typeof record.host === "string" &&
|
|
24
|
+
Number.isInteger(record.pid) &&
|
|
25
|
+
typeof record.bootAtMs === "number" &&
|
|
26
|
+
typeof record.processStartedAtMs === "number");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A liveness probe for one reopen watch, which polls: a writer found gone stays gone, and a live pid's start time
|
|
30
|
+
* (a process spawn on some platforms) is read once. A writer on another host cannot be probed from here, so it is
|
|
31
|
+
* never displaced.
|
|
32
|
+
*/
|
|
33
|
+
export function residentWriterProbe(probes = realWriterProbes) {
|
|
34
|
+
const gone = new Set();
|
|
35
|
+
const verified = new Set();
|
|
36
|
+
return async (writer) => {
|
|
37
|
+
const key = `${writer.host}:${writer.pid}:${writer.processStartedAtMs}`;
|
|
38
|
+
if (gone.has(key))
|
|
39
|
+
return false;
|
|
40
|
+
if (writer.host !== probes.host())
|
|
41
|
+
return true;
|
|
42
|
+
if (!sameBoot(probes.bootAtMs(), writer.bootAtMs) || !probes.pidAlive(writer.pid)) {
|
|
43
|
+
gone.add(key);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (verified.has(key))
|
|
47
|
+
return true;
|
|
48
|
+
const startedAtMs = await probes.readProcessStartMs(writer.pid);
|
|
49
|
+
if (startedAtMs !== undefined && Math.abs(startedAtMs - writer.processStartedAtMs) > BOOT_INSTANT_TOLERANCE_MS) {
|
|
50
|
+
gone.add(key);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
verified.add(key);
|
|
54
|
+
return true;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=host-handover-resident-writer.js.map
|
|
@@ -30,6 +30,11 @@ export declare class HostWarmer {
|
|
|
30
30
|
private readonly load;
|
|
31
31
|
constructor(load: (profile: HostWarmProfile) => Promise<void>);
|
|
32
32
|
run(profile: HostWarmProfile): Promise<HostWarmState>;
|
|
33
|
+
/**
|
|
34
|
+
* Settles once every load of `cwd` in flight NOW has settled; `undefined` when none is. A snapshot: a warm
|
|
35
|
+
* that starts later, or one of another cwd, is not part of it.
|
|
36
|
+
*/
|
|
37
|
+
inFlight(cwd: string): Promise<void> | undefined;
|
|
33
38
|
private loadOnce;
|
|
34
39
|
}
|
|
35
40
|
/**
|
|
@@ -40,7 +45,17 @@ export declare class HostWarmer {
|
|
|
40
45
|
export declare function createRegistryWarm(prepare: NonNullable<PreparableRuntimeFactory["prepare"]>, host: {
|
|
41
46
|
agentDir: string;
|
|
42
47
|
mcpRegistry?: HostMcpRegistry;
|
|
43
|
-
}):
|
|
48
|
+
}): {
|
|
49
|
+
readonly warm: HostWarm;
|
|
50
|
+
readonly inFlight: (cwd: string) => Promise<void> | undefined;
|
|
51
|
+
};
|
|
52
|
+
/** How an open's wait for the warms of its cwd ended. */
|
|
53
|
+
export type WarmWaitOutcome = "settled" | "warm_wait_deadline";
|
|
54
|
+
/**
|
|
55
|
+
* Waits for `warming` at most `boundMs`. A warm whose extension factory never settles (there is no factory
|
|
56
|
+
* timeout) must not stall an open, so past the bound the open proceeds and the reason is reported.
|
|
57
|
+
*/
|
|
58
|
+
export declare function waitForWarms(warming: Promise<void>, boundMs: number, cwd: string): Promise<WarmWaitOutcome>;
|
|
44
59
|
type WarmCommand = Extract<RpcCommand, {
|
|
45
60
|
type: "warm";
|
|
46
61
|
}>;
|
|
@@ -34,14 +34,24 @@ export class HostWarmer {
|
|
|
34
34
|
const key = profileKey(profile);
|
|
35
35
|
if (this.warm.has(key))
|
|
36
36
|
return "already_warm";
|
|
37
|
-
let
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
this.loading.set(key,
|
|
37
|
+
let load = this.loading.get(key);
|
|
38
|
+
if (load === undefined) {
|
|
39
|
+
load = { cwd: profile.cwd, pending: this.loadOnce(key, profile) };
|
|
40
|
+
this.loading.set(key, load);
|
|
41
41
|
}
|
|
42
|
-
await pending;
|
|
42
|
+
await load.pending;
|
|
43
43
|
return "warmed";
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Settles once every load of `cwd` in flight NOW has settled; `undefined` when none is. A snapshot: a warm
|
|
47
|
+
* that starts later, or one of another cwd, is not part of it.
|
|
48
|
+
*/
|
|
49
|
+
inFlight(cwd) {
|
|
50
|
+
const pending = [...this.loading.values()].filter((load) => load.cwd === cwd).map((load) => load.pending);
|
|
51
|
+
if (pending.length === 0)
|
|
52
|
+
return undefined;
|
|
53
|
+
return Promise.allSettled(pending).then(() => undefined);
|
|
54
|
+
}
|
|
45
55
|
async loadOnce(key, profile) {
|
|
46
56
|
try {
|
|
47
57
|
await this.load(profile);
|
|
@@ -85,7 +95,24 @@ export function createRegistryWarm(prepare, host) {
|
|
|
85
95
|
scope.close();
|
|
86
96
|
}
|
|
87
97
|
});
|
|
88
|
-
return (profile) => warmer.run(profile);
|
|
98
|
+
return { warm: (profile) => warmer.run(profile), inFlight: (cwd) => warmer.inFlight(cwd) };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Waits for `warming` at most `boundMs`. A warm whose extension factory never settles (there is no factory
|
|
102
|
+
* timeout) must not stall an open, so past the bound the open proceeds and the reason is reported.
|
|
103
|
+
*/
|
|
104
|
+
export async function waitForWarms(warming, boundMs, cwd) {
|
|
105
|
+
let deadline;
|
|
106
|
+
const outcome = await Promise.race([
|
|
107
|
+
warming.then(() => "settled"),
|
|
108
|
+
new Promise((resolve) => {
|
|
109
|
+
deadline = setTimeout(() => resolve("warm_wait_deadline"), boundMs);
|
|
110
|
+
}),
|
|
111
|
+
]);
|
|
112
|
+
clearTimeout(deadline);
|
|
113
|
+
if (outcome === "warm_wait_deadline")
|
|
114
|
+
process.stderr.write(`senpi rpc open: warm_wait_deadline: a warm of ${cwd} still loading after ${boundMs} ms; opening anyway\n`);
|
|
115
|
+
return outcome;
|
|
89
116
|
}
|
|
90
117
|
function refusal(command, code) {
|
|
91
118
|
return { id: command.id, type: "response", command: "warm", success: false, error: code };
|
|
@@ -478,6 +478,15 @@ export declare const SESSION_RECORD_SCHEMAS: {
|
|
|
478
478
|
} & {
|
|
479
479
|
readonly enabled: Type.TBoolean;
|
|
480
480
|
}>;
|
|
481
|
+
set_permission_preset: Type.TObject<{
|
|
482
|
+
id: Type.TOptional<Type.TString>;
|
|
483
|
+
admissionId: Type.TOptional<Type.TString>;
|
|
484
|
+
type: Type.TLiteral<"set_permission_preset">;
|
|
485
|
+
} & {
|
|
486
|
+
readonly sessionId: Type.TOptional<Type.TString>;
|
|
487
|
+
} & {
|
|
488
|
+
readonly permissionPreset: Type.TString;
|
|
489
|
+
}>;
|
|
481
490
|
set_auto_retry: Type.TObject<{
|
|
482
491
|
id: Type.TOptional<Type.TString>;
|
|
483
492
|
admissionId: Type.TOptional<Type.TString>;
|
|
@@ -131,6 +131,7 @@ const STATE_AND_MODEL_SCHEMAS = {
|
|
|
131
131
|
set_follow_up_mode: session("set_follow_up_mode", { mode: literals("all", "one-at-a-time") }),
|
|
132
132
|
compact: session("compact", { customInstructions: OptStr }),
|
|
133
133
|
set_auto_compaction: session("set_auto_compaction", { enabled: Bool }),
|
|
134
|
+
set_permission_preset: session("set_permission_preset", { permissionPreset: Id }),
|
|
134
135
|
set_auto_retry: session("set_auto_retry", { enabled: Bool }),
|
|
135
136
|
abort_retry: session("abort_retry", {}),
|
|
136
137
|
set_retry_fallback: session("set_retry_fallback", { retryFallback: RetryFallback }),
|
|
@@ -852,6 +852,15 @@ export declare const RPC_INBOUND_SCHEMAS: {
|
|
|
852
852
|
} & {
|
|
853
853
|
readonly enabled: import("typebox").TBoolean;
|
|
854
854
|
}>;
|
|
855
|
+
readonly set_permission_preset: import("typebox").TObject<{
|
|
856
|
+
id: import("typebox").TOptional<import("typebox").TString>;
|
|
857
|
+
admissionId: import("typebox").TOptional<import("typebox").TString>;
|
|
858
|
+
type: import("typebox").TLiteral<"set_permission_preset">;
|
|
859
|
+
} & {
|
|
860
|
+
readonly sessionId: import("typebox").TOptional<import("typebox").TString>;
|
|
861
|
+
} & {
|
|
862
|
+
readonly permissionPreset: import("typebox").TString;
|
|
863
|
+
}>;
|
|
855
864
|
readonly set_auto_retry: import("typebox").TObject<{
|
|
856
865
|
id: import("typebox").TOptional<import("typebox").TString>;
|
|
857
866
|
admissionId: import("typebox").TOptional<import("typebox").TString>;
|
|
@@ -140,6 +140,7 @@ export declare const RPC_METHOD_CLASS: {
|
|
|
140
140
|
readonly version: 1;
|
|
141
141
|
};
|
|
142
142
|
readonly set_auto_compaction: "drive";
|
|
143
|
+
readonly set_permission_preset: "device_admin";
|
|
143
144
|
readonly set_retry_fallback: "drive";
|
|
144
145
|
readonly pause_for_run: "drive";
|
|
145
146
|
readonly resume_for_run: "drive";
|
|
@@ -187,6 +187,8 @@ export const RPC_METHOD_CLASS = {
|
|
|
187
187
|
set_model: { classify: setModelClass, version: 1 },
|
|
188
188
|
// a session override; set_steering_mode, set_follow_up_mode and set_auto_retry write global settings (device_admin)
|
|
189
189
|
set_auto_compaction: "drive",
|
|
190
|
+
// like a preset-naming open_session: it changes what the agent may do without asking
|
|
191
|
+
set_permission_preset: "device_admin",
|
|
190
192
|
set_retry_fallback: "drive",
|
|
191
193
|
pause_for_run: "drive",
|
|
192
194
|
resume_for_run: "drive",
|
|
@@ -13,6 +13,7 @@ import { answerDelegation, relayExecRefusal } from "./delegated-answer.js";
|
|
|
13
13
|
import { parseClientCapabilities } from "./custom-capability.js";
|
|
14
14
|
import { ClientOccupancy } from "./host-client-occupancy.js";
|
|
15
15
|
import { HostCoreGate } from "./host-core-gate.js";
|
|
16
|
+
import { bindRegisteredEndpoint } from "./host-bare-endpoint-registration.js";
|
|
16
17
|
import { createHostDaemonPaths } from "./host-daemon-paths.js";
|
|
17
18
|
import { ENDPOINT_OWNER_FENCE_CAPABILITY, GENERATION_HANDOFF_CAPABILITY } from "./host-decision.js";
|
|
18
19
|
import { HandoffAnnouncement } from "./host-handoff-announcement.js";
|
|
@@ -549,7 +550,11 @@ async function runSocketHost(options, socketPath) {
|
|
|
549
550
|
});
|
|
550
551
|
// Everything the internal listener needs is initialized: the runtime milestone of a coordinated start (senpi#4602).
|
|
551
552
|
reportStartupMilestone("host_runtime");
|
|
552
|
-
|
|
553
|
+
const bind = () => listen(server, socketPath, secret);
|
|
554
|
+
// A host binding its own public socket keeps its registry record; a supervised one's ensure keeps it.
|
|
555
|
+
boundIdentity = await (hostWatchdogBinding() === undefined
|
|
556
|
+
? bindRegisteredEndpoint({ agentDir: options.agentDir, socket: socketPath, log: hostLog }, bind)
|
|
557
|
+
: bind());
|
|
553
558
|
// A key record that cannot be published safely ends this host: held connections are refused, never served keyless.
|
|
554
559
|
await transport.publish(() => shutdown(1));
|
|
555
560
|
if (supervisorPublicOwnerFile) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AgentSessionRuntime } from "../../core/agent-session-runtime.ts";
|
|
2
|
+
import { type RpcCommand, type RpcResponse } from "./rpc-types.ts";
|
|
3
|
+
export declare function runSetPermissionPreset(runtime: Pick<AgentSessionRuntime, "session" | "setPermissionPreset">, command: Extract<RpcCommand, {
|
|
4
|
+
type: "set_permission_preset";
|
|
5
|
+
}>): RpcResponse;
|
|
6
|
+
//# sourceMappingURL=permission-preset-command.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { parsePermissionPresetName } from "../../core/extensions/builtin/permission-system/cli.js";
|
|
2
|
+
import { permissionModeOf, permissionModeReply } from "../../core/permission-mode.js";
|
|
3
|
+
import { RPC_ERROR_INVALID_LAUNCH_PROFILE } from "./rpc-types.js";
|
|
4
|
+
// The live setter a classic `--mode rpc` session has no other way to receive (it refuses `open_session`), and the
|
|
5
|
+
// path-free one for a session on another device. An unknown preset is refused before anything changes, so the
|
|
6
|
+
// session keeps the mode it had; a valid one is applied through the runtime the way an attach naming it applies it.
|
|
7
|
+
export function runSetPermissionPreset(runtime, command) {
|
|
8
|
+
if (parsePermissionPresetName(command.permissionPreset) === undefined) {
|
|
9
|
+
return {
|
|
10
|
+
id: command.id,
|
|
11
|
+
type: "response",
|
|
12
|
+
command: "set_permission_preset",
|
|
13
|
+
success: false,
|
|
14
|
+
error: `${RPC_ERROR_INVALID_LAUNCH_PROFILE}: unknown permissionPreset "${command.permissionPreset}"`,
|
|
15
|
+
errorCode: RPC_ERROR_INVALID_LAUNCH_PROFILE,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
runtime.setPermissionPreset(command.permissionPreset);
|
|
19
|
+
return {
|
|
20
|
+
id: command.id,
|
|
21
|
+
type: "response",
|
|
22
|
+
command: "set_permission_preset",
|
|
23
|
+
success: true,
|
|
24
|
+
data: permissionModeReply(permissionModeOf(runtime.session)),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=permission-preset-command.js.map
|
|
@@ -6,6 +6,7 @@ import type { ClientMessageIdentity } from "../../core/client-message-identity.t
|
|
|
6
6
|
import type { CompactionResult } from "../../core/compaction/index.ts";
|
|
7
7
|
import type { ServiceTier } from "../../core/extensions/builtin/service-tier.ts";
|
|
8
8
|
import type { SessionControlAdmission } from "../../core/extensions/types.ts";
|
|
9
|
+
import type { PermissionModeReply } from "../../core/permission-mode.ts";
|
|
9
10
|
import type { ApiKeyVerification } from "../../core/verify-api-key.ts";
|
|
10
11
|
import type { ClientMessageAdmission } from "./client-admission-record.ts";
|
|
11
12
|
import type { RpcProjectSettings, RpcProjectTrust } from "./project-workspace-commands.ts";
|
|
@@ -234,6 +235,12 @@ export type RpcMutationResponse = {
|
|
|
234
235
|
type: "response";
|
|
235
236
|
command: "set_auto_compaction";
|
|
236
237
|
success: true;
|
|
238
|
+
} | {
|
|
239
|
+
id?: string;
|
|
240
|
+
type: "response";
|
|
241
|
+
command: "set_permission_preset";
|
|
242
|
+
success: true;
|
|
243
|
+
data: PermissionModeReply;
|
|
237
244
|
} | {
|
|
238
245
|
id?: string;
|
|
239
246
|
type: "response";
|
|
@@ -293,6 +293,10 @@ export type RpcSessionCommand = {
|
|
|
293
293
|
id?: string;
|
|
294
294
|
type: "set_auto_compaction";
|
|
295
295
|
enabled: boolean;
|
|
296
|
+
} | {
|
|
297
|
+
id?: string;
|
|
298
|
+
type: "set_permission_preset";
|
|
299
|
+
permissionPreset: string;
|
|
296
300
|
} | {
|
|
297
301
|
id?: string;
|
|
298
302
|
type: "set_auto_retry";
|
|
@@ -109,8 +109,11 @@ export type RpcSessionParkedEvent = {
|
|
|
109
109
|
sessionId: string;
|
|
110
110
|
/** Session file to reopen this session by. */
|
|
111
111
|
sessionPath: string;
|
|
112
|
-
/**
|
|
113
|
-
|
|
112
|
+
/**
|
|
113
|
+
* `idle`: the idle sweep parked it; `unloaded`: `unload_session` did; `host_shutdown`: the host stopped with the session
|
|
114
|
+
* retained, and the next host on the endpoint reopens it. Absent from older hosts.
|
|
115
|
+
*/
|
|
116
|
+
reason?: "idle" | "unloaded" | "host_shutdown";
|
|
114
117
|
};
|
|
115
118
|
/**
|
|
116
119
|
* Sent only to the connections a headless engine's quiet takeover let go (senpi#5732): this connection no longer holds
|
|
@@ -3,6 +3,7 @@ import type { Model, ProviderDiagnostic, ThinkingSelection } from "@earendil-wor
|
|
|
3
3
|
import type { AgentAbortSource } from "../../core/agent-abort-provenance.ts";
|
|
4
4
|
import type { QueuedInput } from "../../core/agent-session.ts";
|
|
5
5
|
import type { HoldKind } from "../../core/engine-turn-hold.ts";
|
|
6
|
+
import type { PermissionMode } from "../../core/permission-mode.ts";
|
|
6
7
|
import type { ProviderOrigin } from "../../core/provider-origin.ts";
|
|
7
8
|
import type { ServiceTier } from "../../core/extensions/builtin/service-tier.ts";
|
|
8
9
|
import type { ContextUsage } from "../../core/extensions/types.ts";
|
|
@@ -105,7 +106,7 @@ export interface RpcSessionModelEntry {
|
|
|
105
106
|
thinkingSelection?: ThinkingSelection;
|
|
106
107
|
serviceTier?: ServiceTier;
|
|
107
108
|
}
|
|
108
|
-
export interface RpcSessionState {
|
|
109
|
+
export interface RpcSessionState extends Partial<PermissionMode> {
|
|
109
110
|
/** The running turn's id; absent when no turn is running. A steer or follow-up names it as `expectedTurnId` (senpi#5776). */
|
|
110
111
|
currentTurnId?: string;
|
|
111
112
|
/** Latest 64 UI-answer receipts in this session generation, oldest first. */
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
6
|
import { sanitizeProviderDiagnostic } from "@earendil-works/pi-ai";
|
|
7
|
+
import { permissionModeOf } from "../../core/permission-mode.js";
|
|
7
8
|
import { ProjectTrustStore } from "../../core/trust-manager.js";
|
|
8
9
|
import { clientFacingIdentity } from "./client-admission-record.js";
|
|
9
10
|
import { sessionQuestionBridges } from "./connection-question-bridge.js";
|
|
@@ -38,6 +39,7 @@ export function buildRpcSessionState(session, lastAbortSource) {
|
|
|
38
39
|
lifecycle: observeLifecycle(session.lifecycle),
|
|
39
40
|
pendingQuestions: sessionQuestionBridges.get(session)?.pendingQuestions(),
|
|
40
41
|
pendingDialogs: sessionDialogRequests.get(session)?.snapshot() ?? [],
|
|
42
|
+
...permissionModeOf(session),
|
|
41
43
|
model: session.model,
|
|
42
44
|
pendingModelSwitch: session.pendingModelSwitch
|
|
43
45
|
? {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { takeOver } from "./attach-takeover.js";
|
|
2
2
|
import { compactOpen } from "./compact-open-admission.js";
|
|
3
|
-
import {
|
|
3
|
+
import { clearRetainedRecord } from "./session-router-residency.js";
|
|
4
4
|
import { hostInstanceId } from "./protocol-identity.js";
|
|
5
5
|
import { canonicalSessionPath } from "./session-path-key.js";
|
|
6
6
|
import { recordAttachment, releaseDisconnectAttachment, releaseOwnerAttachment } from "./session-router-attachments.js";
|
|
@@ -116,8 +116,8 @@ export async function reclaimSession(host, command) {
|
|
|
116
116
|
if (host.sessionsByConnection.get(owner)?.size === 0)
|
|
117
117
|
host.sessionsByConnection.delete(owner);
|
|
118
118
|
connectionLeft(host, row.sessionId, owner);
|
|
119
|
-
if (host.handoverParkedDir !== undefined
|
|
120
|
-
await
|
|
119
|
+
if (host.handoverParkedDir !== undefined)
|
|
120
|
+
await clearRetainedRecord(host, entry);
|
|
121
121
|
if (claim.finalizer) {
|
|
122
122
|
const binding = host.bindings.get(row.sessionId);
|
|
123
123
|
binding?.cancelPendingExtensionUiRequests?.();
|
|
@@ -23,6 +23,7 @@ export class SessionEventWriterConnections extends SessionEventWriterLanes {
|
|
|
23
23
|
record.type === "host_request" ||
|
|
24
24
|
record.type === "relocation_due" ||
|
|
25
25
|
(record.type === "extension_ui_request" &&
|
|
26
|
+
record.audience !== "session" &&
|
|
26
27
|
["select", "confirm", "input", "editor"].includes(String(record.method))));
|
|
27
28
|
}
|
|
28
29
|
isSessionSealed(sessionId) {
|
|
@@ -51,7 +51,7 @@ export declare class SessionEventWriter extends SessionEventWriterDestinations {
|
|
|
51
51
|
* session's reaches every connection. No client asked for this teardown, so there is
|
|
52
52
|
* no close response to answer.
|
|
53
53
|
*/
|
|
54
|
-
parkSession(sessionId: string, sessionPath: string, reason: "idle" | "unloaded"): void;
|
|
54
|
+
parkSession(sessionId: string, sessionPath: string, reason: "idle" | "unloaded" | "host_shutdown"): void;
|
|
55
55
|
/**
|
|
56
56
|
* Seal a session `release_session` handed to a runtime outside this host and publish
|
|
57
57
|
* `session_closed { reason: "released", sessionPath }`. Unlike a park the file must NOT be reopened
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Attach-on-open: a live session outlives individual client attachments, so a resume (or a second
|
|
3
|
-
* surface) for an already-hosted path joins the existing runtime instead of failing. An entry
|
|
4
|
-
* still opening holds the reservation, and an explicit open for that path waits on the entry's
|
|
5
|
-
* openCompletion (bounded on the waiter only, default 45s) and attaches once it opens; on that
|
|
6
|
-
* wait expiring, only the one waiting reopen fails with a retryable session_path_in_use
|
|
7
|
-
* {reason: "open_in_progress", retry_after_ms} naming an attached current owner, and the in-flight
|
|
8
|
-
* open keeps running untouched.
|
|
9
|
-
*/
|
|
10
1
|
import type { SessionPathReservations } from "./host-reservations.ts";
|
|
11
2
|
import { type OpenRpcSession, type RpcSessionEntry, type RpcSessionLaunchProfile, type RpcSessionOpenOptions } from "./session-registry-types.ts";
|
|
12
3
|
export declare function attachToOpenSession(entries: ReadonlyMap<string, RpcSessionEntry>, sessionPath: string, profile: RpcSessionLaunchProfile, options: RpcSessionOpenOptions | undefined, pathReservations: SessionPathReservations | undefined, now: number): Promise<OpenRpcSession>;
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attach-on-open: a live session outlives individual client attachments, so a resume (or a second
|
|
3
|
+
* surface) for an already-hosted path joins the existing runtime instead of failing. An entry
|
|
4
|
+
* still opening holds the reservation, and an explicit open for that path waits on the entry's
|
|
5
|
+
* openCompletion (bounded on the waiter only, default 45s) and attaches once it opens; on that
|
|
6
|
+
* wait expiring, only the one waiting reopen fails with a retryable session_path_in_use
|
|
7
|
+
* {reason: "open_in_progress", retry_after_ms} naming an attached current owner, and the in-flight
|
|
8
|
+
* open keeps running untouched.
|
|
9
|
+
*/
|
|
10
|
+
import { clientNamedPreset, withClientPreset } from "../../core/permission-mode.js";
|
|
1
11
|
import { hostInstanceId } from "./protocol-identity.js";
|
|
2
12
|
import { frozenProfile, RpcSessionRegistryError, } from "./session-registry-types.js";
|
|
3
13
|
/** How long ONE explicit reopen waits on an in-flight open before giving up (the open keeps going). */
|
|
@@ -104,9 +114,12 @@ export async function attachToOpenSession(entries, sessionPath, profile, options
|
|
|
104
114
|
}
|
|
105
115
|
// The preset is applied even when the record already names it: the record is not the live session,
|
|
106
116
|
// so a resent attach also repairs a session that drifted from it (senpi#2842).
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
// Only a preset the client named moves the session: an embedder's host default folded into a preset-less attach is
|
|
118
|
+
// not a choice, and applying it would turn the host default into a client-named mode (remote approval-mode rule).
|
|
119
|
+
const namedPreset = clientNamedPreset(profile);
|
|
120
|
+
if (namedPreset !== undefined) {
|
|
121
|
+
entry.profile = frozenProfile(withClientPreset(entry.profile, namedPreset));
|
|
122
|
+
entry.runtime?.setPermissionPreset(namedPreset);
|
|
110
123
|
}
|
|
111
124
|
entry.lastCommandAt = now;
|
|
112
125
|
if (wasParked) {
|
|
@@ -57,6 +57,8 @@ export interface RpcSessionEntry {
|
|
|
57
57
|
* Requested per `open_session`; an attach may turn it on, never off.
|
|
58
58
|
*/
|
|
59
59
|
retainOnDisconnect?: boolean;
|
|
60
|
+
/** The session path this host's handover-parked record entry is keyed by; it follows a transcript switch. */
|
|
61
|
+
recordedPath?: string;
|
|
60
62
|
/** Timestamp of the last routed command / observed activity; drives idle eviction. */
|
|
61
63
|
lastCommandAt: number;
|
|
62
64
|
/**
|
|
@@ -14,6 +14,7 @@ export declare class RpcSessionRegistry {
|
|
|
14
14
|
readonly closeGraceMs: number;
|
|
15
15
|
/** Present only when the runtime factory can build a session's services alone (`host-warm.ts`). */
|
|
16
16
|
readonly warm?: HostWarm;
|
|
17
|
+
private readonly warmsInFlight?;
|
|
17
18
|
/** The agent directory this host keeps its sessions (and session-move folders) under. */
|
|
18
19
|
get agentDir(): string;
|
|
19
20
|
constructor(options: RpcSessionRegistryOptions);
|
|
@@ -22,6 +23,7 @@ export declare class RpcSessionRegistry {
|
|
|
22
23
|
/** Daemon-family identities come from the endpoint's claims and generation registry. */
|
|
23
24
|
holderPids(observedStarts?: ReadonlyMap<number, number | undefined>): Promise<readonly number[]>;
|
|
24
25
|
openSession(requested: RpcSessionLaunchProfile, options?: RpcSessionOpenOptions): Promise<OpenRpcSession>;
|
|
26
|
+
private openNow;
|
|
25
27
|
/**
|
|
26
28
|
* Read-only lookup with no state transitions or attachment accounting.
|
|
27
29
|
* Exists so lifecycle decisions (e.g. deferring a dropped connection's
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isAbsolute } from "node:path";
|
|
2
2
|
import { assertValidSessionId } from "../../core/session-manager.js";
|
|
3
|
-
import { createRegistryWarm } from "./host-warm.js";
|
|
3
|
+
import { createRegistryWarm, waitForWarms } from "./host-warm.js";
|
|
4
4
|
import { refreshesSessionActivity } from "./session-command-activity.js";
|
|
5
5
|
import { publishedSessionContext } from "./session-context-patch.js";
|
|
6
6
|
import { attachLiveSession, markPathAttached } from "./session-live-attachment.js";
|
|
@@ -20,11 +20,14 @@ export class RpcSessionRegistry {
|
|
|
20
20
|
this.reservations = new Set();
|
|
21
21
|
this.pendingClaims = new Map();
|
|
22
22
|
this.nextHandle = 0;
|
|
23
|
-
if (options.createRuntime.prepare)
|
|
24
|
-
|
|
23
|
+
if (options.createRuntime.prepare) {
|
|
24
|
+
const warming = createRegistryWarm(options.createRuntime.prepare, {
|
|
25
25
|
agentDir: options.agentDir,
|
|
26
26
|
...(options.mcpRegistry !== undefined ? { mcpRegistry: options.mcpRegistry } : {}),
|
|
27
27
|
});
|
|
28
|
+
this.warm = warming.warm;
|
|
29
|
+
this.warmsInFlight = warming.inFlight;
|
|
30
|
+
}
|
|
28
31
|
this.options =
|
|
29
32
|
options.mcpRegistry === undefined
|
|
30
33
|
? options
|
|
@@ -70,6 +73,16 @@ export class RpcSessionRegistry {
|
|
|
70
73
|
return this.options.pathReservations?.holderPids?.(observedStarts) ?? Promise.resolve([]);
|
|
71
74
|
}
|
|
72
75
|
openSession(requested, options) {
|
|
76
|
+
// A warm in flight (the boot load of senpi#6147 included) loads on this loop and yields it between modules
|
|
77
|
+
// (senpi#6199): an open builds its runtime after the warms of ITS cwd that are loading when it arrives, so
|
|
78
|
+
// its factories never run beside that load. Other cwds and later warms are not waited on, and the wait is
|
|
79
|
+
// bounded by `closeGraceMs` like the closing-reservation wait.
|
|
80
|
+
const warming = this.warmsInFlight?.(requested.cwd);
|
|
81
|
+
if (warming === undefined)
|
|
82
|
+
return this.openNow(requested, options);
|
|
83
|
+
return waitForWarms(warming, this.closeGraceMs, requested.cwd).then(() => this.openNow(requested, options));
|
|
84
|
+
}
|
|
85
|
+
openNow(requested, options) {
|
|
73
86
|
return openRegistrySession({
|
|
74
87
|
entries: this.entries,
|
|
75
88
|
reservations: this.reservations,
|
|
@@ -1,8 +1,30 @@
|
|
|
1
|
-
import { clearHandoverParkedSession } from "./host-handover-parked.js";
|
|
2
1
|
import { RPC_ERROR_UNKNOWN_SESSION } from "./rpc-types.js";
|
|
3
2
|
import { forgetSessionOwnership, releaseOwnerAttachment } from "./session-router-attachments.js";
|
|
4
3
|
import { code, error } from "./session-router-errors.js";
|
|
5
4
|
import { stopSweep } from "./session-router-occupancy.js";
|
|
5
|
+
import { clearRetainedRecord, recordRetainedSession } from "./session-router-residency.js";
|
|
6
|
+
/** How long a stop lets a cut turn settle before its teardown proceeds anyway: the default close grace. */
|
|
7
|
+
const HOST_SHUTDOWN_ABORT_BOUND_MS = 10_000;
|
|
8
|
+
/** Ends the running turn as a host shutdown (senpi#6190), bounded so a run its abort cannot release never holds the stop. */
|
|
9
|
+
async function abortForHostShutdownWithin(sessionId, entry) {
|
|
10
|
+
// Async wrapper: a session that throws synchronously still only fails its own abort, never the whole stop.
|
|
11
|
+
const aborting = (async () => {
|
|
12
|
+
await entry?.runtime?.session.abortForHostShutdown();
|
|
13
|
+
})().catch((cause) => {
|
|
14
|
+
process.stderr.write(`senpi rpc host shutdown abort ${sessionId} failed: ${String(cause)}\n`);
|
|
15
|
+
});
|
|
16
|
+
let timer;
|
|
17
|
+
const bound = new Promise((resolve) => {
|
|
18
|
+
timer = setTimeout(resolve, HOST_SHUTDOWN_ABORT_BOUND_MS);
|
|
19
|
+
timer.unref?.();
|
|
20
|
+
});
|
|
21
|
+
try {
|
|
22
|
+
await Promise.race([aborting, bound]);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
6
28
|
/**
|
|
7
29
|
* Releases one attachment (or every attachment when draining) and reports
|
|
8
30
|
* whether this caller became the finalizer. The join token is installed inside
|
|
@@ -66,10 +88,10 @@ export async function close(host, command) {
|
|
|
66
88
|
releaseOwnerAttachment(host, owner, command.sessionId);
|
|
67
89
|
if (claim.finalizer) {
|
|
68
90
|
// An explicit close ends the session for good: drop any handover-parked record entry, or
|
|
69
|
-
// the session would come back on the next host start (MEDIUM-1, senpi#3282 review).
|
|
70
|
-
|
|
71
|
-
if (host.handoverParkedDir !== undefined
|
|
72
|
-
await
|
|
91
|
+
// the session would come back on the next host start (MEDIUM-1, senpi#3282 review). Awaited only when a record
|
|
92
|
+
// exists, so a host without one starts the teardown in the same tick as the claim.
|
|
93
|
+
if (host.handoverParkedDir !== undefined)
|
|
94
|
+
await clearRetainedRecord(host, host.registry.peek(command.sessionId));
|
|
73
95
|
await finalizeClose(host, command.sessionId, host.bindings.get(command.sessionId), () => reply.complete(true, "client_close"));
|
|
74
96
|
}
|
|
75
97
|
else {
|
|
@@ -189,11 +211,26 @@ export async function dispose(host) {
|
|
|
189
211
|
if (host.admissions)
|
|
190
212
|
await host.admissions.dispose();
|
|
191
213
|
await Promise.all([...host.bindings.entries()].map(async ([sessionId, binding]) => {
|
|
214
|
+
const entry = host.registry.peek(sessionId);
|
|
215
|
+
// A retained session outlives this process: it parks under the transcript it is on now (its record stays
|
|
216
|
+
// for the next host on the endpoint, which reopens it) instead of closing, so attached clients do not read
|
|
217
|
+
// it as ended.
|
|
218
|
+
const parkedPath = host.handoverParkedDir !== undefined && entry?.retainOnDisconnect === true
|
|
219
|
+
? (entry.runtime?.session.sessionFile ?? entry.sessionPath)
|
|
220
|
+
: undefined;
|
|
221
|
+
// Claimed before any await, like close(): the stop fences the session in the tick it starts.
|
|
192
222
|
const claim = tryClaimClose(host, sessionId, { drainAttachments: true });
|
|
193
223
|
if (!claim)
|
|
194
224
|
return;
|
|
195
225
|
if (claim.finalizer) {
|
|
196
|
-
await
|
|
226
|
+
await abortForHostShutdownWithin(sessionId, entry);
|
|
227
|
+
if (entry !== undefined && parkedPath !== undefined)
|
|
228
|
+
await recordRetainedSession(host, entry, parkedPath, false).catch((cause) => {
|
|
229
|
+
process.stderr.write(`senpi rpc host shutdown park record ${sessionId} failed: ${String(cause)}\n`);
|
|
230
|
+
});
|
|
231
|
+
await finalizeClose(host, sessionId, binding, () => parkedPath !== undefined
|
|
232
|
+
? host.writer.parkSession(sessionId, parkedPath, "host_shutdown")
|
|
233
|
+
: host.writer.closeSession(sessionId, { type: "response", command: "close_session", success: true, data: {} }, "host_shutdown"));
|
|
197
234
|
}
|
|
198
235
|
else
|
|
199
236
|
await host.finalizations.get(sessionId)?.promise;
|
|
@@ -10,6 +10,7 @@ import { SessionAttachOrder } from "./session-attach-order.js";
|
|
|
10
10
|
import { tearDownReleased } from "./session-router-close.js";
|
|
11
11
|
import { routingIdOf } from "./session-router-errors.js";
|
|
12
12
|
import { sweepIdleSessions } from "./session-router-occupancy.js";
|
|
13
|
+
import { followRetainedRecord } from "./session-router-residency.js";
|
|
13
14
|
import { registryStatusSource } from "./session-status-source.js";
|
|
14
15
|
export class RouterContext {
|
|
15
16
|
constructor(registry, writer, defaults, createBinding = createRpcSessionBinding, connectionOptions = {}, idle) {
|
|
@@ -74,6 +75,7 @@ export class RouterContext {
|
|
|
74
75
|
: { effectId: latest.effectId, kind: latest.kind, state: latest.state };
|
|
75
76
|
});
|
|
76
77
|
writer.afterRecord = (sessionId) => {
|
|
78
|
+
followRetainedRecord(this, sessionId);
|
|
77
79
|
const session = this.registry.peek(sessionId)?.runtime?.session;
|
|
78
80
|
if (session === undefined)
|
|
79
81
|
return;
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { recordHandoverParkedSession } from "./host-handover-parked.js";
|
|
2
|
-
import { parkedEntryFromProfile } from "./host-handover-parked-open.js";
|
|
3
|
-
import { hostInstanceId } from "./protocol-identity.js";
|
|
4
1
|
import { readDrainVerdicts } from "./session-drain.js";
|
|
5
2
|
import { holdUnattendedTurns } from "./session-drain-hold.js";
|
|
6
3
|
import { forgetSessionOwnership } from "./session-router-attachments.js";
|
|
7
4
|
import { finalizeClose, tryClaimClose } from "./session-router-close.js";
|
|
5
|
+
import { recordRetainedSession } from "./session-router-residency.js";
|
|
8
6
|
import { selectSweepEvictions } from "./session-sweep.js";
|
|
9
7
|
const DRAIN_SWEEP_MS = 50;
|
|
10
8
|
/**
|
|
@@ -128,14 +126,7 @@ export async function evictIdleSession(host, sessionId, reason) {
|
|
|
128
126
|
parkedPath !== undefined &&
|
|
129
127
|
retained?.retainOnDisconnect === true &&
|
|
130
128
|
retained.sessionPath !== undefined) {
|
|
131
|
-
await
|
|
132
|
-
...parkedEntryFromProfile({
|
|
133
|
-
sessionPath: retained.sessionPath,
|
|
134
|
-
contextOverlay: retained.contextOverlay,
|
|
135
|
-
...(retained.durableSessionId !== undefined ? { durableSessionId: retained.durableSessionId } : {}),
|
|
136
|
-
}, retained.profile, Object.keys(host.defaults.hostContext ?? {})),
|
|
137
|
-
parkedBy: host.defaults.hostContext?.host_instance ?? hostInstanceId(),
|
|
138
|
-
}).catch((cause) => {
|
|
129
|
+
await recordRetainedSession(host, retained, retained.sessionPath, false).catch((cause) => {
|
|
139
130
|
process.stderr.write(`senpi rpc handover park record ${sessionId} failed: ${String(cause)}\n`);
|
|
140
131
|
});
|
|
141
132
|
}
|
|
@@ -70,6 +70,10 @@ export function validatedOpenProfile(host, command, options) {
|
|
|
70
70
|
: { host_socket: host.defaults.hostContext.host_socket }),
|
|
71
71
|
},
|
|
72
72
|
permissionPreset: command.permissionPreset ?? defaults?.permissionPreset,
|
|
73
|
+
// An embedder's host-level preset is the host default, never a client's choice (remote approval-mode rule).
|
|
74
|
+
...(command.permissionPreset === undefined && defaults?.permissionPreset !== undefined
|
|
75
|
+
? { permissionPresetSource: "host_default" }
|
|
76
|
+
: {}),
|
|
73
77
|
creationModel: command.provider && command.modelId
|
|
74
78
|
? { provider: command.provider, modelId: command.modelId }
|
|
75
79
|
: defaults?.creationModel,
|