pi-session-mcp 0.0.0-stage → 0.6.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/.pi-session-mcp-cli/config.js +88 -0
- package/.pi-session-mcp-cli/doctor-cli.js +11 -0
- package/.pi-session-mcp-cli/doctor.js +178 -0
- package/.pi-session-mcp-cli/execution-profile.js +29 -0
- package/.pi-session-mcp-cli/mcp-config.js +70 -0
- package/.pi-session-mcp-cli/package-metadata.js +32 -0
- package/CHANGELOG.md +62 -0
- package/CONTRIBUTING.md +57 -0
- package/LICENSE +21 -0
- package/README.md +183 -2
- package/SECURITY.md +37 -0
- package/dist/capabilities.d.ts +25 -0
- package/dist/capabilities.js +26 -0
- package/dist/client-adapters/claude-code.d.ts +4 -0
- package/dist/client-adapters/claude-code.js +33 -0
- package/dist/client-adapters/codex.d.ts +4 -0
- package/dist/client-adapters/codex.js +73 -0
- package/dist/client-adapters/command.d.ts +12 -0
- package/dist/client-adapters/command.js +25 -0
- package/dist/client-adapters/index.d.ts +4 -0
- package/dist/client-adapters/index.js +3 -0
- package/dist/client-adapters/mcp-verifier.d.ts +20 -0
- package/dist/client-adapters/mcp-verifier.js +174 -0
- package/dist/client-adapters/types.d.ts +54 -0
- package/dist/client-adapters/types.js +1 -0
- package/dist/config.d.ts +50 -0
- package/dist/config.js +88 -0
- package/dist/doctor-cli.d.ts +1 -0
- package/dist/doctor-cli.js +11 -0
- package/dist/doctor.d.ts +47 -0
- package/dist/doctor.js +178 -0
- package/dist/entry-point.d.ts +5 -0
- package/dist/entry-point.js +16 -0
- package/dist/execution-profile.d.ts +37 -0
- package/dist/execution-profile.js +29 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +51 -0
- package/dist/mcp-config.d.ts +22 -0
- package/dist/mcp-config.js +70 -0
- package/dist/mcp-session-tools.d.ts +54 -0
- package/dist/mcp-session-tools.js +323 -0
- package/dist/mcp-stdio-client.d.ts +3 -0
- package/dist/mcp-stdio-client.js +59 -0
- package/dist/package-metadata.d.ts +18 -0
- package/dist/package-metadata.js +32 -0
- package/dist/pi-adapter.d.ts +42 -0
- package/dist/pi-adapter.js +22 -0
- package/dist/process-diagnostics.d.ts +8 -0
- package/dist/process-diagnostics.js +9 -0
- package/dist/sdk-pi-adapter.d.ts +75 -0
- package/dist/sdk-pi-adapter.js +410 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +102 -0
- package/dist/session-registry.d.ts +104 -0
- package/dist/session-registry.js +373 -0
- package/dist/setup-cli.d.ts +11 -0
- package/dist/setup-cli.js +38 -0
- package/dist/setup-command-guardian.d.ts +2 -0
- package/dist/setup-command-guardian.js +297 -0
- package/dist/setup-main.d.ts +8 -0
- package/dist/setup-main.js +60 -0
- package/dist/setup-ownership.d.ts +53 -0
- package/dist/setup-ownership.js +228 -0
- package/dist/setup-process.d.ts +30 -0
- package/dist/setup-process.js +385 -0
- package/dist/setup-result.d.ts +22 -0
- package/dist/setup-result.js +8 -0
- package/dist/setup.d.ts +55 -0
- package/dist/setup.js +327 -0
- package/dist/shutdown.d.ts +10 -0
- package/dist/shutdown.js +52 -0
- package/docs/acceptance.md +227 -0
- package/docs/adr/0001-stdio-first.md +12 -0
- package/docs/adr/0002-embedded-pi-sdk.md +11 -0
- package/docs/adr/0003-post-mvp-maintenance.md +60 -0
- package/docs/adr/0004-client-contract-not-version.md +31 -0
- package/docs/architecture.md +82 -0
- package/docs/client-setup-design.md +90 -0
- package/docs/client-setup.md +207 -0
- package/docs/doctor-design.md +47 -0
- package/docs/installation.md +352 -0
- package/docs/releasing.md +216 -0
- package/docs/session-mcp.md +260 -0
- package/docs/threat-model.md +89 -0
- package/docs/tool-contracts.md +143 -0
- package/package.json +64 -4
- package/pi-session-mcp.example.json +21 -0
- package/scripts/doctor.mjs +72 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { type PiSessionAdapter } from "./pi-adapter.js";
|
|
2
|
+
import type { PermissionProfile, ResolvedExecutionProfile, ThinkingLevel } from "./execution-profile.js";
|
|
3
|
+
export type SessionState = "idle" | "running" | "failed" | "closing" | "closed";
|
|
4
|
+
export type TurnState = "running" | "completed" | "failed" | "aborted";
|
|
5
|
+
export interface SessionView {
|
|
6
|
+
id: string;
|
|
7
|
+
workspace: string;
|
|
8
|
+
executionProfile: string;
|
|
9
|
+
profile: PermissionProfile;
|
|
10
|
+
provider: string;
|
|
11
|
+
model: string;
|
|
12
|
+
thinkingLevel: ThinkingLevel;
|
|
13
|
+
state: SessionState;
|
|
14
|
+
createdAt: string;
|
|
15
|
+
updatedAt: string;
|
|
16
|
+
lastError?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* `updatedAt` is the registry's own last-activity stamp: while a turn runs it advances on a
|
|
20
|
+
* completed tool execution or completed assistant message, then freezes at the terminal
|
|
21
|
+
* transition. It is a liveness lower bound, not a progress count, and does not prove that a
|
|
22
|
+
* silent turn is healthy.
|
|
23
|
+
*/
|
|
24
|
+
export interface TurnView {
|
|
25
|
+
turnId: string;
|
|
26
|
+
sessionId: string;
|
|
27
|
+
state: TurnState;
|
|
28
|
+
startedAt: string;
|
|
29
|
+
updatedAt: string;
|
|
30
|
+
completedAt?: string;
|
|
31
|
+
assistantText?: string;
|
|
32
|
+
truncated?: boolean;
|
|
33
|
+
error?: {
|
|
34
|
+
code: "turn_failed";
|
|
35
|
+
message: "Pi turn failed";
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export interface PromptResult {
|
|
39
|
+
session: SessionView;
|
|
40
|
+
turn: TurnView;
|
|
41
|
+
}
|
|
42
|
+
export type RegistryErrorCode = "unknown_workspace" | "unknown_session" | "session_running" | "prompt_rejected" | "prompt_timeout" | "abort_failed" | "abort_timeout" | "unknown_turn" | "unknown_execution_profile" | "server_stopping";
|
|
43
|
+
export declare class RegistryError extends Error {
|
|
44
|
+
readonly code: RegistryErrorCode;
|
|
45
|
+
constructor(code: RegistryErrorCode);
|
|
46
|
+
}
|
|
47
|
+
export declare const MAX_TURNS_PER_SESSION = 20;
|
|
48
|
+
export declare const MAX_TURN_TEXT_BYTES: number;
|
|
49
|
+
export declare const MAX_CLOSED_TOMBSTONES = 100;
|
|
50
|
+
export declare const PREFLIGHT_DEADLINE_MS = 5000;
|
|
51
|
+
export declare const ABORT_DEADLINE_MS = 5000;
|
|
52
|
+
export declare const CLOSE_CLEANUP_DEADLINE_MS = 5000;
|
|
53
|
+
export declare const SHUTDOWN_DEADLINE_MS = 12000;
|
|
54
|
+
export interface SessionRegistryOptions {
|
|
55
|
+
preflightDeadlineMs?: number;
|
|
56
|
+
abortDeadlineMs?: number;
|
|
57
|
+
closeCleanupDeadlineMs?: number;
|
|
58
|
+
shutdownDeadlineMs?: number;
|
|
59
|
+
}
|
|
60
|
+
export declare class SessionRegistry {
|
|
61
|
+
private readonly adapter;
|
|
62
|
+
private readonly workspaces;
|
|
63
|
+
private readonly sessions;
|
|
64
|
+
private readonly closed;
|
|
65
|
+
private readonly pendingStarts;
|
|
66
|
+
private readonly options;
|
|
67
|
+
private stopping;
|
|
68
|
+
private readonly shutdownController;
|
|
69
|
+
constructor(adapter: PiSessionAdapter, workspaces: ReadonlyMap<string, string>, options?: SessionRegistryOptions);
|
|
70
|
+
start(workspace: string, executionProfile: ResolvedExecutionProfile): Promise<SessionView>;
|
|
71
|
+
private createAndRegister;
|
|
72
|
+
list(): SessionView[];
|
|
73
|
+
get(id: string): SessionView;
|
|
74
|
+
getTurn(sessionId: string, turnId: string): TurnView;
|
|
75
|
+
prompt(id: string, text: string): Promise<PromptResult>;
|
|
76
|
+
abort(id: string): Promise<SessionView>;
|
|
77
|
+
close(id: string): Promise<SessionView>;
|
|
78
|
+
shutdown(): Promise<void>;
|
|
79
|
+
private require;
|
|
80
|
+
private verifySelection;
|
|
81
|
+
private completeTurn;
|
|
82
|
+
private failTurn;
|
|
83
|
+
/**
|
|
84
|
+
* Liveness only. A completed tool execution or assistant message advances this turn,
|
|
85
|
+
* never another turn or the session. Activity after accepted preflight but before
|
|
86
|
+
* publication is stamped and applied only if the turn is published; callbacks before
|
|
87
|
+
* acceptance or after terminal cleanup cannot publish or revive a turn.
|
|
88
|
+
*/
|
|
89
|
+
private recordActivity;
|
|
90
|
+
/** The registry's own clock: never earlier than `current`, even across a wall-clock step. */
|
|
91
|
+
private advance;
|
|
92
|
+
private publishTurn;
|
|
93
|
+
private restorePreflight;
|
|
94
|
+
private trim;
|
|
95
|
+
private beginAbort;
|
|
96
|
+
private quarantineTimedOutPreflight;
|
|
97
|
+
private dispose;
|
|
98
|
+
private disposeHandle;
|
|
99
|
+
private rememberClosed;
|
|
100
|
+
private turnView;
|
|
101
|
+
private view;
|
|
102
|
+
private withTimeout;
|
|
103
|
+
private serial;
|
|
104
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { PiSessionCreationError } from "./pi-adapter.js";
|
|
3
|
+
const REGISTRY_ERROR_MESSAGES = {
|
|
4
|
+
unknown_workspace: "Unknown workspace",
|
|
5
|
+
unknown_session: "Unknown session",
|
|
6
|
+
session_running: "Session already has a running turn",
|
|
7
|
+
prompt_rejected: "Pi rejected the prompt during preflight",
|
|
8
|
+
prompt_timeout: "Pi prompt preflight timed out",
|
|
9
|
+
abort_failed: "Pi abort failed",
|
|
10
|
+
abort_timeout: "Pi abort timed out",
|
|
11
|
+
unknown_turn: "Unknown turn",
|
|
12
|
+
unknown_execution_profile: "Unknown execution profile",
|
|
13
|
+
server_stopping: "Server is shutting down",
|
|
14
|
+
};
|
|
15
|
+
export class RegistryError extends Error {
|
|
16
|
+
code;
|
|
17
|
+
constructor(code) {
|
|
18
|
+
super(REGISTRY_ERROR_MESSAGES[code]);
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.name = "RegistryError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export const MAX_TURNS_PER_SESSION = 20;
|
|
24
|
+
export const MAX_TURN_TEXT_BYTES = 64 * 1024;
|
|
25
|
+
export const MAX_CLOSED_TOMBSTONES = 100;
|
|
26
|
+
export const PREFLIGHT_DEADLINE_MS = 5_000;
|
|
27
|
+
export const ABORT_DEADLINE_MS = 5_000;
|
|
28
|
+
export const CLOSE_CLEANUP_DEADLINE_MS = 5_000;
|
|
29
|
+
export const SHUTDOWN_DEADLINE_MS = 12_000;
|
|
30
|
+
const stamp = () => new Date().toISOString();
|
|
31
|
+
function normalize(text) { const value = text.trim(); if (Buffer.byteLength(value, "utf8") <= MAX_TURN_TEXT_BYTES)
|
|
32
|
+
return { text: value, truncated: false }; let out = ""; let size = 0; for (const c of value) {
|
|
33
|
+
const n = Buffer.byteLength(c, "utf8");
|
|
34
|
+
if (size + n > MAX_TURN_TEXT_BYTES)
|
|
35
|
+
break;
|
|
36
|
+
out += c;
|
|
37
|
+
size += n;
|
|
38
|
+
} return { text: out, truncated: true }; }
|
|
39
|
+
export class SessionRegistry {
|
|
40
|
+
adapter;
|
|
41
|
+
workspaces;
|
|
42
|
+
sessions = new Map();
|
|
43
|
+
closed = new Map();
|
|
44
|
+
pendingStarts = new Set();
|
|
45
|
+
options;
|
|
46
|
+
stopping = false;
|
|
47
|
+
shutdownController = new AbortController();
|
|
48
|
+
constructor(adapter, workspaces, options = {}) {
|
|
49
|
+
this.adapter = adapter;
|
|
50
|
+
this.workspaces = workspaces;
|
|
51
|
+
this.options = { preflightDeadlineMs: options.preflightDeadlineMs ?? PREFLIGHT_DEADLINE_MS, abortDeadlineMs: options.abortDeadlineMs ?? ABORT_DEADLINE_MS, closeCleanupDeadlineMs: options.closeCleanupDeadlineMs ?? CLOSE_CLEANUP_DEADLINE_MS, shutdownDeadlineMs: options.shutdownDeadlineMs ?? SHUTDOWN_DEADLINE_MS };
|
|
52
|
+
}
|
|
53
|
+
async start(workspace, executionProfile) {
|
|
54
|
+
if (this.stopping)
|
|
55
|
+
throw new RegistryError("server_stopping");
|
|
56
|
+
const cwd = this.workspaces.get(workspace);
|
|
57
|
+
if (!cwd)
|
|
58
|
+
throw new RegistryError("unknown_workspace");
|
|
59
|
+
// Track registration and orphan cleanup too, not just the raw adapter promise.
|
|
60
|
+
const start = this.createAndRegister(cwd, workspace, executionProfile);
|
|
61
|
+
this.pendingStarts.add(start);
|
|
62
|
+
try {
|
|
63
|
+
return await start;
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
this.pendingStarts.delete(start);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async createAndRegister(cwd, workspace, executionProfile) {
|
|
70
|
+
const handle = await Promise.resolve().then(() => this.adapter.create({ cwd, executionProfile, signal: this.shutdownController.signal }));
|
|
71
|
+
try {
|
|
72
|
+
if (this.stopping)
|
|
73
|
+
throw new RegistryError("server_stopping");
|
|
74
|
+
this.verifySelection(handle.appliedSelection, executionProfile);
|
|
75
|
+
const time = stamp();
|
|
76
|
+
const entry = {
|
|
77
|
+
id: randomUUID(), workspace, executionProfile: executionProfile.alias, profile: executionProfile.permissionProfile,
|
|
78
|
+
provider: handle.appliedSelection.provider, model: handle.appliedSelection.model,
|
|
79
|
+
thinkingLevel: handle.appliedSelection.thinkingLevel, state: "idle", createdAt: time, updatedAt: time,
|
|
80
|
+
handle, operation: Promise.resolve(), disposed: false, turns: new Map(), order: [],
|
|
81
|
+
};
|
|
82
|
+
this.sessions.set(entry.id, entry);
|
|
83
|
+
return this.view(entry);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
await this.disposeHandle(handle);
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
list() { return [...this.sessions.values()].map((e) => this.view(e)); }
|
|
91
|
+
get(id) { return this.view(this.require(id)); }
|
|
92
|
+
getTurn(sessionId, turnId) {
|
|
93
|
+
const entry = this.require(sessionId);
|
|
94
|
+
const turn = entry.turns.get(turnId);
|
|
95
|
+
if (!turn)
|
|
96
|
+
throw new RegistryError("unknown_turn");
|
|
97
|
+
return this.turnView(turn);
|
|
98
|
+
}
|
|
99
|
+
async prompt(id, text) {
|
|
100
|
+
const entry = this.require(id);
|
|
101
|
+
return this.serial(entry, async () => {
|
|
102
|
+
if (entry.state === "running" || entry.abortPromise)
|
|
103
|
+
throw new RegistryError("session_running");
|
|
104
|
+
if (entry.state === "closing" || entry.state === "closed")
|
|
105
|
+
throw new RegistryError("unknown_session");
|
|
106
|
+
const previous = { state: entry.state, updatedAt: entry.updatedAt, lastError: entry.lastError };
|
|
107
|
+
const time = stamp();
|
|
108
|
+
const turn = { turnId: randomUUID(), sessionId: id, state: "running", startedAt: time, updatedAt: time, terminal: false };
|
|
109
|
+
entry.turn = turn;
|
|
110
|
+
entry.state = "running";
|
|
111
|
+
entry.updatedAt = time;
|
|
112
|
+
delete entry.lastError;
|
|
113
|
+
let accepted;
|
|
114
|
+
let signaled = false;
|
|
115
|
+
let signal;
|
|
116
|
+
const preflight = new Promise((resolve) => {
|
|
117
|
+
signal = (value) => {
|
|
118
|
+
if (signaled)
|
|
119
|
+
return;
|
|
120
|
+
signaled = true;
|
|
121
|
+
accepted = value;
|
|
122
|
+
resolve(value);
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
let pendingActivityAt;
|
|
126
|
+
const onActivity = () => {
|
|
127
|
+
if (accepted !== true || this.stopping || entry.disposed || entry.turn !== turn || turn.terminal)
|
|
128
|
+
return;
|
|
129
|
+
if (entry.turns.has(turn.turnId)) {
|
|
130
|
+
this.recordActivity(entry, turn);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const time = stamp();
|
|
134
|
+
if (pendingActivityAt === undefined || time > pendingActivityAt)
|
|
135
|
+
pendingActivityAt = time;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const publishAcceptedTurn = () => {
|
|
139
|
+
this.publishTurn(entry, turn);
|
|
140
|
+
if (pendingActivityAt !== undefined)
|
|
141
|
+
this.recordActivity(entry, turn, pendingActivityAt);
|
|
142
|
+
};
|
|
143
|
+
let operation;
|
|
144
|
+
try {
|
|
145
|
+
operation = Promise.resolve(entry.handle.prompt(text, signal, onActivity));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
if (accepted === true) {
|
|
149
|
+
publishAcceptedTurn();
|
|
150
|
+
this.failTurn(entry, turn);
|
|
151
|
+
return { session: this.view(entry), turn: this.turnView(turn) };
|
|
152
|
+
}
|
|
153
|
+
this.restorePreflight(entry, turn, undefined, previous);
|
|
154
|
+
throw new RegistryError("prompt_rejected");
|
|
155
|
+
}
|
|
156
|
+
entry.turnPromise = operation;
|
|
157
|
+
void operation.then(() => undefined, () => undefined);
|
|
158
|
+
const result = await this.withTimeout(Promise.race([
|
|
159
|
+
preflight.then((value) => ({ kind: "preflight", value })),
|
|
160
|
+
operation.then(() => ({ kind: "operation" }), () => ({ kind: "operation" })),
|
|
161
|
+
]), this.options.preflightDeadlineMs, { kind: "timeout" });
|
|
162
|
+
if (result.kind === "timeout") {
|
|
163
|
+
this.quarantineTimedOutPreflight(entry, turn, operation, previous);
|
|
164
|
+
throw new RegistryError("prompt_timeout");
|
|
165
|
+
}
|
|
166
|
+
if (result.kind === "operation" || result.value === false) {
|
|
167
|
+
this.restorePreflight(entry, turn, operation, previous);
|
|
168
|
+
throw new RegistryError("prompt_rejected");
|
|
169
|
+
}
|
|
170
|
+
publishAcceptedTurn();
|
|
171
|
+
void operation.then((outcome) => this.completeTurn(entry, turn, outcome), () => this.failTurn(entry, turn));
|
|
172
|
+
return { session: this.view(entry), turn: this.turnView(turn) };
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async abort(id) {
|
|
176
|
+
const entry = this.require(id);
|
|
177
|
+
const operation = this.serial(entry, async () => {
|
|
178
|
+
const turn = entry.turn;
|
|
179
|
+
if (entry.state !== "running" || !turn || turn.terminal)
|
|
180
|
+
return undefined;
|
|
181
|
+
return { turn, abortPromise: this.beginAbort(entry, turn) };
|
|
182
|
+
}).then(async (prepared) => {
|
|
183
|
+
if (!prepared)
|
|
184
|
+
return this.view(entry);
|
|
185
|
+
try {
|
|
186
|
+
await prepared.abortPromise;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
throw new RegistryError("abort_failed");
|
|
190
|
+
}
|
|
191
|
+
if (entry.turns.has(prepared.turn.turnId) && entry.turn === prepared.turn && !prepared.turn.terminal && !entry.disposed) {
|
|
192
|
+
this.completeTurn(entry, prepared.turn, { status: "aborted" });
|
|
193
|
+
}
|
|
194
|
+
return this.view(entry);
|
|
195
|
+
});
|
|
196
|
+
const result = await this.withTimeout(operation.then((session) => ({ kind: "completed", session }), () => ({ kind: "failed" })), this.options.abortDeadlineMs, { kind: "timeout" });
|
|
197
|
+
if (result.kind === "timeout")
|
|
198
|
+
throw new RegistryError("abort_timeout");
|
|
199
|
+
if (result.kind === "failed")
|
|
200
|
+
throw new RegistryError("abort_failed");
|
|
201
|
+
return result.session;
|
|
202
|
+
}
|
|
203
|
+
async close(id) {
|
|
204
|
+
const existing = this.sessions.get(id);
|
|
205
|
+
if (!existing) {
|
|
206
|
+
const closed = this.closed.get(id);
|
|
207
|
+
if (closed)
|
|
208
|
+
return { ...closed };
|
|
209
|
+
throw new RegistryError("unknown_session");
|
|
210
|
+
}
|
|
211
|
+
return this.serial(existing, async () => {
|
|
212
|
+
if (existing.disposed)
|
|
213
|
+
return this.view(existing);
|
|
214
|
+
existing.state = "closing";
|
|
215
|
+
existing.updatedAt = stamp();
|
|
216
|
+
const turn = existing.turn;
|
|
217
|
+
if (turn && !turn.terminal) {
|
|
218
|
+
turn.terminal = true;
|
|
219
|
+
turn.state = "aborted";
|
|
220
|
+
const time = this.advance(turn.updatedAt);
|
|
221
|
+
turn.updatedAt = time;
|
|
222
|
+
turn.completedAt = time;
|
|
223
|
+
const abortPromise = this.beginAbort(existing, turn);
|
|
224
|
+
await this.withTimeout(abortPromise.then(() => undefined, () => undefined), this.options.closeCleanupDeadlineMs, undefined);
|
|
225
|
+
}
|
|
226
|
+
existing.turn = undefined;
|
|
227
|
+
existing.turnPromise = undefined;
|
|
228
|
+
existing.turns.clear();
|
|
229
|
+
existing.order.length = 0;
|
|
230
|
+
await this.dispose(existing);
|
|
231
|
+
existing.state = "closed";
|
|
232
|
+
existing.updatedAt = stamp();
|
|
233
|
+
const view = this.view(existing);
|
|
234
|
+
this.rememberClosed(id, view);
|
|
235
|
+
this.sessions.delete(id);
|
|
236
|
+
return view;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
async shutdown() {
|
|
240
|
+
this.stopping = true;
|
|
241
|
+
this.shutdownController.abort();
|
|
242
|
+
const cleanup = Promise.all([
|
|
243
|
+
Promise.allSettled([...this.sessions.keys()].map((id) => this.close(id))),
|
|
244
|
+
Promise.allSettled([...this.pendingStarts]),
|
|
245
|
+
]).then(() => undefined);
|
|
246
|
+
await this.withTimeout(cleanup, this.options.shutdownDeadlineMs, undefined);
|
|
247
|
+
}
|
|
248
|
+
require(id) { const entry = this.sessions.get(id); if (!entry)
|
|
249
|
+
throw new RegistryError("unknown_session"); return entry; }
|
|
250
|
+
verifySelection(applied, expected) {
|
|
251
|
+
if (applied.provider !== expected.provider || applied.model !== expected.model || applied.thinkingLevel !== expected.thinkingLevel) {
|
|
252
|
+
throw new PiSessionCreationError("execution_selection_mismatch");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
completeTurn(entry, turn, outcome) { if (turn.terminal || entry.disposed)
|
|
256
|
+
return; turn.terminal = true; turn.updatedAt = this.advance(turn.updatedAt); turn.completedAt = turn.updatedAt; if (outcome.status === "completed") {
|
|
257
|
+
const normalized = normalize(outcome.assistantText);
|
|
258
|
+
turn.state = "completed";
|
|
259
|
+
turn.assistantText = normalized.text;
|
|
260
|
+
turn.truncated = normalized.truncated;
|
|
261
|
+
}
|
|
262
|
+
else if (outcome.status === "aborted")
|
|
263
|
+
turn.state = "aborted";
|
|
264
|
+
else {
|
|
265
|
+
turn.state = "failed";
|
|
266
|
+
turn.error = { code: "turn_failed", message: "Pi turn failed" };
|
|
267
|
+
entry.lastError = "Pi turn failed";
|
|
268
|
+
} if (entry.turn === turn)
|
|
269
|
+
entry.turn = undefined; if (entry.turnPromise)
|
|
270
|
+
entry.turnPromise = undefined; entry.state = turn.state === "failed" ? "failed" : "idle"; entry.updatedAt = turn.updatedAt; }
|
|
271
|
+
failTurn(entry, turn) { if (turn.terminal)
|
|
272
|
+
return; turn.terminal = true; turn.state = "failed"; turn.updatedAt = this.advance(turn.updatedAt); turn.completedAt = turn.updatedAt; turn.error = { code: "turn_failed", message: "Pi turn failed" }; if (entry.turn === turn)
|
|
273
|
+
entry.turn = undefined; if (entry.turnPromise)
|
|
274
|
+
entry.turnPromise = undefined; entry.state = "failed"; entry.updatedAt = turn.updatedAt; entry.lastError = "Pi turn failed"; }
|
|
275
|
+
/**
|
|
276
|
+
* Liveness only. A completed tool execution or assistant message advances this turn,
|
|
277
|
+
* never another turn or the session. Activity after accepted preflight but before
|
|
278
|
+
* publication is stamped and applied only if the turn is published; callbacks before
|
|
279
|
+
* acceptance or after terminal cleanup cannot publish or revive a turn.
|
|
280
|
+
*/
|
|
281
|
+
recordActivity(entry, turn, observedAt) {
|
|
282
|
+
if (this.stopping || entry.disposed)
|
|
283
|
+
return;
|
|
284
|
+
if (entry.turn !== turn || !entry.turns.has(turn.turnId))
|
|
285
|
+
return;
|
|
286
|
+
if (entry.state !== "running" || turn.state !== "running" || turn.terminal)
|
|
287
|
+
return;
|
|
288
|
+
const time = this.advance(turn.updatedAt, observedAt);
|
|
289
|
+
// Sub-millisecond activity is simply the same stamp; a timestamp never moves backwards.
|
|
290
|
+
if (time === turn.updatedAt)
|
|
291
|
+
return;
|
|
292
|
+
turn.updatedAt = time;
|
|
293
|
+
}
|
|
294
|
+
/** The registry's own clock: never earlier than `current`, even across a wall-clock step. */
|
|
295
|
+
advance(current, observedAt = stamp()) { return observedAt > current ? observedAt : current; }
|
|
296
|
+
publishTurn(entry, turn) { if (entry.turns.has(turn.turnId))
|
|
297
|
+
return; entry.turns.set(turn.turnId, turn); entry.order.push(turn.turnId); this.trim(entry); }
|
|
298
|
+
restorePreflight(entry, turn, operation, previous) { turn.terminal = true; if (entry.turn === turn)
|
|
299
|
+
entry.turn = undefined; if (!operation || entry.turnPromise === operation)
|
|
300
|
+
entry.turnPromise = undefined; entry.state = previous.state; entry.updatedAt = previous.updatedAt; if (previous.lastError === undefined)
|
|
301
|
+
delete entry.lastError;
|
|
302
|
+
else
|
|
303
|
+
entry.lastError = previous.lastError; }
|
|
304
|
+
trim(entry) { while (entry.order.length > MAX_TURNS_PER_SESSION) {
|
|
305
|
+
const old = entry.order.shift();
|
|
306
|
+
if (old)
|
|
307
|
+
entry.turns.delete(old);
|
|
308
|
+
} }
|
|
309
|
+
beginAbort(entry, turn) {
|
|
310
|
+
if (turn.abortPromise)
|
|
311
|
+
return turn.abortPromise;
|
|
312
|
+
const pending = Promise.resolve().then(() => entry.handle.abort());
|
|
313
|
+
turn.abortPromise = pending;
|
|
314
|
+
entry.abortPromise = pending;
|
|
315
|
+
void pending.then(() => {
|
|
316
|
+
if (entry.abortPromise === pending)
|
|
317
|
+
entry.abortPromise = undefined;
|
|
318
|
+
if (entry.turns.has(turn.turnId) && entry.turn === turn && !turn.terminal && !entry.disposed) {
|
|
319
|
+
this.completeTurn(entry, turn, { status: "aborted" });
|
|
320
|
+
}
|
|
321
|
+
}, () => {
|
|
322
|
+
if (entry.abortPromise === pending)
|
|
323
|
+
entry.abortPromise = undefined;
|
|
324
|
+
if (turn.abortPromise === pending)
|
|
325
|
+
turn.abortPromise = undefined;
|
|
326
|
+
});
|
|
327
|
+
return pending;
|
|
328
|
+
}
|
|
329
|
+
quarantineTimedOutPreflight(entry, turn, operation, previous) {
|
|
330
|
+
let promptSettled = false;
|
|
331
|
+
let abortSettled = false;
|
|
332
|
+
const releaseIfSafe = () => {
|
|
333
|
+
if (turn.terminal || entry.disposed || entry.turn !== turn)
|
|
334
|
+
return;
|
|
335
|
+
if (abortSettled && promptSettled)
|
|
336
|
+
this.restorePreflight(entry, turn, operation, previous);
|
|
337
|
+
};
|
|
338
|
+
void operation.then(() => { promptSettled = true; releaseIfSafe(); }, () => { promptSettled = true; releaseIfSafe(); });
|
|
339
|
+
const abortPromise = this.beginAbort(entry, turn);
|
|
340
|
+
void abortPromise.then(() => { abortSettled = true; releaseIfSafe(); }, () => { abortSettled = true; releaseIfSafe(); });
|
|
341
|
+
}
|
|
342
|
+
async dispose(entry) {
|
|
343
|
+
if (entry.disposed)
|
|
344
|
+
return;
|
|
345
|
+
entry.disposed = true;
|
|
346
|
+
await this.disposeHandle(entry.handle);
|
|
347
|
+
}
|
|
348
|
+
async disposeHandle(handle) {
|
|
349
|
+
try {
|
|
350
|
+
const cleanup = Promise.resolve(handle.dispose()).then(() => undefined, () => undefined);
|
|
351
|
+
await this.withTimeout(cleanup, this.options.closeCleanupDeadlineMs, undefined);
|
|
352
|
+
}
|
|
353
|
+
catch { /* Cleanup errors must never expose adapter internals. */ }
|
|
354
|
+
}
|
|
355
|
+
rememberClosed(id, view) { this.closed.set(id, view); while (this.closed.size > MAX_CLOSED_TOMBSTONES)
|
|
356
|
+
this.closed.delete(this.closed.keys().next().value); }
|
|
357
|
+
turnView({ terminal: _terminal, abortPromise: _abortPromise, ...turn }) { return { ...turn }; }
|
|
358
|
+
view({ handle: _handle, turn: _turn, turnPromise: _turnPromise, abortPromise: _abortPromise, operation: _operation, disposed: _disposed, turns: _turns, order: _order, ...view }) { return { ...view }; }
|
|
359
|
+
async withTimeout(promise, milliseconds, fallback) { if (milliseconds <= 0)
|
|
360
|
+
return fallback; let timer; const timeout = new Promise((resolve) => { timer = setTimeout(() => resolve(fallback), milliseconds); }); try {
|
|
361
|
+
return await Promise.race([promise, timeout]);
|
|
362
|
+
}
|
|
363
|
+
finally {
|
|
364
|
+
if (timer !== undefined)
|
|
365
|
+
clearTimeout(timer);
|
|
366
|
+
} }
|
|
367
|
+
async serial(entry, action) { const previous = entry.operation; let release; entry.operation = new Promise((resolve) => { release = resolve; }); await previous.catch(() => undefined); try {
|
|
368
|
+
return await action();
|
|
369
|
+
}
|
|
370
|
+
finally {
|
|
371
|
+
release();
|
|
372
|
+
} }
|
|
373
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { SetupOrchestrator, type SetupRequest } from "./setup.js";
|
|
2
|
+
import type { SetupResult } from "./setup-result.js";
|
|
3
|
+
/** Small argv adapter; command execution remains injected in SetupClientAdapter. */
|
|
4
|
+
export interface SetupCliRequest extends SetupRequest {
|
|
5
|
+
readonly json: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function parseSetupArgs(argv: readonly string[]): SetupCliRequest;
|
|
8
|
+
export declare function runSetupCli(argv: readonly string[], orchestrator: SetupOrchestrator): Promise<SetupResult>;
|
|
9
|
+
/** Both report modes are projections of the same sanitized result. */
|
|
10
|
+
export declare function formatSetupResult(setupResult: SetupResult): string;
|
|
11
|
+
export declare function formatSetupHuman(setupResult: SetupResult): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function parseSetupArgs(argv) {
|
|
2
|
+
let operation;
|
|
3
|
+
let json = false;
|
|
4
|
+
const targets = [];
|
|
5
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
6
|
+
const argument = argv[index];
|
|
7
|
+
if (argument === "--json") {
|
|
8
|
+
if (json)
|
|
9
|
+
throw new Error("json_repeated");
|
|
10
|
+
json = true;
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
if (argument === "--dry-run" || argument === "--apply" || argument === "--verify" || argument === "--rollback" || argument === "--remove") {
|
|
14
|
+
if (operation)
|
|
15
|
+
throw new Error("operation_repeated");
|
|
16
|
+
operation = argument.slice(2);
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (argument === "--target") {
|
|
20
|
+
const value = argv[++index];
|
|
21
|
+
const parts = value?.split(":");
|
|
22
|
+
if (!parts || parts.length !== 3)
|
|
23
|
+
throw new Error("target_invalid");
|
|
24
|
+
targets.push({ client: parts[0], scope: parts[1], alias: parts[2] });
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
throw new Error("argument_unsupported");
|
|
28
|
+
}
|
|
29
|
+
return operation ? { operation, targets, json } : { targets, json };
|
|
30
|
+
}
|
|
31
|
+
export async function runSetupCli(argv, orchestrator) {
|
|
32
|
+
return orchestrator.run(parseSetupArgs(argv));
|
|
33
|
+
}
|
|
34
|
+
/** Both report modes are projections of the same sanitized result. */
|
|
35
|
+
export function formatSetupResult(setupResult) {
|
|
36
|
+
return JSON.stringify(setupResult);
|
|
37
|
+
}
|
|
38
|
+
export function formatSetupHuman(setupResult) { return [`${setupResult.operation}: ${setupResult.status}`, ...setupResult.findings.map((x) => `${x.target.client}/${x.target.scope}/${x.target.alias}: ${x.status} (${x.code})`)].join("\n"); }
|