@code-yeongyu/senpi 2026.10.10-10 → 2026.10.10-12
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 +62 -0
- package/dist/bundle/chunks/{anthropic-messages-2B35OVB7.js → anthropic-messages-I4Z6VKS3.js} +1 -1
- package/dist/bundle/chunks/{app-server-command-CV6N7JDD.js → app-server-command-EGTBOL5W.js} +2 -2
- package/dist/bundle/chunks/bun-extension-importer-QSQHKXVG.js +20 -0
- package/dist/bundle/chunks/{chunk-4WZF7G6N.js → chunk-2AYAOU6W.js} +12 -10
- package/dist/bundle/chunks/chunk-2ZGQRUGM.js +2506 -0
- package/dist/bundle/chunks/{chunk-ZPJHS64R.js → chunk-5BRVRFJI.js} +1 -1
- package/dist/bundle/chunks/chunk-6SBY4ZWC.js +2 -0
- package/dist/bundle/chunks/chunk-DRVT7RIO.js +2 -0
- package/dist/bundle/chunks/chunk-E75RO54O.js +14 -0
- package/dist/bundle/chunks/{chunk-ZCYTYHDN.js → chunk-IAYFQEXI.js} +1 -1
- package/dist/bundle/chunks/chunk-M4C6Z7UT.js +288 -0
- package/dist/bundle/chunks/chunk-N6HU6BEY.js +9 -0
- package/dist/bundle/chunks/chunk-NEFNNQSV.js +9 -0
- package/dist/bundle/chunks/{chunk-YI7SPFDN.js → chunk-Q3R44CGT.js} +2 -2
- package/dist/bundle/chunks/chunk-QKU6JOA4.js +2 -0
- package/dist/bundle/chunks/{chunk-JRGNL6WM.js → chunk-QXREUA5F.js} +1 -1
- package/dist/bundle/chunks/chunk-SIIACSBE.js +30 -0
- package/dist/bundle/chunks/{chunk-QQZREV6B.js → chunk-SSH4CLHJ.js} +1 -1
- package/dist/bundle/chunks/chunk-TOZBUPZP.js +3 -0
- package/dist/bundle/chunks/chunk-WASCMT6Y.js +2 -0
- package/dist/bundle/chunks/chunk-YU4AYH4C.js +2 -0
- package/dist/bundle/chunks/chunk-ZGCPUGLT.js +6 -0
- package/dist/bundle/chunks/{chunk-NTH33AXY.js → chunk-ZJOYLQXK.js} +1 -1
- package/dist/bundle/chunks/chunk-ZRWOLP4E.js +2 -0
- package/dist/bundle/chunks/cli-main-PANLBILV.js +3 -0
- package/dist/bundle/chunks/{help-fast-path-5P73752B.js → help-fast-path-IQAJFO2G.js} +1 -1
- package/dist/bundle/chunks/{host-command-MWQNTWFA.js → host-command-MWSVX37K.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle-X2J5FTJL.js +3 -0
- package/dist/bundle/chunks/host-lifecycle.js +8 -8
- package/dist/bundle/chunks/interactive-mode-TQJU6K27.js +2 -0
- package/dist/bundle/chunks/multi-session-host-GBSMDUMG.js +26 -0
- package/dist/bundle/chunks/{package-manager-cli-PXP63ZPT.js → package-manager-cli-IBPABDGR.js} +1 -1
- package/dist/bundle/chunks/rotation-stream-JXT3JYW5.js +2 -0
- package/dist/bundle/chunks/rpc-mode-PFIAS2E5.js +2 -0
- package/dist/bundle/chunks/runtime-session-I4NDNGC5.js +2 -0
- package/dist/bundle/chunks/{schedule-command-MGZLHARF.js → schedule-command-Q4W37FAP.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-YYFCJZXZ.js +3 -0
- package/dist/bundle/chunks/session-picker-YLDBGXTR.js +2 -0
- package/dist/bundle/chunks/session-worker.js +455 -448
- package/dist/bundle/cli.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/cli/session-opening.d.ts +5 -0
- package/dist/cli/session-opening.js +11 -0
- package/dist/core/agent-session.d.ts +5 -0
- package/dist/core/agent-session.js +56 -33
- package/dist/core/engine-paused.d.ts +6 -0
- package/dist/core/engine-paused.js +50 -0
- package/dist/core/extensions/builtin/compaction/deterministic-fallback.d.ts +9 -2
- package/dist/core/extensions/builtin/compaction/deterministic-fallback.js +22 -45
- package/dist/core/extensions/builtin/compaction/index.js +55 -9
- package/dist/core/extensions/builtin/compaction/rejected-recovery.d.ts +11 -0
- package/dist/core/extensions/builtin/compaction/rejected-recovery.js +73 -0
- package/dist/core/extensions/builtin/compaction/retained-message-projection.d.ts +10 -0
- package/dist/core/extensions/builtin/compaction/retained-message-projection.js +102 -0
- package/dist/core/extensions/builtin/goal/continuation.js +2 -0
- package/dist/core/extensions/builtin/goal/direct-input-lifecycle.js +3 -1
- package/dist/core/extensions/builtin/goal/elapsed-ticker.js +2 -0
- package/dist/core/extensions/builtin/goal/format.d.ts +1 -1
- package/dist/core/extensions/builtin/goal/format.js +19 -4
- package/dist/core/extensions/builtin/goal/index.js +16 -9
- package/dist/core/extensions/builtin/goal/lifecycle-helpers.d.ts +1 -0
- package/dist/core/extensions/builtin/goal/lifecycle-helpers.js +57 -13
- package/dist/core/extensions/builtin/goal/monitor-continuation.js +2 -1
- package/dist/core/extensions/builtin/goal/persistence.js +2 -0
- package/dist/core/extensions/builtin/goal/renderers.js +9 -4
- package/dist/core/extensions/builtin/goal/store.d.ts +5 -0
- package/dist/core/extensions/builtin/goal/store.js +44 -0
- package/dist/core/extensions/builtin/goal/types.d.ts +10 -0
- package/dist/core/extensions/builtin/goal/types.js +4 -0
- package/dist/core/extensions/builtin/goal/ui.js +6 -3
- package/dist/core/extensions/builtin/mcp/index.js +4 -2
- package/dist/core/extensions/builtin/mcp/service-connection.js +1 -0
- package/dist/core/extensions/builtin/mcp/service-types.d.ts +2 -0
- package/dist/core/extensions/builtin/mcp/service.js +257 -74
- package/dist/core/extensions/builtin/terminal/process-identity.d.ts +1 -3
- package/dist/core/extensions/builtin/terminal/process-identity.js +1 -7
- package/dist/core/extensions/builtin/terminal/process-start-probe.js +11 -7
- package/dist/core/extensions/builtin/ttsr/index.js +19 -0
- package/dist/core/extensions/bun-extension-importer.d.ts +1 -1
- package/dist/core/extensions/bun-extension-importer.js +7 -3
- package/dist/core/extensions/extension-module-cache.d.ts +4 -3
- package/dist/core/extensions/extension-module-cache.js +8 -22
- package/dist/core/foreign-session-holders.d.ts +7 -0
- package/dist/core/foreign-session-holders.js +63 -0
- package/dist/core/moved-session-cwd.d.ts +7 -0
- package/dist/core/moved-session-cwd.js +23 -0
- package/dist/core/moved-sessions.d.ts +6 -0
- package/dist/core/moved-sessions.js +83 -24
- package/dist/core/resource-loader.js +5 -6
- package/dist/core/resume-target.js +3 -1
- package/dist/core/session-holders.d.ts +2 -1
- package/dist/core/session-holders.js +4 -4
- package/dist/core/session-manager.js +29 -10
- package/dist/core/webview/webview-readiness.d.ts +2 -0
- package/dist/core/webview/webview-readiness.js +9 -0
- package/dist/core/webview/webview-service.d.ts +4 -0
- package/dist/core/webview/webview-service.js +30 -5
- package/dist/main.js +14 -3
- package/dist/modes/app-server/daemon/process.d.ts +7 -1
- package/dist/modes/app-server/daemon/process.js +58 -6
- package/dist/modes/app-server/threads/goal-wire.js +2 -1
- package/dist/modes/interactive/interactive-mode.js +7 -0
- package/dist/modes/rpc/connection-handler.js +3 -2
- package/dist/modes/rpc/custom-capability.d.ts +2 -0
- package/dist/modes/rpc/custom-capability.js +2 -0
- package/dist/modes/rpc/host-capabilities.js +3 -1
- package/dist/modes/rpc/host-client-occupancy.d.ts +2 -1
- package/dist/modes/rpc/host-client-occupancy.js +3 -0
- package/dist/modes/rpc/host-daemon-registration.js +20 -6
- package/dist/modes/rpc/host-daemon-state.d.ts +17 -0
- package/dist/modes/rpc/host-daemon-state.js +60 -0
- package/dist/modes/rpc/host-ensure-start.js +13 -3
- package/dist/modes/rpc/host-ensure-stop.d.ts +1 -1
- package/dist/modes/rpc/host-ensure-stop.js +17 -4
- package/dist/modes/rpc/host-ensure-types.d.ts +2 -0
- package/dist/modes/rpc/host-ensure.js +20 -2
- package/dist/modes/rpc/host-gc-evidence.js +3 -0
- package/dist/modes/rpc/host-generations.js +4 -1
- package/dist/modes/rpc/host-legacy.d.ts +1 -1
- package/dist/modes/rpc/host-legacy.js +11 -2
- package/dist/modes/rpc/host-lifecycle-activity.d.ts +30 -0
- package/dist/modes/rpc/host-lifecycle-activity.js +136 -4
- package/dist/modes/rpc/host-lifecycle-launch.d.ts +4 -0
- package/dist/modes/rpc/host-lifecycle-launch.js +24 -1
- package/dist/modes/rpc/host-lifecycle-proxy.d.ts +2 -0
- package/dist/modes/rpc/host-lifecycle-proxy.js +4 -1
- package/dist/modes/rpc/host-lifecycle-scratch.js +5 -2
- package/dist/modes/rpc/host-lifecycle-shutdown.d.ts +1 -1
- package/dist/modes/rpc/host-lifecycle-shutdown.js +81 -37
- package/dist/modes/rpc/host-lifecycle.js +23 -1
- package/dist/modes/rpc/host-reservations.d.ts +2 -0
- package/dist/modes/rpc/host-reservations.js +61 -3
- package/dist/modes/rpc/host-stalled-evidence.js +17 -3
- package/dist/modes/rpc/host-stop-intent.d.ts +3 -0
- package/dist/modes/rpc/host-stop-intent.js +3 -0
- package/dist/modes/rpc/host-stop.js +4 -9
- package/dist/modes/rpc/host-successor.js +10 -3
- package/dist/modes/rpc/host-supervisor-log.d.ts +1 -0
- package/dist/modes/rpc/host-supervisor-log.js +9 -0
- package/dist/modes/rpc/multi-session-host.js +8 -6
- package/dist/modes/rpc/rpc-mode.d.ts +3 -1
- package/dist/modes/rpc/rpc-mode.js +3 -1
- package/dist/modes/rpc/rpc-types.d.ts +3 -1
- package/dist/modes/rpc/rpc-types.js +2 -0
- package/dist/modes/rpc/session-command-router.d.ts +1 -1
- package/dist/modes/rpc/session-command-router.js +18 -14
- package/dist/modes/rpc/session-held.d.ts +8 -0
- package/dist/modes/rpc/session-held.js +127 -0
- package/dist/modes/rpc/session-registry-types.d.ts +1 -1
- package/dist/modes/rpc/session-registry.d.ts +2 -0
- package/dist/modes/rpc/session-registry.js +4 -0
- package/dist/modes/rpc/session-worker-client.d.ts +3 -3
- package/dist/modes/rpc/session-worker-client.js +24 -12
- package/dist/modes/rpc/worker-session-claims.d.ts +15 -0
- package/dist/modes/rpc/worker-session-claims.js +61 -0
- package/dist/modes/rpc/worker-session-registry.d.ts +4 -0
- package/dist/modes/rpc/worker-session-registry.js +51 -8
- package/dist/utils/shell.d.ts +6 -2
- package/dist/utils/shell.js +20 -6
- package/docs/app-server.md +1 -1
- package/docs/rpc.md +75 -2
- package/docs/session-format.md +19 -0
- package/package.json +6 -6
- package/dist/bundle/chunks/bun-extension-importer-3TEDTSKJ.js +0 -20
- package/dist/bundle/chunks/chunk-5AKP2VFB.js +0 -16
- package/dist/bundle/chunks/chunk-6Y7VGPJB.js +0 -9
- package/dist/bundle/chunks/chunk-CIRZTP7W.js +0 -2
- package/dist/bundle/chunks/chunk-HKRW6HT5.js +0 -30
- package/dist/bundle/chunks/chunk-JEZWV7JW.js +0 -17
- package/dist/bundle/chunks/chunk-LR5EN45D.js +0 -7
- package/dist/bundle/chunks/chunk-MYVBK47X.js +0 -2
- package/dist/bundle/chunks/chunk-O6YOQUVX.js +0 -2773
- package/dist/bundle/chunks/chunk-TJPMB4VH.js +0 -2
- package/dist/bundle/chunks/chunk-U5KVIAHJ.js +0 -2
- package/dist/bundle/chunks/chunk-XMHO4QQN.js +0 -7
- package/dist/bundle/chunks/cli-main-TM4UFZVC.js +0 -3
- package/dist/bundle/chunks/host-lifecycle-B24G65GD.js +0 -3
- package/dist/bundle/chunks/interactive-mode-CBQW7WRK.js +0 -2
- package/dist/bundle/chunks/multi-session-host-4KXWGRNS.js +0 -25
- package/dist/bundle/chunks/rotation-stream-FQH63DMY.js +0 -2
- package/dist/bundle/chunks/rpc-mode-7WHJHMUP.js +0 -2
- package/dist/bundle/chunks/runtime-session-CWJNFRHY.js +0 -2
- package/dist/bundle/chunks/session-control-endpoint-OH7XV2FN.js +0 -3
- package/dist/bundle/chunks/session-picker-UUSW6YSU.js +0 -2
|
@@ -8,6 +8,7 @@ import { RPC_ERROR_INVALID_LAUNCH_PROFILE, RPC_ERROR_INVALID_SESSION_CONTEXT, RP
|
|
|
8
8
|
import { runWithSessionAttribution } from "./session-attribution.js";
|
|
9
9
|
import { createRpcSessionBinding } from "./session-binding.js";
|
|
10
10
|
import { readDrainVerdicts } from "./session-drain.js";
|
|
11
|
+
import { dispatchSessionBinding, sessionHeldCheck } from "./session-held.js";
|
|
11
12
|
import { RpcSessionRegistryError } from "./session-registry.js";
|
|
12
13
|
import { releaseSession } from "./session-release.js";
|
|
13
14
|
import { selectSweepEvictions } from "./session-sweep.js";
|
|
@@ -250,20 +251,14 @@ export class SessionCommandRouter {
|
|
|
250
251
|
}
|
|
251
252
|
if (!command.sessionId)
|
|
252
253
|
return error(command.id, command.type, RPC_ERROR_MISSING_SESSION_ID);
|
|
254
|
+
const sessionId = command.sessionId;
|
|
253
255
|
try {
|
|
254
|
-
this.registry.getForCommand(
|
|
255
|
-
|
|
256
|
-
if (!binding)
|
|
257
|
-
return error(command.id, command.type, RPC_ERROR_UNKNOWN_SESSION);
|
|
258
|
-
// A prompt's preflight runs on the session's loop and can outlive the client's request deadline on a
|
|
259
|
-
// starved host; acknowledging receipt first lets the client wait it out (senpi#2871).
|
|
260
|
-
if (command.type === "prompt")
|
|
261
|
-
this.acknowledgePrompt(command.id, command.sessionId);
|
|
262
|
-
await binding.handle(command);
|
|
256
|
+
const entry = this.registry.getForCommand(sessionId, command.type);
|
|
257
|
+
await dispatchSessionBinding(command, entry, this.bindings.get(sessionId), () => this.acknowledgePrompt(command.id, sessionId), (observedStarts) => this.registry.holderPids?.(observedStarts) ?? Promise.resolve([]));
|
|
263
258
|
return undefined;
|
|
264
259
|
}
|
|
265
260
|
catch (cause) {
|
|
266
|
-
return error(command.id, command.type, this.code(cause));
|
|
261
|
+
return error(command.id, command.type, this.code(cause), this.detail(cause));
|
|
267
262
|
}
|
|
268
263
|
}
|
|
269
264
|
async dispose() {
|
|
@@ -483,7 +478,9 @@ export class SessionCommandRouter {
|
|
|
483
478
|
if (retryFallbackError)
|
|
484
479
|
return error(command.id, "open_session", `${RPC_ERROR_INVALID_LAUNCH_PROFILE}: ${retryFallbackError}`);
|
|
485
480
|
let opened;
|
|
481
|
+
const assertSessionNotHeld = sessionHeldCheck((observedStarts) => this.registry.holderPids?.(observedStarts) ?? Promise.resolve([]));
|
|
486
482
|
try {
|
|
483
|
+
await assertSessionNotHeld(command.sessionPath);
|
|
487
484
|
opened = await this.registry.openSession({
|
|
488
485
|
cwd: command.cwd ?? this.defaults.cwd,
|
|
489
486
|
sessionPath: command.sessionPath,
|
|
@@ -506,6 +503,10 @@ export class SessionCommandRouter {
|
|
|
506
503
|
{ retainOnDisconnect: command.retain_on_disconnect === true });
|
|
507
504
|
const openedSession = opened;
|
|
508
505
|
const entry = this.registry.getForCommand(openedSession.sessionId, "open_session");
|
|
506
|
+
const state = entry.worker?.snapshot?.state;
|
|
507
|
+
// The runtime has now published its own lease. Recheck before attaching or replying,
|
|
508
|
+
// so an opener racing the initial lookup is rolled back rather than admitted.
|
|
509
|
+
await assertSessionNotHeld(entry.runtime?.session.sessionFile ?? state?.sessionFile ?? entry.sessionPath, entry.runtime?.session.sessionId ?? state?.sessionId ?? entry.durableSessionId);
|
|
509
510
|
this.writer.setSessionKind(openedSession.sessionId, entry.kind);
|
|
510
511
|
this.writer.setSessionMedia(openedSession.sessionId, toolMediaPersister(() => liveToolMediaScope(entry)));
|
|
511
512
|
if (owner !== undefined) {
|
|
@@ -567,8 +568,8 @@ export class SessionCommandRouter {
|
|
|
567
568
|
this.releaseOwnerAttachment(owner, openedSession.sessionId);
|
|
568
569
|
await this.releaseOwnedSession(openedSession.sessionId);
|
|
569
570
|
}
|
|
570
|
-
const
|
|
571
|
-
if (!
|
|
571
|
+
const replyState = entry.worker?.snapshot?.state ?? (entry.runtime ? buildRpcSessionState(entry.runtime.session) : undefined);
|
|
572
|
+
if (!replyState)
|
|
572
573
|
throw new Error("Session state was not created");
|
|
573
574
|
this.writer.enqueue(opened.sessionId, {
|
|
574
575
|
id: command.id,
|
|
@@ -577,7 +578,7 @@ export class SessionCommandRouter {
|
|
|
577
578
|
success: true,
|
|
578
579
|
data: {
|
|
579
580
|
sessionId: opened.sessionId,
|
|
580
|
-
state,
|
|
581
|
+
state: replyState,
|
|
581
582
|
...(opened.attached ? { attached: true } : {}),
|
|
582
583
|
},
|
|
583
584
|
});
|
|
@@ -586,7 +587,10 @@ export class SessionCommandRouter {
|
|
|
586
587
|
catch (cause) {
|
|
587
588
|
if (opened) {
|
|
588
589
|
try {
|
|
589
|
-
|
|
590
|
+
if (opened.attached)
|
|
591
|
+
await this.releaseOwnedSession(opened.sessionId);
|
|
592
|
+
else
|
|
593
|
+
await this.registry.close(opened.sessionId);
|
|
590
594
|
}
|
|
591
595
|
catch {
|
|
592
596
|
/* The open rollback has already removed the entry. */
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RpcInboundRecord } from "./rpc-types.ts";
|
|
2
|
+
import type { RpcSessionBinding } from "./session-binding.ts";
|
|
3
|
+
import { type RpcSessionEntry } from "./session-registry-types.ts";
|
|
4
|
+
/** Fresh leases at each boundary; daemon identity resolution is lazy and scoped to one command. */
|
|
5
|
+
export declare function sessionHeldCheck(resolveDaemonPids?: (observedStarts: ReadonlyMap<number, number | undefined>) => Promise<readonly number[]>): (sessionFile: string | undefined, sessionId?: string) => Promise<void>;
|
|
6
|
+
/** The session-command admission boundary shared by in-process and worker hosts. */
|
|
7
|
+
export declare function dispatchSessionBinding(command: RpcInboundRecord, entry: RpcSessionEntry, binding: RpcSessionBinding | undefined, acknowledgePrompt: () => void, daemonPids?: (observedStarts: ReadonlyMap<number, number | undefined>) => Promise<readonly number[]>): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=session-held.d.ts.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readProcessStartMs } from "../../core/extensions/builtin/terminal/process-start-probe.js";
|
|
3
|
+
import { foreignSessionHolders } from "../../core/foreign-session-holders.js";
|
|
4
|
+
import { processStartTimeMs } from "../app-server/daemon/process.js";
|
|
5
|
+
import { RPC_ERROR_SESSION_HELD } from "./rpc-types.js";
|
|
6
|
+
import { RpcSessionRegistryError } from "./session-registry-types.js";
|
|
7
|
+
// Admission is conservative: a new command must pass the guard unless it is explicitly
|
|
8
|
+
// a read or a control needed to finish/cancel an already-admitted turn.
|
|
9
|
+
const READ_ONLY_OR_CONTROL = new Set([
|
|
10
|
+
"get_state",
|
|
11
|
+
"get_steering_messages",
|
|
12
|
+
"get_follow_up_messages",
|
|
13
|
+
"get_available_models",
|
|
14
|
+
"get_available_thinking_levels",
|
|
15
|
+
"get_fast_mode",
|
|
16
|
+
"get_session_stats",
|
|
17
|
+
"get_fork_messages",
|
|
18
|
+
"get_entries",
|
|
19
|
+
"get_tree",
|
|
20
|
+
"get_last_assistant_text",
|
|
21
|
+
"get_messages",
|
|
22
|
+
"get_media",
|
|
23
|
+
"get_commands",
|
|
24
|
+
"get_loaded_surfaces",
|
|
25
|
+
"get_auth_providers",
|
|
26
|
+
"get_provider_accounts",
|
|
27
|
+
"memory_report",
|
|
28
|
+
"export_html",
|
|
29
|
+
"export_jsonl",
|
|
30
|
+
"abort",
|
|
31
|
+
"interrupt",
|
|
32
|
+
"abort_compaction",
|
|
33
|
+
"abort_branch_summary",
|
|
34
|
+
"abort_retry",
|
|
35
|
+
"abort_bash",
|
|
36
|
+
"clear_queue",
|
|
37
|
+
"check_reload_veto",
|
|
38
|
+
"cleanup_bash_output",
|
|
39
|
+
"set_client_info",
|
|
40
|
+
"extension_ui_response",
|
|
41
|
+
"extension_ui_progress",
|
|
42
|
+
]);
|
|
43
|
+
/** One bounded identity snapshot for all foreign PIDs, never one subprocess per lease. */
|
|
44
|
+
function readProcessStarts() {
|
|
45
|
+
const windows = process.platform === "win32";
|
|
46
|
+
const command = windows ? "powershell.exe" : "ps";
|
|
47
|
+
const args = windows
|
|
48
|
+
? [
|
|
49
|
+
"-NoProfile",
|
|
50
|
+
"-NonInteractive",
|
|
51
|
+
"-Command",
|
|
52
|
+
'Get-CimInstance Win32_Process -ErrorAction Stop | ForEach-Object { if ($null -ne $_.CreationDate) { "{0} {1}" -f $_.ProcessId, $_.CreationDate.ToUniversalTime().ToString("o") } }',
|
|
53
|
+
]
|
|
54
|
+
: ["-axo", "pid=,lstart="];
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
execFile(command, args, {
|
|
57
|
+
timeout: windows ? 5_000 : 1_000,
|
|
58
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
59
|
+
windowsHide: true,
|
|
60
|
+
...(windows ? {} : { env: { ...process.env, LC_ALL: "C", LANG: "C" } }),
|
|
61
|
+
}, (error, stdout) => {
|
|
62
|
+
if (error)
|
|
63
|
+
return resolve(undefined);
|
|
64
|
+
const starts = new Map();
|
|
65
|
+
for (const line of stdout.split("\n")) {
|
|
66
|
+
const row = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
67
|
+
if (!row)
|
|
68
|
+
continue;
|
|
69
|
+
const start = processStartTimeMs(row[2]);
|
|
70
|
+
if (start !== undefined)
|
|
71
|
+
starts.set(Number(row[1]), start);
|
|
72
|
+
}
|
|
73
|
+
resolve(starts);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** Fresh leases at each boundary; daemon identity resolution is lazy and scoped to one command. */
|
|
78
|
+
export function sessionHeldCheck(resolveDaemonPids) {
|
|
79
|
+
let daemonPids;
|
|
80
|
+
let processStarts;
|
|
81
|
+
const pidStarts = new Map();
|
|
82
|
+
const observedStarts = new Map();
|
|
83
|
+
return async (sessionFile, sessionId) => {
|
|
84
|
+
const holders = await foreignSessionHolders(sessionFile, sessionId, {
|
|
85
|
+
readProcessStartMs: (pid) => {
|
|
86
|
+
let probe = pidStarts.get(pid);
|
|
87
|
+
if (!probe) {
|
|
88
|
+
probe = (async () => {
|
|
89
|
+
if (process.platform !== "linux")
|
|
90
|
+
processStarts ??= readProcessStarts();
|
|
91
|
+
const starts = await processStarts;
|
|
92
|
+
const start = starts?.get(pid) ?? (await readProcessStartMs(pid));
|
|
93
|
+
observedStarts.set(pid, start);
|
|
94
|
+
return start;
|
|
95
|
+
})();
|
|
96
|
+
pidStarts.set(pid, probe);
|
|
97
|
+
}
|
|
98
|
+
return probe;
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
if (holders.length === 0)
|
|
102
|
+
return;
|
|
103
|
+
daemonPids ??= resolveDaemonPids?.(observedStarts) ?? Promise.resolve([]);
|
|
104
|
+
const family = await daemonPids;
|
|
105
|
+
const foreign = holders.filter((holder) => !family.includes(holder.pid));
|
|
106
|
+
if (foreign.length > 0)
|
|
107
|
+
throw new RpcSessionRegistryError(RPC_ERROR_SESSION_HELD, undefined, {
|
|
108
|
+
holders: foreign.map(({ pid, cwd }) => ({ pid, ...(cwd === undefined ? {} : { cwd }) })),
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** The session-command admission boundary shared by in-process and worker hosts. */
|
|
113
|
+
export async function dispatchSessionBinding(command, entry, binding, acknowledgePrompt, daemonPids) {
|
|
114
|
+
if (!binding)
|
|
115
|
+
throw new RpcSessionRegistryError("unknown_session");
|
|
116
|
+
if (!READ_ONLY_OR_CONTROL.has(command.type)) {
|
|
117
|
+
const assertSessionNotHeld = sessionHeldCheck(daemonPids);
|
|
118
|
+
const state = entry.worker?.snapshot?.state;
|
|
119
|
+
await assertSessionNotHeld(entry.runtime?.session.sessionFile ?? state?.sessionFile ?? entry.sessionPath, entry.runtime?.session.sessionId ?? state?.sessionId ?? entry.durableSessionId);
|
|
120
|
+
if ("sessionPath" in command)
|
|
121
|
+
await assertSessionNotHeld(command.sessionPath);
|
|
122
|
+
}
|
|
123
|
+
if (command.type === "prompt")
|
|
124
|
+
acknowledgePrompt();
|
|
125
|
+
await binding.handle(command);
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=session-held.js.map
|
|
@@ -63,7 +63,7 @@ export interface RpcSessionEntry {
|
|
|
63
63
|
closeStarted?: boolean;
|
|
64
64
|
}
|
|
65
65
|
export declare class RpcSessionRegistryError extends Error {
|
|
66
|
-
readonly code: "unknown_session" | "session_closing" | "session_path_in_use" | "session_id_in_use" | "session_reservation_limit" | "invalid_path" | "invalid_session_id" | "open_failed";
|
|
66
|
+
readonly code: "unknown_session" | "session_closing" | "session_path_in_use" | "session_held" | "session_id_in_use" | "session_reservation_limit" | "invalid_path" | "invalid_session_id" | "open_failed";
|
|
67
67
|
/** Machine-readable context for the wire (`errorData`): who holds a path, when to retry. */
|
|
68
68
|
readonly detail?: Readonly<Record<string, unknown>>;
|
|
69
69
|
constructor(code: RpcSessionRegistryError["code"], reason?: string, detail?: Readonly<Record<string, unknown>>);
|
|
@@ -15,6 +15,8 @@ export declare class RpcSessionRegistry {
|
|
|
15
15
|
constructor(options: RpcSessionRegistryOptions);
|
|
16
16
|
/** Number of live entries, including ones still opening or closing. */
|
|
17
17
|
get size(): number;
|
|
18
|
+
/** Daemon-family identities come from the endpoint's claims and generation registry. */
|
|
19
|
+
holderPids(observedStarts?: ReadonlyMap<number, number | undefined>): Promise<readonly number[]>;
|
|
18
20
|
openSession(requested: RpcSessionLaunchProfile, options?: RpcSessionOpenOptions): Promise<OpenRpcSession>;
|
|
19
21
|
/**
|
|
20
22
|
* Read-only lookup with no state transitions or attachment accounting.
|
|
@@ -56,6 +56,10 @@ export class RpcSessionRegistry {
|
|
|
56
56
|
get size() {
|
|
57
57
|
return this.entries.size;
|
|
58
58
|
}
|
|
59
|
+
/** Daemon-family identities come from the endpoint's claims and generation registry. */
|
|
60
|
+
holderPids(observedStarts) {
|
|
61
|
+
return this.options.pathReservations?.holderPids?.(observedStarts) ?? Promise.resolve([]);
|
|
62
|
+
}
|
|
59
63
|
async openSession(requested, options) {
|
|
60
64
|
this.validateProfile(requested);
|
|
61
65
|
const profile = resolveMovedProfile(requested);
|
|
@@ -9,10 +9,10 @@ import type { RpcSessionLaunchProfile } from "./session-registry.ts";
|
|
|
9
9
|
import type { SessionWriteGrant, WorkerSnapshot } from "./session-worker-protocol.ts";
|
|
10
10
|
/** Lifecycle hooks the owning registry installs on every worker it allocates. */
|
|
11
11
|
export interface SessionWorkerCallbacks {
|
|
12
|
-
reserve: (path: string) => SessionWriteGrant
|
|
12
|
+
reserve: (path: string) => SessionWriteGrant | Promise<SessionWriteGrant>;
|
|
13
13
|
/** Every snapshot republishes which paths this worker still writes. */
|
|
14
|
-
reconcile: (livePaths: readonly string[]) => void
|
|
15
|
-
exit: () => void
|
|
14
|
+
reconcile: (livePaths: readonly string[]) => void | Promise<void>;
|
|
15
|
+
exit: () => void | Promise<void>;
|
|
16
16
|
failure: (error: string) => void;
|
|
17
17
|
}
|
|
18
18
|
export declare class SessionWorkerClient {
|
|
@@ -24,7 +24,7 @@ export class SessionWorkerClient {
|
|
|
24
24
|
this.listeners = new Set();
|
|
25
25
|
this.cancelUiPending = false;
|
|
26
26
|
this.callbacks = callbacks;
|
|
27
|
-
this.exited = new Promise((resolve) => {
|
|
27
|
+
this.exited = new Promise((resolve, reject) => {
|
|
28
28
|
this.worker.once("exit", () => {
|
|
29
29
|
if (!this.stopped && !this.closeTimer)
|
|
30
30
|
this.fail("session_worker_exited");
|
|
@@ -34,9 +34,16 @@ export class SessionWorkerClient {
|
|
|
34
34
|
this.requests.close(new Error("session_worker_exited"));
|
|
35
35
|
void this.webviewBroker?.dispose();
|
|
36
36
|
this.listeners.clear();
|
|
37
|
-
callbacks.exit();
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
const released = callbacks.exit();
|
|
38
|
+
if (released)
|
|
39
|
+
void released.then(() => {
|
|
40
|
+
this.publishTerminalFailure();
|
|
41
|
+
resolve();
|
|
42
|
+
}, reject);
|
|
43
|
+
else {
|
|
44
|
+
this.publishTerminalFailure();
|
|
45
|
+
resolve();
|
|
46
|
+
}
|
|
40
47
|
});
|
|
41
48
|
});
|
|
42
49
|
this.worker.on("message", (message) => this.receive(message));
|
|
@@ -141,18 +148,24 @@ export class SessionWorkerClient {
|
|
|
141
148
|
case "ready":
|
|
142
149
|
case "result": {
|
|
143
150
|
this.requests.receive(message);
|
|
151
|
+
if (this.snapshot)
|
|
152
|
+
void Promise.resolve(this.callbacks.reconcile(this.snapshot.liveSessionPaths)).catch(() => this.fail("session_claim_reconcile_failed"));
|
|
144
153
|
return;
|
|
145
154
|
}
|
|
146
155
|
case "reserve":
|
|
147
|
-
acknowledgeGrant(message.signal, this.
|
|
156
|
+
void Promise.resolve(this.callbacks.reserve(message.path)).then((grant) => acknowledgeGrant(message.signal, this.stopped ? "conflict" : grant), () => acknowledgeGrant(message.signal, "conflict"));
|
|
148
157
|
return;
|
|
149
158
|
case "snapshot":
|
|
150
159
|
this.snapshot = message.snapshot;
|
|
151
|
-
this.callbacks.reconcile(message.snapshot.liveSessionPaths)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
160
|
+
void Promise.resolve(this.callbacks.reconcile(message.snapshot.liveSessionPaths)).then(() => {
|
|
161
|
+
acknowledge(message.signal, !this.stopped);
|
|
162
|
+
if (message.settled)
|
|
163
|
+
for (const listener of [...this.listeners])
|
|
164
|
+
listener();
|
|
165
|
+
}, () => {
|
|
166
|
+
acknowledge(message.signal, false);
|
|
167
|
+
this.fail("session_claim_reconcile_failed");
|
|
168
|
+
});
|
|
156
169
|
return;
|
|
157
170
|
case "control_done":
|
|
158
171
|
this.cancelUiPending = false;
|
|
@@ -167,7 +180,6 @@ export class SessionWorkerClient {
|
|
|
167
180
|
// Identity and activity commit before publication; clients may attach or disconnect on that event.
|
|
168
181
|
if (message.snapshot) {
|
|
169
182
|
this.snapshot = message.snapshot;
|
|
170
|
-
this.callbacks.reconcile(message.snapshot.liveSessionPaths);
|
|
171
183
|
}
|
|
172
184
|
else if (this.snapshot)
|
|
173
185
|
this.snapshot = {
|
|
@@ -183,7 +195,7 @@ export class SessionWorkerClient {
|
|
|
183
195
|
}
|
|
184
196
|
return writer.waitForSessionBackpressure(sessionId);
|
|
185
197
|
};
|
|
186
|
-
const consumed = message.connection === undefined ? enqueue() : writer.withConnection(message.connection, enqueue);
|
|
198
|
+
const consumed = Promise.resolve(message.snapshot ? this.callbacks.reconcile(message.snapshot.liveSessionPaths) : undefined).then(() => message.connection === undefined ? enqueue() : writer.withConnection(message.connection, enqueue));
|
|
187
199
|
void consumed.then(() => acknowledge(message.signal, true), (cause) => {
|
|
188
200
|
acknowledge(message.signal, false);
|
|
189
201
|
this.fail(cause instanceof Error ? cause.message : String(cause));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SessionPathOwner, SessionPathReservations } from "./host-reservations.ts";
|
|
2
|
+
/** Claims precede write grants; native exit also removes claims whose publication is still pending. */
|
|
3
|
+
export declare class WorkerSessionClaims {
|
|
4
|
+
private readonly reservations;
|
|
5
|
+
private readonly paths;
|
|
6
|
+
private readonly releases;
|
|
7
|
+
private closed;
|
|
8
|
+
constructor(reservations: SessionPathReservations | undefined);
|
|
9
|
+
claim(path: string, attached?: boolean): Promise<SessionPathOwner | undefined>;
|
|
10
|
+
setAttached(attached: boolean): void;
|
|
11
|
+
reconcile(livePaths: readonly string[], attached: boolean): void;
|
|
12
|
+
private release;
|
|
13
|
+
close(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=worker-session-claims.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { RpcSessionRegistryError } from "./session-registry-types.js";
|
|
2
|
+
/** Claims precede write grants; native exit also removes claims whose publication is still pending. */
|
|
3
|
+
export class WorkerSessionClaims {
|
|
4
|
+
constructor(reservations) {
|
|
5
|
+
this.paths = new Map();
|
|
6
|
+
this.releases = new Set();
|
|
7
|
+
this.closed = false;
|
|
8
|
+
this.reservations = reservations;
|
|
9
|
+
}
|
|
10
|
+
async claim(path, attached = true) {
|
|
11
|
+
if (this.closed)
|
|
12
|
+
throw new RpcSessionRegistryError("session_closing");
|
|
13
|
+
if (!this.reservations)
|
|
14
|
+
return undefined;
|
|
15
|
+
const previous = this.paths.get(path);
|
|
16
|
+
if (previous)
|
|
17
|
+
return previous.ownership;
|
|
18
|
+
const record = { ownership: this.reservations.claim(path, attached), attached };
|
|
19
|
+
this.paths.set(path, record);
|
|
20
|
+
const owner = await record.ownership;
|
|
21
|
+
if (owner && this.paths.get(path) === record)
|
|
22
|
+
this.paths.delete(path);
|
|
23
|
+
return owner;
|
|
24
|
+
}
|
|
25
|
+
setAttached(attached) {
|
|
26
|
+
for (const [path, record] of this.paths) {
|
|
27
|
+
if (record.attached === attached)
|
|
28
|
+
continue;
|
|
29
|
+
record.attached = attached;
|
|
30
|
+
void record.ownership.then((owner) => {
|
|
31
|
+
if (!owner && this.paths.get(path) === record)
|
|
32
|
+
this.reservations?.setAttached(path, record.attached);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
reconcile(livePaths, attached) {
|
|
37
|
+
const live = new Set(livePaths);
|
|
38
|
+
for (const path of this.paths.keys())
|
|
39
|
+
if (!live.has(path))
|
|
40
|
+
void this.release(path);
|
|
41
|
+
this.setAttached(attached);
|
|
42
|
+
}
|
|
43
|
+
release(path) {
|
|
44
|
+
const record = this.paths.get(path);
|
|
45
|
+
if (!record)
|
|
46
|
+
return Promise.resolve();
|
|
47
|
+
this.paths.delete(path);
|
|
48
|
+
const removal = record.ownership.then(async (owner) => {
|
|
49
|
+
if (!owner)
|
|
50
|
+
await this.reservations?.release(path);
|
|
51
|
+
});
|
|
52
|
+
this.releases.add(removal);
|
|
53
|
+
void removal.then(() => this.releases.delete(removal), () => this.releases.delete(removal));
|
|
54
|
+
return removal;
|
|
55
|
+
}
|
|
56
|
+
async close() {
|
|
57
|
+
this.closed = true;
|
|
58
|
+
await Promise.all([...this.paths.keys()].map((path) => this.release(path)).concat([...this.releases]));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=worker-session-claims.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CliRuntimeConfiguration } from "../../main.ts";
|
|
2
|
+
import { type SessionPathReservations as DaemonPathReservations } from "./host-reservations.ts";
|
|
2
3
|
import { type OpenRpcSession, type RpcSessionEntry, type RpcSessionLaunchProfile, type RpcSessionOpenOptions, type RpcSessionRow } from "./session-registry.ts";
|
|
3
4
|
import { SessionWorkerClient } from "./session-worker-client.ts";
|
|
4
5
|
type SessionWorkerCallbacks = ConstructorParameters<typeof SessionWorkerClient>[0];
|
|
@@ -8,11 +9,13 @@ export interface WorkerSessionRegistryOptions {
|
|
|
8
9
|
readonly now: () => number;
|
|
9
10
|
/** Production builds the real worker; a caller may supply one to drive a lifecycle path deterministically. */
|
|
10
11
|
readonly createWorker?: (callbacks: SessionWorkerCallbacks) => SessionWorkerClient;
|
|
12
|
+
readonly pathReservations?: DaemonPathReservations;
|
|
11
13
|
}
|
|
12
14
|
/** Transport-side lifecycle owner. Caller paths are never inspected on this event loop. */
|
|
13
15
|
export declare class WorkerSessionRegistry {
|
|
14
16
|
private readonly entries;
|
|
15
17
|
private readonly reservations;
|
|
18
|
+
private readonly claims;
|
|
16
19
|
private serial;
|
|
17
20
|
readonly closeGraceMs: number;
|
|
18
21
|
private readonly now;
|
|
@@ -20,6 +23,7 @@ export declare class WorkerSessionRegistry {
|
|
|
20
23
|
private readonly createWorker;
|
|
21
24
|
constructor(options: WorkerSessionRegistryOptions);
|
|
22
25
|
get size(): number;
|
|
26
|
+
holderPids(observedStarts?: ReadonlyMap<number, number | undefined>): Promise<readonly number[]>;
|
|
23
27
|
openSession(profile: RpcSessionLaunchProfile, options?: RpcSessionOpenOptions): Promise<OpenRpcSession>;
|
|
24
28
|
peek(handle: string): RpcSessionEntry | undefined;
|
|
25
29
|
getForCommand(handle: string, command: string): RpcSessionEntry;
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { isAbsolute } from "node:path";
|
|
2
2
|
import { ProviderScope } from "@earendil-works/pi-ai/node/provider-scope";
|
|
3
3
|
import { assertValidSessionId } from "../../core/session-manager.js";
|
|
4
|
+
import { SESSION_PATH_RETRY_AFTER_MS, } from "./host-reservations.js";
|
|
4
5
|
import { refreshesSessionActivity } from "./session-command-activity.js";
|
|
5
6
|
import { RESERVATION_DENIAL_CODES, SessionPathReservations, } from "./session-path-reservations.js";
|
|
6
7
|
import { frozenProfile, RpcSessionRegistryError, sessionIdentity, } from "./session-registry.js";
|
|
8
|
+
import { releaseWithinGrace } from "./session-teardown.js";
|
|
7
9
|
import { SessionWorkerClient } from "./session-worker-client.js";
|
|
8
10
|
import { SESSION_WORKER_LIMITS, typedWorkerRefusal } from "./session-worker-protocol.js";
|
|
11
|
+
import { WorkerSessionClaims } from "./worker-session-claims.js";
|
|
9
12
|
/** Transport-side lifecycle owner. Caller paths are never inspected on this event loop. */
|
|
10
13
|
export class WorkerSessionRegistry {
|
|
11
14
|
constructor(options) {
|
|
12
15
|
this.entries = new Map();
|
|
13
16
|
this.reservations = new SessionPathReservations();
|
|
17
|
+
this.claims = new Map();
|
|
14
18
|
this.serial = 0;
|
|
15
19
|
this.options = options;
|
|
16
20
|
this.closeGraceMs = options.closeGraceMs;
|
|
@@ -20,6 +24,9 @@ export class WorkerSessionRegistry {
|
|
|
20
24
|
get size() {
|
|
21
25
|
return this.entries.size;
|
|
22
26
|
}
|
|
27
|
+
holderPids(observedStarts) {
|
|
28
|
+
return this.options.pathReservations?.holderPids?.(observedStarts) ?? Promise.resolve([]);
|
|
29
|
+
}
|
|
23
30
|
async openSession(profile, options) {
|
|
24
31
|
if (!isAbsolute(profile.cwd) || (profile.sessionPath !== undefined && !isAbsolute(profile.sessionPath)))
|
|
25
32
|
throw new RpcSessionRegistryError("invalid_path");
|
|
@@ -57,6 +64,8 @@ export class WorkerSessionRegistry {
|
|
|
57
64
|
if (this.size >= SESSION_WORKER_LIMITS.workers)
|
|
58
65
|
throw new Error("too_many_sessions");
|
|
59
66
|
const handle = `rpc-${++this.serial}`;
|
|
67
|
+
const claims = new WorkerSessionClaims(this.options.pathReservations);
|
|
68
|
+
this.claims.set(handle, claims);
|
|
60
69
|
const storedProfile = frozenProfile(profile);
|
|
61
70
|
const entry = {
|
|
62
71
|
state: "opening",
|
|
@@ -80,10 +89,16 @@ export class WorkerSessionRegistry {
|
|
|
80
89
|
exit: () => {
|
|
81
90
|
if (this.entries.get(handle) !== entry)
|
|
82
91
|
return;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
const finish = () => {
|
|
93
|
+
if (this.entries.get(handle) !== entry)
|
|
94
|
+
return;
|
|
95
|
+
entry.state = "closed";
|
|
96
|
+
this.entries.delete(handle);
|
|
97
|
+
this.claims.delete(handle);
|
|
98
|
+
this.reservations.releaseAll(handle);
|
|
99
|
+
entry.closeResolve?.();
|
|
100
|
+
};
|
|
101
|
+
return releaseWithinGrace({ closeGraceMs: this.closeGraceMs, releaseReservation: () => claims.close() }, handle, entry.reservationKey ?? handle).then(finish);
|
|
87
102
|
},
|
|
88
103
|
failure: (error) => {
|
|
89
104
|
// The open below only learns that its entry left `opening`, never why. Without this the
|
|
@@ -105,7 +120,13 @@ export class WorkerSessionRegistry {
|
|
|
105
120
|
worker.quarantine();
|
|
106
121
|
return attached;
|
|
107
122
|
}
|
|
108
|
-
const
|
|
123
|
+
const predecessor = await claims.claim(path);
|
|
124
|
+
if (predecessor)
|
|
125
|
+
throw new RpcSessionRegistryError("session_path_in_use", undefined, {
|
|
126
|
+
owner: predecessor,
|
|
127
|
+
retry_after_ms: SESSION_PATH_RETRY_AFTER_MS,
|
|
128
|
+
});
|
|
129
|
+
const grant = await this.reserve(handle, path);
|
|
109
130
|
if (grant !== "granted")
|
|
110
131
|
throw new RpcSessionRegistryError(RESERVATION_DENIAL_CODES[grant]);
|
|
111
132
|
entry.reservationKey = path;
|
|
@@ -163,6 +184,9 @@ export class WorkerSessionRegistry {
|
|
|
163
184
|
// attachments; only an explicit close or eviction reaches the worker.
|
|
164
185
|
if (options?.detach && entry.retainOnDisconnect) {
|
|
165
186
|
entry.attachments = 0;
|
|
187
|
+
entry.detachedAt ??= this.now();
|
|
188
|
+
if (!entry.worker?.busy)
|
|
189
|
+
this.claims.get(handle)?.setAttached(false);
|
|
166
190
|
return entry;
|
|
167
191
|
}
|
|
168
192
|
entry.state = "closing";
|
|
@@ -253,6 +277,7 @@ export class WorkerSessionRegistry {
|
|
|
253
277
|
}
|
|
254
278
|
const result = this.openResult(owner, entry);
|
|
255
279
|
entry.attachments++;
|
|
280
|
+
this.claims.get(owner)?.setAttached(true);
|
|
256
281
|
// Retention is a property of the live session: any attach may ask for it, and
|
|
257
282
|
// no attach may revoke it for the clients that already rely on it.
|
|
258
283
|
if (options?.retainOnDisconnect)
|
|
@@ -264,7 +289,7 @@ export class WorkerSessionRegistry {
|
|
|
264
289
|
reservationCount(handle) {
|
|
265
290
|
return this.reservations.count(handle);
|
|
266
291
|
}
|
|
267
|
-
reserve(handle, path) {
|
|
292
|
+
async reserve(handle, path) {
|
|
268
293
|
const entry = this.entries.get(handle);
|
|
269
294
|
if (!entry)
|
|
270
295
|
return "conflict";
|
|
@@ -272,7 +297,16 @@ export class WorkerSessionRegistry {
|
|
|
272
297
|
return "granted";
|
|
273
298
|
if (entry.state !== "opening" && entry.state !== "open")
|
|
274
299
|
return "conflict";
|
|
275
|
-
|
|
300
|
+
const grant = this.reservations.reserve(handle, path, this.liveWorkerPaths(entry));
|
|
301
|
+
if (grant !== "granted")
|
|
302
|
+
return grant;
|
|
303
|
+
const owner = await this.claims.get(handle)?.claim(path, entry.attachments > 0 || entry.worker?.busy === true);
|
|
304
|
+
if (owner || this.entries.get(handle) !== entry || (entry.state !== "opening" && entry.state !== "open")) {
|
|
305
|
+
const live = this.liveWorkerPaths(entry);
|
|
306
|
+
this.reservations.reconcile(handle, live ?? { livePaths: [], sessionPath: entry.reservationKey });
|
|
307
|
+
return "conflict";
|
|
308
|
+
}
|
|
309
|
+
return "granted";
|
|
276
310
|
}
|
|
277
311
|
/**
|
|
278
312
|
* Releases the grants a worker's latest snapshot no longer claims.
|
|
@@ -281,12 +315,21 @@ export class WorkerSessionRegistry {
|
|
|
281
315
|
* quarantined keeps every grant until its real exit, because a worker stuck in a
|
|
282
316
|
* syscall can still be writing a path it can no longer report.
|
|
283
317
|
*/
|
|
284
|
-
reconcile(handle, livePaths) {
|
|
318
|
+
async reconcile(handle, livePaths) {
|
|
285
319
|
const entry = this.entries.get(handle);
|
|
286
320
|
if (entry?.state !== "open")
|
|
287
321
|
return;
|
|
288
322
|
const sessionPath = entry.worker?.snapshot?.sessionPath;
|
|
323
|
+
const attached = entry.attachments > 0 || entry.worker?.busy === true;
|
|
324
|
+
if (sessionPath) {
|
|
325
|
+
const owner = await this.claims.get(handle)?.claim(sessionPath, attached);
|
|
326
|
+
if (owner)
|
|
327
|
+
throw new RpcSessionRegistryError("session_path_in_use");
|
|
328
|
+
if (this.entries.get(handle) !== entry || entry.state !== "open")
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
289
331
|
this.reservations.reconcile(handle, { livePaths, sessionPath });
|
|
332
|
+
this.claims.get(handle)?.reconcile(sessionPath ? [...livePaths, sessionPath] : livePaths, attached);
|
|
290
333
|
if (!sessionPath)
|
|
291
334
|
return;
|
|
292
335
|
entry.reservationKey = sessionPath;
|
package/dist/utils/shell.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type WindowsProcessRow } from "@earendil-works/pi-agent-core/node";
|
|
1
2
|
/** Family of a resolved shell executable, used to pick invocation arguments. */
|
|
2
3
|
export type ShellKind = "bash" | "sh" | "cmd" | "powershell";
|
|
3
4
|
export interface ShellConfig {
|
|
@@ -63,7 +64,10 @@ export declare function killTrackedDetachedChildren(): void;
|
|
|
63
64
|
*/
|
|
64
65
|
export declare function windowsTaskkillCandidates(env?: NodeJS.ProcessEnv): string[];
|
|
65
66
|
/**
|
|
66
|
-
* Kill a process and
|
|
67
|
+
* Kill a process and its descendants on Windows: one process listing (bounded by `TASKKILL_TIMEOUT_MS`)
|
|
68
|
+
* decides the tree, a process counting as a child only when it started at or after the parent it names,
|
|
69
|
+
* and `taskkill /F` ends each pid by name. `/T` would also adopt an unrelated older process through a
|
|
70
|
+
* recycled parent pid (senpi#2999); it is used only when no listing can be read.
|
|
67
71
|
*
|
|
68
72
|
* Synchronous on purpose. Shutdown paths call `killTrackedDetachedChildren()` and then
|
|
69
73
|
* `process.exit()` in the same tick (`emergencyTerminalExit()`), so neither an
|
|
@@ -78,7 +82,7 @@ export declare function windowsTaskkillCandidates(env?: NodeJS.ProcessEnv): stri
|
|
|
78
82
|
* Nothing in-process can walk a Windows process tree without an external tool, so this
|
|
79
83
|
* still beats leaving the whole tree running.
|
|
80
84
|
*/
|
|
81
|
-
export declare function killWindowsProcessTree(pid: number, taskkillPaths?: string[]): void;
|
|
85
|
+
export declare function killWindowsProcessTree(pid: number, taskkillPaths?: string[], listProcesses?: () => readonly WindowsProcessRow[] | undefined): void;
|
|
82
86
|
/**
|
|
83
87
|
* Kill a process and all its children (cross-platform)
|
|
84
88
|
*/
|