@code-yeongyu/senpi 2026.11.10-1 → 2026.11.10-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 +20 -0
- package/dist/bundle/chunks/{chunk-GPXZTSEX.js → chunk-2M54EAAY.js} +3 -3
- package/dist/bundle/chunks/{chunk-YPAQXRP3.js → chunk-47T6YMI2.js} +2 -2
- package/dist/bundle/chunks/{chunk-I5ASHSLZ.js → chunk-4JMGWVRH.js} +1 -1
- package/dist/bundle/chunks/{chunk-HI5JSLG5.js → chunk-74IB3JPP.js} +2 -2
- package/dist/bundle/chunks/{chunk-NH7SYIGN.js → chunk-DLPV4B3R.js} +1 -1
- package/dist/bundle/chunks/{chunk-Y7Z7XPUF.js → chunk-DZQA5LVN.js} +1 -1
- package/dist/bundle/chunks/{chunk-2CNSCMYC.js → chunk-E5O4SP5I.js} +1 -1
- package/dist/bundle/chunks/{chunk-3IYWH6RO.js → chunk-IKJCA5SH.js} +1 -1
- package/dist/bundle/chunks/{chunk-H33A33M4.js → chunk-M2VJVJMK.js} +1 -1
- package/dist/bundle/chunks/{chunk-L6NRDUWJ.js → chunk-OF3XVGG4.js} +1 -1
- package/dist/bundle/chunks/{chunk-THYEZBUG.js → chunk-Q4G5DECT.js} +2 -2
- package/dist/bundle/chunks/{chunk-ARUV3US4.js → chunk-QATU6BBC.js} +1 -1
- package/dist/bundle/chunks/{chunk-FAFIGBUD.js → chunk-TUEZNIBK.js} +1 -1
- package/dist/bundle/chunks/{chunk-BELNOPAI.js → chunk-UOZDDYBN.js} +2 -2
- package/dist/bundle/chunks/{chunk-DBQBJTMF.js → chunk-VUY4BZDI.js} +1 -1
- package/dist/bundle/chunks/{chunk-TGW4R7SI.js → chunk-XH24X5B2.js} +1 -1
- package/dist/bundle/chunks/chunk-XI6THO4F.js +2 -0
- package/dist/bundle/chunks/{chunk-X4XWAB3J.js → chunk-YCPURLPU.js} +38 -38
- package/dist/bundle/chunks/{chunk-LHEP3ACI.js → chunk-YNGEZBTP.js} +1 -1
- package/dist/bundle/chunks/{cli-main-VG3C2EXI.js → cli-main-Y6ALA5OI.js} +1 -1
- package/dist/bundle/chunks/{hand-entry-3LQKUTYS.js → hand-entry-IPDAIOGI.js} +1 -1
- package/dist/bundle/chunks/{host-command-CCASS7W4.js → host-command-2326GCUP.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-WWC7HXQ6.js → host-lifecycle-TNHASSFJ.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +1 -1
- package/dist/bundle/chunks/{host-session-command-YJB2FLKU.js → host-session-command-BZYBDOIE.js} +1 -1
- package/dist/bundle/chunks/{host-socket-request-DMYXC56U.js → host-socket-request-QN4Q2P5E.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-CUN3WMUF.js → interactive-mode-LBQB7DDR.js} +1 -1
- package/dist/bundle/chunks/{main-P7XRC2OI.js → main-FFRFP34W.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-244X3P6L.js → multi-session-host-NDIHHJ2L.js} +11 -8
- package/dist/bundle/chunks/{package-manager-cli-UVWJOQA3.js → package-manager-cli-XCPAMOYI.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-G4I3HHPB.js → rpc-mode-TKNZM4PZ.js} +1 -1
- package/dist/bundle/chunks/{rpc-monitor-source-C6VQ3BC4.js → rpc-monitor-source-PGTDSGWP.js} +1 -1
- package/dist/bundle/chunks/{run-attached-tui-R3RB5P7G.js → run-attached-tui-JDUKXDI2.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-5FG2OKSR.js → schedule-command-HF2DHABB.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-XWCFLJYC.js → session-control-endpoint-A6R6W2JQ.js} +1 -1
- package/dist/bundle/chunks/{session-picker-2FZSNFS7.js → session-picker-GWA4S2PO.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +119 -116
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/host-supervisor.js +1 -1
- 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 +5 -0
- package/dist/core/agent-session.d.ts +1 -1
- package/dist/core/agent-session.js +30 -10
- package/dist/core/extensions/builtin/config-reload/change-groups.js +6 -2
- package/dist/core/extensions/builtin/mcp/auth/catalog-identity.js +32 -8
- package/dist/core/extensions/builtin/mcp/auth/oauth-grant.d.ts +43 -0
- package/dist/core/extensions/builtin/mcp/auth/oauth-grant.js +196 -0
- package/dist/core/extensions/builtin/mcp/auth/oauth-provider.d.ts +4 -17
- package/dist/core/extensions/builtin/mcp/auth/oauth-provider.js +64 -75
- package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.d.ts +1 -1
- package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.js +25 -4
- package/dist/core/extensions/builtin/mcp/auth/oauth.js +10 -7
- package/dist/core/extensions/builtin/mcp/auth/token-store.d.ts +18 -0
- package/dist/core/extensions/builtin/mcp/auth/token-store.js +3 -0
- package/dist/core/extensions/builtin/mcp/catalog-cache.js +7 -6
- package/dist/core/extensions/builtin/mcp/catalog.js +3 -2
- package/dist/core/extensions/builtin/mcp/invocation.js +6 -0
- package/dist/core/extensions/builtin/mcp/service-register.js +16 -2
- package/dist/core/extensions/builtin/mcp/service-tools-changed.js +61 -54
- package/dist/core/extensions/builtin/mcp/startup-race.d.ts +2 -0
- package/dist/core/extensions/builtin/mcp/startup-race.js +25 -7
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-2.js +6 -6
- package/dist/core/open-timing.d.ts +2 -0
- package/dist/core/open-timing.js +7 -1
- package/dist/core/session-manager.d.ts +2 -0
- package/dist/core/session-manager.js +15 -1
- package/dist/modes/interactive/session-control-rpc-commands.d.ts +1 -1
- package/dist/modes/interactive/session-control-rpc-commands.js +5 -0
- package/dist/modes/rpc/compact-admission-validation.js +4 -1
- package/dist/modes/rpc/connection-handler.d.ts +2 -0
- package/dist/modes/rpc/connection-handler.js +10 -2
- package/dist/modes/rpc/custom-capability.d.ts +2 -0
- package/dist/modes/rpc/custom-capability.js +2 -0
- package/dist/modes/rpc/effect-commands.d.ts +21 -0
- package/dist/modes/rpc/host-capabilities.d.ts +1 -0
- package/dist/modes/rpc/host-capabilities.js +3 -1
- package/dist/modes/rpc/host-observe-request.d.ts +2 -0
- package/dist/modes/rpc/host-observe-request.js +6 -1
- package/dist/modes/rpc/host-park-ledger.d.ts +19 -0
- package/dist/modes/rpc/host-park-ledger.js +110 -0
- package/dist/modes/rpc/lifecycle-delivery.js +4 -1
- package/dist/modes/rpc/media-placeholders.js +1 -0
- package/dist/modes/rpc/method-classes-schema-host.d.ts +42 -1
- package/dist/modes/rpc/method-classes-schema-host.js +24 -21
- package/dist/modes/rpc/method-classes-schema.d.ts +42 -1
- package/dist/modes/rpc/method-classes-schema.js +5 -2
- package/dist/modes/rpc/method-classes.d.ts +2 -0
- package/dist/modes/rpc/method-classes.js +3 -0
- package/dist/modes/rpc/multi-session-host.js +1 -1
- package/dist/modes/rpc/rpc-host-command-types.d.ts +14 -1
- package/dist/modes/rpc/rpc-host-response-types.d.ts +21 -0
- package/dist/modes/rpc/session-attachment-gate.js +2 -0
- package/dist/modes/rpc/session-binding.d.ts +1 -0
- package/dist/modes/rpc/session-binding.js +6 -1
- package/dist/modes/rpc/session-connection-context.d.ts +2 -0
- package/dist/modes/rpc/session-connection-context.js +4 -0
- package/dist/modes/rpc/session-registry-attach.d.ts +2 -0
- package/dist/modes/rpc/session-registry-attach.js +15 -11
- package/dist/modes/rpc/session-registry-claims.js +2 -0
- package/dist/modes/rpc/session-registry-open.js +13 -7
- package/dist/modes/rpc/session-registry-opening.js +2 -1
- package/dist/modes/rpc/session-registry-types.d.ts +4 -0
- package/dist/modes/rpc/session-registry.d.ts +3 -0
- package/dist/modes/rpc/session-registry.js +27 -3
- package/dist/modes/rpc/session-router-close.js +1 -0
- package/dist/modes/rpc/session-router-context.d.ts +4 -2
- package/dist/modes/rpc/session-router-context.js +3 -0
- package/dist/modes/rpc/session-router-host-reads.js +1 -0
- package/dist/modes/rpc/session-router-occupancy.js +10 -1
- package/dist/modes/rpc/session-router-open-result.d.ts +12 -0
- package/dist/modes/rpc/session-router-open-result.js +63 -0
- package/dist/modes/rpc/session-router-open.d.ts +3 -0
- package/dist/modes/rpc/session-router-open.js +15 -63
- package/dist/modes/rpc/session-router-park.d.ts +15 -0
- package/dist/modes/rpc/session-router-park.js +176 -0
- package/dist/modes/rpc/session-router-routing.d.ts +1 -1
- package/dist/modes/rpc/session-router-routing.js +7 -0
- package/dist/modes/rpc/session-status-source.js +2 -2
- package/docs/engine-socket/fixtures/admission-routes/h-legacy.json +1 -1
- package/docs/engine-socket/fixtures/rpc-inbound-samples.json +22 -2
- package/docs/rpc.md +50 -0
- 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-436DNN4Z.js +0 -2
|
@@ -24,6 +24,8 @@ export type SessionRuntime = AgentSessionRuntime;
|
|
|
24
24
|
export type RpcSessionState = "opening" | "open" | "closing" | "quarantined" | "closed";
|
|
25
25
|
export interface RpcSessionEntry {
|
|
26
26
|
state: RpcSessionState;
|
|
27
|
+
/** Built speculatively: no public identity, attachment, listing, or disk persistence until claimed. */
|
|
28
|
+
speculative?: boolean;
|
|
27
29
|
runtime?: SessionRuntime;
|
|
28
30
|
worker?: SessionWorkerClient;
|
|
29
31
|
/** The in-process session's ordered acceptance lane, present only while the compact-updates flag is on. */
|
|
@@ -116,6 +118,8 @@ export interface RpcSessionRegistryOptions {
|
|
|
116
118
|
}
|
|
117
119
|
/** Host-side lifecycle policy for one `open_session`, distinct from the session's launch profile. */
|
|
118
120
|
export interface RpcSessionOpenOptions {
|
|
121
|
+
/** Router-owned park construction; never accepted directly from the wire. */
|
|
122
|
+
speculative?: boolean;
|
|
119
123
|
/** Refuse unless the registry already owns the path; never prepare or construct a writer. */
|
|
120
124
|
attachOnly?: boolean;
|
|
121
125
|
/** Keep the session alive when its last client disconnects (`open_session.retain_on_disconnect`). */
|
|
@@ -4,6 +4,7 @@ export { frozenProfile, type OpenRpcSession, type RpcSessionEntry, type RpcSessi
|
|
|
4
4
|
/** Process-local lifecycle owner for multi-session RPC runtimes. */
|
|
5
5
|
export declare class RpcSessionRegistry {
|
|
6
6
|
readonly supportsCompact: (profile?: RpcSessionLaunchProfile) => boolean;
|
|
7
|
+
readonly supportsParkClaim = true;
|
|
7
8
|
private readonly entries;
|
|
8
9
|
private readonly reservations;
|
|
9
10
|
private readonly pendingClaims;
|
|
@@ -20,6 +21,8 @@ export declare class RpcSessionRegistry {
|
|
|
20
21
|
constructor(options: RpcSessionRegistryOptions);
|
|
21
22
|
/** Number of live entries, including ones still opening or closing. */
|
|
22
23
|
get size(): number;
|
|
24
|
+
/** Commit the ledger's exclusive claim after its existing runtime has acquired a persistent file. */
|
|
25
|
+
claimPark(handle: string, retainOnDisconnect: boolean): Promise<OpenRpcSession>;
|
|
23
26
|
/** Daemon-family identities come from the endpoint's claims and generation registry. */
|
|
24
27
|
holderPids(observedStarts?: ReadonlyMap<number, number | undefined>): Promise<readonly number[]>;
|
|
25
28
|
openSession(requested: RpcSessionLaunchProfile, options?: RpcSessionOpenOptions): Promise<OpenRpcSession>;
|
|
@@ -16,6 +16,7 @@ export class RpcSessionRegistry {
|
|
|
16
16
|
return this.options.agentDir;
|
|
17
17
|
}
|
|
18
18
|
constructor(options) {
|
|
19
|
+
this.supportsParkClaim = true;
|
|
19
20
|
this.entries = new Map();
|
|
20
21
|
this.reservations = new Set();
|
|
21
22
|
this.pendingClaims = new Map();
|
|
@@ -42,8 +43,10 @@ export class RpcSessionRegistry {
|
|
|
42
43
|
closeGraceMs: this.closeGraceMs,
|
|
43
44
|
get: (handle) => this.entries.get(handle),
|
|
44
45
|
delete: (handle) => {
|
|
46
|
+
const speculative = this.entries.get(handle)?.speculative;
|
|
45
47
|
const deleted = this.entries.delete(handle);
|
|
46
|
-
|
|
48
|
+
if (!speculative)
|
|
49
|
+
this.options.onSizeChange?.(this.size);
|
|
47
50
|
return deleted;
|
|
48
51
|
},
|
|
49
52
|
releaseReservation: async (key) => {
|
|
@@ -66,7 +69,26 @@ export class RpcSessionRegistry {
|
|
|
66
69
|
}
|
|
67
70
|
/** Number of live entries, including ones still opening or closing. */
|
|
68
71
|
get size() {
|
|
69
|
-
return this.entries.
|
|
72
|
+
return [...this.entries.values()].filter((entry) => !entry.speculative).length;
|
|
73
|
+
}
|
|
74
|
+
/** Commit the ledger's exclusive claim after its existing runtime has acquired a persistent file. */
|
|
75
|
+
async claimPark(handle, retainOnDisconnect) {
|
|
76
|
+
const entry = this.entries.get(handle);
|
|
77
|
+
if (!entry?.speculative || entry.state !== "open" || !entry.runtime)
|
|
78
|
+
throw new RpcSessionRegistryError("unknown_session");
|
|
79
|
+
await entry.runtime.persistSession();
|
|
80
|
+
// No await between the duplicate decision and publication: an ordinary open may have
|
|
81
|
+
// reserved this identity while persistence was being enabled.
|
|
82
|
+
for (const other of this.entries.values())
|
|
83
|
+
if (!other.speculative && other.durableSessionId === entry.runtime.session.sessionId)
|
|
84
|
+
throw new RpcSessionRegistryError("session_id_in_use");
|
|
85
|
+
entry.speculative = false;
|
|
86
|
+
entry.attachments = 1;
|
|
87
|
+
entry.retainOnDisconnect = retainOnDisconnect;
|
|
88
|
+
entry.lastCommandAt = this.now();
|
|
89
|
+
this.syncRuntimeMetadata();
|
|
90
|
+
this.options.onSizeChange?.(this.size);
|
|
91
|
+
return { sessionId: handle, durableSessionId: entry.runtime.session.sessionId, sessionPath: entry.sessionPath };
|
|
70
92
|
}
|
|
71
93
|
/** Daemon-family identities come from the endpoint's claims and generation registry. */
|
|
72
94
|
holderPids(observedStarts) {
|
|
@@ -143,7 +165,9 @@ export class RpcSessionRegistry {
|
|
|
143
165
|
list() {
|
|
144
166
|
this.syncRuntimeMetadata();
|
|
145
167
|
const now = this.now();
|
|
146
|
-
return [...this.entries]
|
|
168
|
+
return [...this.entries]
|
|
169
|
+
.filter(([, entry]) => !entry.speculative)
|
|
170
|
+
.map(([sessionId, entry]) => ({
|
|
147
171
|
sessionId,
|
|
148
172
|
durableSessionId: entry.durableSessionId,
|
|
149
173
|
sessionPath: entry.sessionPath,
|
|
@@ -208,6 +208,7 @@ export async function dispose(host) {
|
|
|
208
208
|
stopSweep(host);
|
|
209
209
|
if (host.drainTimer !== undefined)
|
|
210
210
|
clearInterval(host.drainTimer);
|
|
211
|
+
await host.parks.stop();
|
|
211
212
|
if (host.admissions)
|
|
212
213
|
await host.admissions.dispose();
|
|
213
214
|
await Promise.all([...host.bindings.entries()].map(async ([sessionId, binding]) => {
|
|
@@ -6,7 +6,9 @@ import type { SessionEventWriter } from "./session-event-writer.ts";
|
|
|
6
6
|
import type { RpcSessionRegistry } from "./session-registry.ts";
|
|
7
7
|
import { SessionAttachOrder } from "./session-attach-order.ts";
|
|
8
8
|
import type { RpcHostSessionDefaults, RpcSessionIdlePolicy } from "./session-router-types.ts";
|
|
9
|
+
import { HostParkLedger } from "./host-park-ledger.ts";
|
|
9
10
|
export declare class RouterContext {
|
|
11
|
+
readonly parks: HostParkLedger;
|
|
10
12
|
readonly admissions: CompactAdmissions | undefined;
|
|
11
13
|
readonly bindings: Map<string, RpcSessionBinding>;
|
|
12
14
|
readonly stopLimitBroadcast: () => void;
|
|
@@ -18,7 +20,7 @@ export declare class RouterContext {
|
|
|
18
20
|
readonly opensByConnection: Map<string, Set<Promise<void>>>;
|
|
19
21
|
/** Connections whose socket has already been detached. */
|
|
20
22
|
readonly releasedConnections: Set<string>;
|
|
21
|
-
readonly registry: Pick<RpcSessionRegistry, "openSession" | "peek" | "getForCommand" | "beginClose" | "close" | "closeMarked" | "list" | "size" | "warm" | "closeGraceMs"> & Partial<Pick<RpcSessionRegistry, "supportsCompact" | "holderPids" | "attachSession" | "agentDir">>;
|
|
23
|
+
readonly registry: Pick<RpcSessionRegistry, "openSession" | "peek" | "getForCommand" | "beginClose" | "close" | "closeMarked" | "list" | "size" | "warm" | "closeGraceMs"> & Partial<Pick<RpcSessionRegistry, "supportsCompact" | "supportsParkClaim" | "claimPark" | "holderPids" | "attachSession" | "agentDir">>;
|
|
22
24
|
readonly writer: SessionEventWriter;
|
|
23
25
|
readonly defaults: RpcHostSessionDefaults;
|
|
24
26
|
readonly createBinding: typeof createRpcSessionBinding;
|
|
@@ -59,6 +61,6 @@ export declare class RouterContext {
|
|
|
59
61
|
readonly mainHeapBytes: () => number;
|
|
60
62
|
readonly kernels: () => readonly RpcHostKernelMemory[];
|
|
61
63
|
} | undefined;
|
|
62
|
-
constructor(registry: Pick<RpcSessionRegistry, "openSession" | "peek" | "getForCommand" | "beginClose" | "close" | "closeMarked" | "list" | "size" | "warm" | "closeGraceMs"> & Partial<Pick<RpcSessionRegistry, "supportsCompact" | "holderPids" | "attachSession" | "agentDir">>, writer: SessionEventWriter, defaults: RpcHostSessionDefaults, createBinding?: typeof createRpcSessionBinding, connectionOptions?: Parameters<typeof createRpcSessionBinding>[4], idle?: RpcSessionIdlePolicy);
|
|
64
|
+
constructor(registry: Pick<RpcSessionRegistry, "openSession" | "peek" | "getForCommand" | "beginClose" | "close" | "closeMarked" | "list" | "size" | "warm" | "closeGraceMs"> & Partial<Pick<RpcSessionRegistry, "supportsCompact" | "supportsParkClaim" | "claimPark" | "holderPids" | "attachSession" | "agentDir">>, writer: SessionEventWriter, defaults: RpcHostSessionDefaults, createBinding?: typeof createRpcSessionBinding, connectionOptions?: Parameters<typeof createRpcSessionBinding>[4], idle?: RpcSessionIdlePolicy);
|
|
63
65
|
}
|
|
64
66
|
//# sourceMappingURL=session-router-context.d.ts.map
|
|
@@ -12,8 +12,11 @@ import { routingIdOf } from "./session-router-errors.js";
|
|
|
12
12
|
import { sweepIdleSessions } from "./session-router-occupancy.js";
|
|
13
13
|
import { followRetainedRecord } from "./session-router-residency.js";
|
|
14
14
|
import { registryStatusSource } from "./session-status-source.js";
|
|
15
|
+
import { HostParkLedger } from "./host-park-ledger.js";
|
|
16
|
+
import { closePark } from "./session-router-park.js";
|
|
15
17
|
export class RouterContext {
|
|
16
18
|
constructor(registry, writer, defaults, createBinding = createRpcSessionBinding, connectionOptions = {}, idle) {
|
|
19
|
+
this.parks = new HostParkLedger((opened) => closePark(this, opened));
|
|
17
20
|
this.admissions = compactUpdatesEnabled()
|
|
18
21
|
? new CompactAdmissions(ADMISSION_LIMITS, (socketEpoch, admissionId) => this.writer.routesAdmission(socketEpoch, admissionId))
|
|
19
22
|
: undefined;
|
|
@@ -14,6 +14,7 @@ import { sessionMemory } from "./session-router-observation.js";
|
|
|
14
14
|
export function answerProtocolInfo(host, command) {
|
|
15
15
|
const hostCapabilities = multiSessionHostCapabilities({
|
|
16
16
|
warm: host.registry.warm !== undefined,
|
|
17
|
+
parkClaim: host.registry.supportsParkClaim === true,
|
|
17
18
|
toolRuntime: host.defaults.toolRuntime ?? "in-process",
|
|
18
19
|
negotiated: host.connectionOptions?.capabilities ?? [],
|
|
19
20
|
mcpPresenters: host.writer.mcpPresenters !== undefined,
|
|
@@ -12,6 +12,15 @@ const DRAIN_SWEEP_MS = 50;
|
|
|
12
12
|
*/
|
|
13
13
|
export function beginDrain(host) {
|
|
14
14
|
host.draining = true;
|
|
15
|
+
void host.parks
|
|
16
|
+
.stop()
|
|
17
|
+
.catch((cause) => {
|
|
18
|
+
process.stderr.write(`senpi rpc park close failed during drain: ${String(cause)}\n`);
|
|
19
|
+
})
|
|
20
|
+
.then(() => sweepDrain(host))
|
|
21
|
+
.catch((cause) => {
|
|
22
|
+
process.stderr.write(`senpi rpc drain sweep failed after park close: ${String(cause)}\n`);
|
|
23
|
+
});
|
|
15
24
|
stopSweep(host);
|
|
16
25
|
// Armed BEFORE the first pass, so a pass that fails is retried instead of ending the drain
|
|
17
26
|
// (senpi#2285). Unref'd: a draining host must never be the reason the event loop stays open.
|
|
@@ -35,7 +44,7 @@ export function sweepDrain(host) {
|
|
|
35
44
|
parkForHandoff(host, sessionId, "session_dir_removed");
|
|
36
45
|
for (const sessionId of verdicts.park)
|
|
37
46
|
parkForHandoff(host, sessionId, "handoff_parked");
|
|
38
|
-
if (host.registry.size > 0 || host.handoffParks > 0 || host.finalizations.size > 0)
|
|
47
|
+
if (host.registry.size > 0 || host.parks.size > 0 || host.handoffParks > 0 || host.finalizations.size > 0)
|
|
39
48
|
return;
|
|
40
49
|
if (host.drainTimer !== undefined) {
|
|
41
50
|
clearInterval(host.drainTimer);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { OpenRpcSession, RpcSessionEntry } from "./session-registry.ts";
|
|
2
|
+
import type { RouterContext } from "./session-router-context.ts";
|
|
3
|
+
/** A dropped opener waits until its open or claim has either transferred or released ownership. */
|
|
4
|
+
export declare function holdOpenConnection(host: RouterContext, owner: string): () => void;
|
|
5
|
+
/** One ownership transition for both a freshly built open and a claimed speculative open. */
|
|
6
|
+
export declare function ownOpen(host: RouterContext, opened: OpenRpcSession, owner: string | undefined, compact?: boolean): void;
|
|
7
|
+
export declare function publishOpen(host: RouterContext, opened: OpenRpcSession, entry: RpcSessionEntry): void;
|
|
8
|
+
export declare function replyOpen(host: RouterContext, opened: OpenRpcSession, entry: RpcSessionEntry, command: {
|
|
9
|
+
readonly type: "open_session" | "claim_session";
|
|
10
|
+
readonly id?: string;
|
|
11
|
+
}, reply?: boolean): void;
|
|
12
|
+
//# sourceMappingURL=session-router-open-result.d.ts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { openReplyPermissionMode } from "../../core/permission-mode.js";
|
|
2
|
+
import { buildRpcSessionState } from "./connection-handler.js";
|
|
3
|
+
/** A dropped opener waits until its open or claim has either transferred or released ownership. */
|
|
4
|
+
export function holdOpenConnection(host, owner) {
|
|
5
|
+
const barrier = Promise.withResolvers();
|
|
6
|
+
const opens = host.opensByConnection.get(owner) ?? new Set();
|
|
7
|
+
opens.add(barrier.promise);
|
|
8
|
+
host.opensByConnection.set(owner, opens);
|
|
9
|
+
return () => {
|
|
10
|
+
barrier.resolve();
|
|
11
|
+
opens.delete(barrier.promise);
|
|
12
|
+
if (opens.size === 0)
|
|
13
|
+
host.opensByConnection.delete(owner);
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** One ownership transition for both a freshly built open and a claimed speculative open. */
|
|
17
|
+
export function ownOpen(host, opened, owner, compact = false) {
|
|
18
|
+
if (owner === undefined)
|
|
19
|
+
return;
|
|
20
|
+
if (!host.writer.hasRegisteredConnectionCapabilities(owner))
|
|
21
|
+
host.writer.setConnectionCapabilities(owner, host.pendingCapabilities.get(owner) ?? host.connectionOptions?.capabilities ?? []);
|
|
22
|
+
host.writer.attachConnectionToSession(owner, opened.sessionId, compact);
|
|
23
|
+
const owned = host.sessionsByConnection.get(owner) ?? new Map();
|
|
24
|
+
owned.set(opened.sessionId, (owned.get(opened.sessionId) ?? 0) + 1);
|
|
25
|
+
host.sessionsByConnection.set(owner, owned);
|
|
26
|
+
host.attachOrder.attached(opened.sessionId, owner);
|
|
27
|
+
}
|
|
28
|
+
export function publishOpen(host, opened, entry) {
|
|
29
|
+
if (opened.attached)
|
|
30
|
+
host.writer.status.publish(opened.sessionId, "attach", "client");
|
|
31
|
+
else {
|
|
32
|
+
host.writer.publishSessionOpened({
|
|
33
|
+
type: "session_opened",
|
|
34
|
+
sessionId: opened.sessionId,
|
|
35
|
+
durableSessionId: opened.durableSessionId,
|
|
36
|
+
...(opened.sessionPath !== undefined ? { sessionPath: opened.sessionPath } : {}),
|
|
37
|
+
kind: entry.kind,
|
|
38
|
+
cwd: entry.cwd,
|
|
39
|
+
context: entry.context,
|
|
40
|
+
});
|
|
41
|
+
host.writer.status.publish(opened.sessionId, "open", "client");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function replyOpen(host, opened, entry, command, reply = true) {
|
|
45
|
+
const state = entry.worker?.snapshot?.state ?? (entry.runtime ? buildRpcSessionState(entry.runtime.session) : undefined);
|
|
46
|
+
if (!state)
|
|
47
|
+
throw new Error("Session state was not created");
|
|
48
|
+
if (reply)
|
|
49
|
+
host.writer.enqueue(opened.sessionId, {
|
|
50
|
+
id: command.id,
|
|
51
|
+
type: "response",
|
|
52
|
+
command: command.type,
|
|
53
|
+
success: true,
|
|
54
|
+
data: {
|
|
55
|
+
sessionId: opened.sessionId,
|
|
56
|
+
state,
|
|
57
|
+
...openReplyPermissionMode(state),
|
|
58
|
+
...(opened.attached ? { attached: true } : {}),
|
|
59
|
+
...(host.defaults.sessionPresence ? { presence: host.defaults.sessionPresence.view(state.sessionId) } : {}),
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=session-router-open-result.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type OpenAcceptance } from "./compact-open-admission.ts";
|
|
2
2
|
import type { HandoverParkedEntry } from "./host-handover-parked.ts";
|
|
3
3
|
import type { RpcCommand, RpcResponse } from "./rpc-types.ts";
|
|
4
|
+
import type { OpenRpcSession } from "./session-registry.ts";
|
|
4
5
|
import type { RouterContext } from "./session-router-context.ts";
|
|
5
6
|
export declare function openWithBarrier(host: RouterContext, command: Extract<RpcCommand, {
|
|
6
7
|
type: "open_session";
|
|
@@ -19,5 +20,7 @@ export declare function open(host: RouterContext, command: Extract<RpcCommand, {
|
|
|
19
20
|
readonly signal?: AbortSignal;
|
|
20
21
|
readonly defaults?: boolean;
|
|
21
22
|
readonly contextOverlay?: HandoverParkedEntry["contextOverlay"];
|
|
23
|
+
/** An invisible speculative open, published only when its ledger hands it to a caller. */
|
|
24
|
+
readonly park?: (opened: OpenRpcSession) => void;
|
|
22
25
|
}): Promise<RpcResponse | undefined>;
|
|
23
26
|
//# sourceMappingURL=session-router-open.d.ts.map
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { buildRpcSessionState } from "./connection-handler.js";
|
|
2
1
|
import { compactUpdatesEnabled } from "../../core/engine-socket-flag.js";
|
|
3
2
|
import { startOpenTimer } from "../../core/open-timing.js";
|
|
4
|
-
import { openReplyPermissionMode } from "../../core/permission-mode.js";
|
|
5
3
|
import { compactOpen } from "./compact-open-admission.js";
|
|
6
4
|
import { CompactAdmissionError } from "./compact-admission-error.js";
|
|
7
5
|
import { runAdmissionProducer } from "./compact-admission-producer.js";
|
|
@@ -12,6 +10,8 @@ import { close } from "./session-router-close.js";
|
|
|
12
10
|
import { code, detail, error } from "./session-router-errors.js";
|
|
13
11
|
import { sweepDrain } from "./session-router-occupancy.js";
|
|
14
12
|
import { invalidSpawnedBy, validatedOpenProfile } from "./session-router-open-profile.js";
|
|
13
|
+
import { holdOpenConnection, ownOpen, publishOpen, replyOpen } from "./session-router-open-result.js";
|
|
14
|
+
import { closePark } from "./session-router-park.js";
|
|
15
15
|
import { relocationConnection } from "./session-router-relocation.js";
|
|
16
16
|
import { syncRetainedRecord } from "./session-router-residency.js";
|
|
17
17
|
import { liveToolMediaScope, toolMediaPersister } from "./tool-media-store.js";
|
|
@@ -24,18 +24,12 @@ function openPhysicalWithBarrier(host, command, accept, signal) {
|
|
|
24
24
|
const owner = host.writer.currentConnection();
|
|
25
25
|
if (owner === undefined)
|
|
26
26
|
return open(host, command);
|
|
27
|
-
let resolveBarrier;
|
|
28
|
-
const barrier = new Promise((resolve) => {
|
|
29
|
-
resolveBarrier = resolve;
|
|
30
|
-
});
|
|
31
|
-
const opens = host.opensByConnection.get(owner) ?? new Set();
|
|
32
27
|
// Every open already accepted anywhere on this host, not just on this connection: the
|
|
33
28
|
// in-process runtime serves them on ONE loop, so the wait this caller faces is the total.
|
|
34
29
|
let inFlight = 0;
|
|
35
30
|
for (const pending of host.opensByConnection.values())
|
|
36
31
|
inFlight += pending.size;
|
|
37
|
-
|
|
38
|
-
host.opensByConnection.set(owner, opens);
|
|
32
|
+
const releaseBarrier = holdOpenConnection(host, owner);
|
|
39
33
|
// Before the open reaches the loop: a client that later hits its deadline can then say
|
|
40
34
|
// where it was queued instead of reporting a bare timeout (senpi#1844).
|
|
41
35
|
if (command.id !== undefined)
|
|
@@ -49,12 +43,7 @@ function openPhysicalWithBarrier(host, command, accept, signal) {
|
|
|
49
43
|
// A late open's physical session is released by its admission (the accept/rollback path, 4272-admission-
|
|
50
44
|
// abandonment), so the late response itself owns nothing to release here.
|
|
51
45
|
const opened = signal === undefined ? start() : runAdmissionProducer(signal, start, async () => { });
|
|
52
|
-
return opened.finally(
|
|
53
|
-
resolveBarrier();
|
|
54
|
-
opens.delete(barrier);
|
|
55
|
-
if (opens.size === 0)
|
|
56
|
-
host.opensByConnection.delete(owner);
|
|
57
|
-
});
|
|
46
|
+
return opened.finally(releaseBarrier);
|
|
58
47
|
}
|
|
59
48
|
/**
|
|
60
49
|
* The successor's reopen of a handoff-parked session: the same open as an `open_session`, with no
|
|
@@ -87,6 +76,7 @@ export async function open(host, command, owner, options) {
|
|
|
87
76
|
{
|
|
88
77
|
retainOnDisconnect: command.retain_on_disconnect === true,
|
|
89
78
|
attachOnly: command.expectedHostInstance !== undefined,
|
|
79
|
+
...(options?.park ? { speculative: true } : {}),
|
|
90
80
|
});
|
|
91
81
|
timer.mark("registry_open");
|
|
92
82
|
options?.signal?.throwIfAborted();
|
|
@@ -119,22 +109,11 @@ export async function open(host, command, owner, options) {
|
|
|
119
109
|
host.writer.registerAcceptance(openedSession.sessionId, lane);
|
|
120
110
|
if (options?.accept)
|
|
121
111
|
await options.accept(openedSession);
|
|
122
|
-
|
|
123
|
-
if (!host.writer.hasRegisteredConnectionCapabilities(owner))
|
|
124
|
-
host.writer.setConnectionCapabilities(owner, host.pendingCapabilities.get(owner) ?? host.connectionOptions?.capabilities ?? []);
|
|
125
|
-
host.writer.attachConnectionToSession(owner, openedSession.sessionId, options?.accept !== undefined);
|
|
126
|
-
}
|
|
127
|
-
// A client that dies without close_session (terminal closed, SIGKILL, dropped
|
|
128
|
-
// SSH) still holds this handle's attachment and its path reservation. Remember
|
|
129
|
-
// which connection owns it so releaseConnection() can close exactly that.
|
|
130
|
-
if (owner !== undefined) {
|
|
131
|
-
const owned = host.sessionsByConnection.get(owner) ?? new Map();
|
|
132
|
-
owned.set(openedSession.sessionId, (owned.get(openedSession.sessionId) ?? 0) + 1);
|
|
133
|
-
host.sessionsByConnection.set(owner, owned);
|
|
134
|
-
host.attachOrder.attached(openedSession.sessionId, owner);
|
|
135
|
-
}
|
|
112
|
+
ownOpen(host, openedSession, owner, options?.accept !== undefined);
|
|
136
113
|
if (!host.bindings.has(openedSession.sessionId)) {
|
|
137
|
-
host.bindings.set(openedSession.sessionId, await host.createBinding(openedSession.sessionId, entry, host.writer, () => void
|
|
114
|
+
host.bindings.set(openedSession.sessionId, await host.createBinding(openedSession.sessionId, entry, host.writer, () => void (entry.speculative
|
|
115
|
+
? closePark(host, openedSession)
|
|
116
|
+
: close(host, { type: "close_session", sessionId: openedSession.sessionId })), {
|
|
138
117
|
...host.connectionOptions,
|
|
139
118
|
capabilities: owner !== undefined
|
|
140
119
|
? (host.pendingCapabilities.get(owner) ?? host.connectionOptions?.capabilities ?? [])
|
|
@@ -181,20 +160,11 @@ export async function open(host, command, owner, options) {
|
|
|
181
160
|
});
|
|
182
161
|
}
|
|
183
162
|
options?.signal?.throwIfAborted();
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
host.writer.publishSessionOpened({
|
|
188
|
-
type: "session_opened",
|
|
189
|
-
sessionId: openedSession.sessionId,
|
|
190
|
-
durableSessionId: openedSession.durableSessionId,
|
|
191
|
-
...(openedSession.sessionPath !== undefined ? { sessionPath: openedSession.sessionPath } : {}),
|
|
192
|
-
kind: entry.kind,
|
|
193
|
-
cwd: entry.cwd,
|
|
194
|
-
context: entry.context,
|
|
195
|
-
});
|
|
196
|
-
host.writer.status.publish(openedSession.sessionId, "open", "client");
|
|
163
|
+
if (options?.park) {
|
|
164
|
+
options.park(openedSession);
|
|
165
|
+
return undefined;
|
|
197
166
|
}
|
|
167
|
+
publishOpen(host, openedSession, entry);
|
|
198
168
|
if (!options?.accept && !host.draining && owner !== undefined && host.releasedConnections.has(owner)) {
|
|
199
169
|
releaseOwnerAttachment(host, owner, openedSession.sessionId);
|
|
200
170
|
await releaseOwnedSession(host, openedSession.sessionId);
|
|
@@ -205,26 +175,8 @@ export async function open(host, command, owner, options) {
|
|
|
205
175
|
if (options?.accept) {
|
|
206
176
|
return undefined;
|
|
207
177
|
}
|
|
208
|
-
const replyState = entry.worker?.snapshot?.state ?? (entry.runtime ? buildRpcSessionState(entry.runtime.session) : undefined);
|
|
209
|
-
if (!replyState)
|
|
210
|
-
throw new Error("Session state was not created");
|
|
211
178
|
timer.mark("reply");
|
|
212
|
-
|
|
213
|
-
host.writer.enqueue(opened.sessionId, {
|
|
214
|
-
id: command.id,
|
|
215
|
-
type: "response",
|
|
216
|
-
command: "open_session",
|
|
217
|
-
success: true,
|
|
218
|
-
data: {
|
|
219
|
-
sessionId: opened.sessionId,
|
|
220
|
-
state: replyState,
|
|
221
|
-
...openReplyPermissionMode(replyState),
|
|
222
|
-
...(opened.attached ? { attached: true } : {}),
|
|
223
|
-
...(host.defaults.sessionPresence
|
|
224
|
-
? { presence: host.defaults.sessionPresence.view(replyState.sessionId) }
|
|
225
|
-
: {}),
|
|
226
|
-
},
|
|
227
|
-
});
|
|
179
|
+
replyOpen(host, opened, entry, command, options?.reply !== false);
|
|
228
180
|
return undefined;
|
|
229
181
|
}
|
|
230
182
|
catch (cause) {
|
|
@@ -233,7 +185,7 @@ export async function open(host, command, owner, options) {
|
|
|
233
185
|
// rollback, and only while the registry still holds the live entry, so only that
|
|
234
186
|
// rollback owes its subscribers the terminal record; an attach rollback, or one whose
|
|
235
187
|
// entry is already gone, never announced the session in the first place (#3198 review, L1).
|
|
236
|
-
if (!opened.attached && !options?.signal?.aborted) {
|
|
188
|
+
if (!options?.park && !opened.attached && !options?.signal?.aborted) {
|
|
237
189
|
const entry = host.registry.peek(opened.sessionId);
|
|
238
190
|
if (entry?.runtime !== undefined || entry?.worker !== undefined)
|
|
239
191
|
host.writer.sealRolledBackOpen(opened.sessionId, entry.sessionPath);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RpcCommand, RpcResponse } from "./rpc-types.ts";
|
|
2
|
+
import type { OpenRpcSession } from "./session-registry.ts";
|
|
3
|
+
import type { RouterContext } from "./session-router-context.ts";
|
|
4
|
+
type ParkCommand = Extract<RpcCommand, {
|
|
5
|
+
type: "park_session";
|
|
6
|
+
}>;
|
|
7
|
+
type ClaimCommand = Extract<RpcCommand, {
|
|
8
|
+
type: "claim_session";
|
|
9
|
+
}>;
|
|
10
|
+
/** Quiet teardown: speculation never owes a public terminal record or a handover park. */
|
|
11
|
+
export declare function closePark(host: RouterContext, opened: OpenRpcSession): Promise<void>;
|
|
12
|
+
export declare function parkSession(host: RouterContext, command: ParkCommand): Promise<RpcResponse>;
|
|
13
|
+
export declare function claimSession(host: RouterContext, command: ClaimCommand): Promise<RpcResponse | undefined>;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=session-router-park.d.ts.map
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { runWithProviderScope } from "@earendil-works/pi-ai/node/provider-scope";
|
|
2
|
+
import { withoutOpenTiming } from "../../core/open-timing.js";
|
|
3
|
+
import { outsideCommand } from "./command-authority.js";
|
|
4
|
+
import { runSetThinkingLevel } from "./effect-commands.js";
|
|
5
|
+
import { runSetModel } from "./handlers-model.js";
|
|
6
|
+
import { runSetPermissionPreset } from "./permission-preset-command.js";
|
|
7
|
+
import { applyAttachedProfile } from "./session-registry-attach.js";
|
|
8
|
+
import { releaseOwnedSession, releaseOwnerAttachment } from "./session-router-attachments.js";
|
|
9
|
+
import { finalizeClose, tryClaimClose } from "./session-router-close.js";
|
|
10
|
+
import { code, error } from "./session-router-errors.js";
|
|
11
|
+
import { open } from "./session-router-open.js";
|
|
12
|
+
import { validatedOpenProfile } from "./session-router-open-profile.js";
|
|
13
|
+
import { holdOpenConnection, ownOpen, publishOpen, replyOpen } from "./session-router-open-result.js";
|
|
14
|
+
import { syncRetainedRecord } from "./session-router-residency.js";
|
|
15
|
+
function profileKey(profile) {
|
|
16
|
+
return JSON.stringify([
|
|
17
|
+
profile.cwd,
|
|
18
|
+
profile.sessionKind ?? "interactive",
|
|
19
|
+
Object.entries(profile.sessionContext ?? {}).sort(([a], [b]) => a.localeCompare(b)),
|
|
20
|
+
]);
|
|
21
|
+
}
|
|
22
|
+
/** Same open, host-context merge, kind, media and binding; no owner and no publication. */
|
|
23
|
+
async function buildPark(host, command) {
|
|
24
|
+
let opened;
|
|
25
|
+
const response = await outsideCommand(() => host.writer.connectionContext.outside(() => withoutOpenTiming(() => open(host, { ...command, type: "open_session" }, undefined, {
|
|
26
|
+
reply: false,
|
|
27
|
+
park: (result) => {
|
|
28
|
+
opened = result;
|
|
29
|
+
},
|
|
30
|
+
}))));
|
|
31
|
+
if (response && !response.success)
|
|
32
|
+
throw new Error(response.error);
|
|
33
|
+
if (!opened)
|
|
34
|
+
throw new Error("park_open_failed");
|
|
35
|
+
return opened;
|
|
36
|
+
}
|
|
37
|
+
/** Quiet teardown: speculation never owes a public terminal record or a handover park. */
|
|
38
|
+
export async function closePark(host, opened) {
|
|
39
|
+
const claim = tryClaimClose(host, opened.sessionId, { drainAttachments: true });
|
|
40
|
+
if (claim?.finalizer)
|
|
41
|
+
await finalizeClose(host, opened.sessionId, host.bindings.get(opened.sessionId));
|
|
42
|
+
else
|
|
43
|
+
await host.finalizations.get(opened.sessionId)?.promise;
|
|
44
|
+
host.writer.forgetSession(opened.sessionId);
|
|
45
|
+
}
|
|
46
|
+
export async function parkSession(host, command) {
|
|
47
|
+
if (!host.registry.supportsParkClaim)
|
|
48
|
+
return error(command.id, command.type, "park_claim_unsupported");
|
|
49
|
+
if (host.draining || host.drainExitRequested)
|
|
50
|
+
return error(command.id, command.type, "host_draining");
|
|
51
|
+
const profile = validatedOpenProfile(host, { ...command, type: "open_session" });
|
|
52
|
+
if ("type" in profile)
|
|
53
|
+
return { ...profile, command: command.type };
|
|
54
|
+
try {
|
|
55
|
+
const state = await host.parks.park(profileKey(profile), () => buildPark(host, command));
|
|
56
|
+
if (host.draining || host.drainExitRequested)
|
|
57
|
+
return error(command.id, command.type, "host_draining");
|
|
58
|
+
return { id: command.id, type: "response", command: command.type, success: true, data: { state } };
|
|
59
|
+
}
|
|
60
|
+
catch (cause) {
|
|
61
|
+
return error(command.id, command.type, code(cause));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function claimSession(host, command) {
|
|
65
|
+
const owner = host.writer.currentConnection();
|
|
66
|
+
const release = owner === undefined ? undefined : holdOpenConnection(host, owner);
|
|
67
|
+
return claimReady(host, command, owner).finally(release);
|
|
68
|
+
}
|
|
69
|
+
async function claimReady(host, command, owner) {
|
|
70
|
+
if (!host.registry.supportsParkClaim || !host.registry.claimPark)
|
|
71
|
+
return error(command.id, command.type, "park_claim_unsupported");
|
|
72
|
+
if (host.draining || host.drainExitRequested)
|
|
73
|
+
return error(command.id, command.type, "host_draining");
|
|
74
|
+
const miss = () => ({
|
|
75
|
+
id: command.id,
|
|
76
|
+
type: "response",
|
|
77
|
+
command: "claim_session",
|
|
78
|
+
success: true,
|
|
79
|
+
data: { claimed: false },
|
|
80
|
+
});
|
|
81
|
+
// These require construction or a distinct admission contract. A miss leaves the park untouched.
|
|
82
|
+
if (command.sessionPath !== undefined ||
|
|
83
|
+
command.durableSessionId !== undefined ||
|
|
84
|
+
command.expectedHostInstance !== undefined ||
|
|
85
|
+
command.admissionId !== undefined ||
|
|
86
|
+
command.attended === false ||
|
|
87
|
+
command.retry !== undefined ||
|
|
88
|
+
(command.provider !== undefined) !== (command.modelId !== undefined) ||
|
|
89
|
+
command.spawnedBy !== undefined ||
|
|
90
|
+
command.env !== undefined ||
|
|
91
|
+
command.auto_title !== undefined ||
|
|
92
|
+
command.retryFallback !== undefined ||
|
|
93
|
+
(owner !== undefined && host.writer.compactSelection(owner)?.selected))
|
|
94
|
+
return miss();
|
|
95
|
+
const profile = validatedOpenProfile(host, { ...command, type: "open_session" });
|
|
96
|
+
if ("type" in profile)
|
|
97
|
+
return { ...profile, command: command.type };
|
|
98
|
+
const key = profileKey(profile);
|
|
99
|
+
const opened = host.parks.take(key);
|
|
100
|
+
if (!opened)
|
|
101
|
+
return miss();
|
|
102
|
+
let success = false;
|
|
103
|
+
try {
|
|
104
|
+
const entry = host.registry.peek(opened.sessionId);
|
|
105
|
+
if (entry?.state !== "open" || !entry.runtime)
|
|
106
|
+
return miss();
|
|
107
|
+
const runtime = entry.runtime;
|
|
108
|
+
const ctx = { session: runtime.session, sessionId: opened.sessionId, by: { connectionId: owner ?? "host" } };
|
|
109
|
+
await runWithProviderScope(entry.scope, async () => {
|
|
110
|
+
const responses = [];
|
|
111
|
+
if (command.provider !== undefined && command.modelId !== undefined)
|
|
112
|
+
responses.push(await runSetModel(ctx, {
|
|
113
|
+
type: "set_model",
|
|
114
|
+
provider: command.provider,
|
|
115
|
+
modelId: command.modelId,
|
|
116
|
+
scope: "session",
|
|
117
|
+
}));
|
|
118
|
+
if (command.thinkingLevel !== undefined)
|
|
119
|
+
responses.push(await runSetThinkingLevel(ctx, {
|
|
120
|
+
type: "set_thinking_level",
|
|
121
|
+
level: command.thinkingLevel,
|
|
122
|
+
scope: "turn",
|
|
123
|
+
}));
|
|
124
|
+
if (command.permissionPreset !== undefined)
|
|
125
|
+
responses.push(runSetPermissionPreset(runtime, {
|
|
126
|
+
type: "set_permission_preset",
|
|
127
|
+
permissionPreset: command.permissionPreset,
|
|
128
|
+
}));
|
|
129
|
+
const refused = responses.find((response) => !response.success);
|
|
130
|
+
if (refused && !refused.success)
|
|
131
|
+
throw new Error(refused.error);
|
|
132
|
+
applyAttachedProfile(entry, { ...profile, permissionPreset: undefined, permissionPresetSource: undefined });
|
|
133
|
+
if (runtime.launchProfile)
|
|
134
|
+
entry.profile = runtime.launchProfile;
|
|
135
|
+
});
|
|
136
|
+
if (host.draining || host.drainExitRequested || (owner !== undefined && host.releasedConnections.has(owner)))
|
|
137
|
+
return miss();
|
|
138
|
+
const claimed = await host.registry.claimPark(opened.sessionId, command.retain_on_disconnect === true);
|
|
139
|
+
if (host.draining || host.drainExitRequested)
|
|
140
|
+
return miss();
|
|
141
|
+
entry.acceptance?.applyTranscript(runtime.session.sessionManager.drainTranscript());
|
|
142
|
+
ownOpen(host, claimed, owner);
|
|
143
|
+
host.bindings
|
|
144
|
+
.get(claimed.sessionId)
|
|
145
|
+
?.setClientCapabilities?.((owner === undefined ? undefined : host.pendingCapabilities.get(owner)) ??
|
|
146
|
+
host.connectionOptions?.capabilities ??
|
|
147
|
+
[]);
|
|
148
|
+
publishOpen(host, claimed, entry);
|
|
149
|
+
await syncRetainedRecord(host, claimed.sessionId, undefined);
|
|
150
|
+
replyOpen(host, claimed, entry, command);
|
|
151
|
+
success = true;
|
|
152
|
+
if (owner !== undefined && host.releasedConnections.has(owner)) {
|
|
153
|
+
releaseOwnerAttachment(host, owner, claimed.sessionId);
|
|
154
|
+
await releaseOwnedSession(host, claimed.sessionId);
|
|
155
|
+
}
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
catch (cause) {
|
|
159
|
+
return error(command.id, command.type, code(cause));
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
if (!success)
|
|
163
|
+
await closePark(host, opened);
|
|
164
|
+
host.parks.finish(key);
|
|
165
|
+
if (success) {
|
|
166
|
+
const replacement = {
|
|
167
|
+
type: "park_session",
|
|
168
|
+
cwd: command.cwd,
|
|
169
|
+
kind: command.kind,
|
|
170
|
+
context: command.context,
|
|
171
|
+
};
|
|
172
|
+
host.parks.replace(key, () => buildPark(host, replacement));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=session-router-park.js.map
|
|
@@ -7,7 +7,7 @@ import type { RouterContext } from "./session-router-context.ts";
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function handle(host: RouterContext, command: RpcCommand): Promise<RpcResponse | undefined>;
|
|
9
9
|
export declare function dispatch(host: RouterContext, command: Exclude<RpcCommand, {
|
|
10
|
-
type: "warm";
|
|
10
|
+
type: "warm" | "park_session";
|
|
11
11
|
}>): Promise<RpcResponse | undefined>;
|
|
12
12
|
/** Any attached connection may patch; a host with no per-connection identity (stdio) owns every session. */
|
|
13
13
|
export declare function setSessionContext(host: RouterContext, command: Extract<RpcCommand, {
|
|
@@ -24,6 +24,7 @@ import { answerListSessions, answerProtocolInfo, answerSubscriptionLimits } from
|
|
|
24
24
|
import { acknowledgePrompt, otherRequestsSettled } from "./session-router-observation.js";
|
|
25
25
|
import { sweepDrain } from "./session-router-occupancy.js";
|
|
26
26
|
import { openWithBarrier } from "./session-router-open.js";
|
|
27
|
+
import { claimSession, parkSession } from "./session-router-park.js";
|
|
27
28
|
import { syncRetainedRecord } from "./session-router-residency.js";
|
|
28
29
|
import { answerSetSessionContext, answerSubscribeSessions, answerUnsubscribeSessions, notAttachedRefusal, } from "./session-status-commands.js";
|
|
29
30
|
import { unloadSession } from "./session-unload.js";
|
|
@@ -42,6 +43,10 @@ export function handle(host, command) {
|
|
|
42
43
|
return ownership ? Promise.resolve(ownership) : admitted(host, command);
|
|
43
44
|
}
|
|
44
45
|
function admitted(host, command) {
|
|
46
|
+
if ("sessionId" in command && command.sessionId !== undefined && host.registry.peek(command.sessionId)?.speculative)
|
|
47
|
+
return Promise.resolve(error(command.id, command.type, "unknown_session"));
|
|
48
|
+
if (command.type === "park_session")
|
|
49
|
+
return parkSession(host, command);
|
|
45
50
|
const refusal = attachmentGate(host, command) ?? compactAdmissionGate(host, command);
|
|
46
51
|
if (refusal)
|
|
47
52
|
return Promise.resolve(refusal);
|
|
@@ -117,6 +122,8 @@ export async function dispatch(host, command) {
|
|
|
117
122
|
return detachConnection(command, host.writer.currentConnection(), (id) => releaseConnection(host, id));
|
|
118
123
|
if (command.type === "open_session")
|
|
119
124
|
return openWithBarrier(host, command);
|
|
125
|
+
if (command.type === "claim_session")
|
|
126
|
+
return claimSession(host, command);
|
|
120
127
|
if (command.type === "attach_session")
|
|
121
128
|
return attachSession(host, command);
|
|
122
129
|
if (command.type === "release_attachment")
|