pi-session-mcp 0.11.9 → 0.12.1

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.
Files changed (75) hide show
  1. package/.pi-session-mcp-cli/client-adapters/mcp-verifier.js +6 -2
  2. package/.pi-session-mcp-cli/doctor.js +89 -4
  3. package/.pi-session-mcp-cli/execution-modes.js +19 -5
  4. package/.pi-session-mcp-cli/override-lock.js +4 -4
  5. package/.pi-session-mcp-cli/setup-process.js +5 -4
  6. package/.pi-session-mcp-cli/writer-boundary.js +10 -4
  7. package/.pi-session-mcp-cli/writer-fence.js +50 -0
  8. package/.pi-session-mcp-cli/writer-lease.js +140 -0
  9. package/.pi-session-mcp-cli/writer-lifecycle.js +55 -23
  10. package/.pi-session-mcp-cli/writer-state.js +4 -2
  11. package/CHANGELOG.md +91 -0
  12. package/README.md +3 -2
  13. package/dist/capabilities.d.ts +2 -0
  14. package/dist/capabilities.js +4 -1
  15. package/dist/child-stdio-transport.d.ts +21 -0
  16. package/dist/child-stdio-transport.js +88 -0
  17. package/dist/client-adapters/mcp-verifier.js +6 -2
  18. package/dist/client-adapters/types.d.ts +1 -0
  19. package/dist/doctor.d.ts +3 -1
  20. package/dist/doctor.js +89 -4
  21. package/dist/execution-modes.d.ts +14 -2
  22. package/dist/execution-modes.js +19 -5
  23. package/dist/main.js +9 -2
  24. package/dist/override-lock.d.ts +2 -0
  25. package/dist/override-lock.js +4 -4
  26. package/dist/pi-adapter.d.ts +30 -0
  27. package/dist/pi-adapter.js +11 -0
  28. package/dist/refusal-server.d.ts +1 -1
  29. package/dist/refusal-server.js +24 -2
  30. package/dist/server.d.ts +2 -2
  31. package/dist/server.js +18 -53
  32. package/dist/session-registry.d.ts +9 -2
  33. package/dist/session-registry.js +17 -12
  34. package/dist/setup-cli.js +7 -2
  35. package/dist/setup-hooks.js +0 -2
  36. package/dist/setup-modes.js +7 -1
  37. package/dist/setup-process.d.ts +3 -1
  38. package/dist/setup-process.js +5 -4
  39. package/dist/setup-writers.d.ts +5 -0
  40. package/dist/setup-writers.js +203 -94
  41. package/dist/tool-schemas.d.ts +339 -0
  42. package/dist/tool-schemas.js +49 -0
  43. package/dist/wake-hook.d.ts +4 -5
  44. package/dist/wake-hook.js +3 -6
  45. package/dist/writer-acceptance.js +10 -1
  46. package/dist/writer-boundary.d.ts +6 -4
  47. package/dist/writer-boundary.js +10 -4
  48. package/dist/writer-fence.d.ts +12 -0
  49. package/dist/writer-fence.js +50 -0
  50. package/dist/writer-launch.d.ts +9 -1
  51. package/dist/writer-launch.js +127 -30
  52. package/dist/writer-lease.d.ts +28 -0
  53. package/dist/writer-lease.js +140 -0
  54. package/dist/writer-lifecycle.d.ts +7 -1
  55. package/dist/writer-lifecycle.js +55 -23
  56. package/dist/writer-relay.d.ts +33 -0
  57. package/dist/writer-relay.js +359 -0
  58. package/dist/writer-state.d.ts +13 -1
  59. package/dist/writer-state.js +4 -2
  60. package/docs/acceptance.md +172 -0
  61. package/docs/adr/0012-leased-writer-pool.md +357 -0
  62. package/docs/architecture.md +61 -13
  63. package/docs/client-setup.md +11 -15
  64. package/docs/doctor-design.md +10 -4
  65. package/docs/execution-modes.md +87 -44
  66. package/docs/installation.md +43 -22
  67. package/docs/isolation-setup.md +88 -102
  68. package/docs/roadmap.md +6 -0
  69. package/docs/threat-model.md +7 -4
  70. package/docs/tool-contracts.md +29 -12
  71. package/package.json +1 -1
  72. package/scripts/isolation-acceptance.mjs +7 -1
  73. package/skills/pi-session-implementation/SKILL.md +1 -0
  74. package/skills/pi-session-orchestration/SKILL.md +6 -5
  75. package/skills/pi-session-orchestration/references/start-errors.md +5 -1
@@ -1,6 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { PiSessionCreationError } from "./pi-adapter.js";
3
3
  const REGISTRY_ERROR_MESSAGES = {
4
+ writer_session_lost: "Writer session was lost",
5
+ base_not_applicable: "Base is only applicable to coding sessions on container workspaces",
4
6
  unknown_workspace: "Unknown workspace",
5
7
  writer_uncollected: "Writer is not at its collected baseline",
6
8
  writer_busy: "Writer already has an open coding session",
@@ -58,7 +60,7 @@ export class SessionRegistry {
58
60
  this.onTurnTerminal = options.onTurnTerminal;
59
61
  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 };
60
62
  }
61
- async start(workspace, executionProfile) {
63
+ async start(workspace, executionProfile, options = {}) {
62
64
  if (this.stopping)
63
65
  throw new RegistryError("server_stopping");
64
66
  const cwd = this.workspaces.get(workspace);
@@ -66,8 +68,8 @@ export class SessionRegistry {
66
68
  throw new RegistryError("unknown_workspace");
67
69
  // Track registration and orphan cleanup too, not just the raw adapter promise.
68
70
  const start = this.writerGuard && executionProfile.permissionProfile === "coding"
69
- ? this.guardedCreate(cwd, workspace, executionProfile)
70
- : this.createAndRegister(cwd, workspace, executionProfile);
71
+ ? this.guardedCreate(cwd, workspace, executionProfile, options)
72
+ : this.createAndRegister(cwd, workspace, executionProfile, options);
71
73
  this.pendingStarts.add(start);
72
74
  try {
73
75
  return await start;
@@ -76,7 +78,7 @@ export class SessionRegistry {
76
78
  this.pendingStarts.delete(start);
77
79
  }
78
80
  }
79
- async guardedCreate(cwd, workspace, executionProfile) {
81
+ async guardedCreate(cwd, workspace, executionProfile, options) {
80
82
  const previous = this.codingAdmissions.get(workspace) ?? Promise.resolve();
81
83
  let release;
82
84
  const admission = new Promise((resolve) => { release = resolve; });
@@ -89,7 +91,7 @@ export class SessionRegistry {
89
91
  await this.writerGuard.check(workspace, open);
90
92
  if (this.stopping)
91
93
  throw new RegistryError("server_stopping");
92
- return await this.createAndRegister(cwd, workspace, executionProfile);
94
+ return await this.createAndRegister(cwd, workspace, executionProfile, options);
93
95
  }
94
96
  finally {
95
97
  release();
@@ -97,8 +99,8 @@ export class SessionRegistry {
97
99
  this.codingAdmissions.delete(workspace);
98
100
  }
99
101
  }
100
- async createAndRegister(cwd, workspace, executionProfile) {
101
- const handle = await Promise.resolve().then(() => this.adapter.create({ cwd, executionProfile, signal: this.shutdownController.signal }));
102
+ async createAndRegister(cwd, workspace, executionProfile, options) {
103
+ const handle = await Promise.resolve().then(() => this.adapter.create({ cwd, workspace, ...(options.base === undefined ? {} : { base: options.base }), executionProfile, signal: this.shutdownController.signal }));
102
104
  try {
103
105
  if (this.stopping)
104
106
  throw new RegistryError("server_stopping");
@@ -108,6 +110,7 @@ export class SessionRegistry {
108
110
  id: randomUUID(), workspace, executionProfile: executionProfile.alias, profile: executionProfile.permissionProfile,
109
111
  provider: handle.appliedSelection.provider, model: handle.appliedSelection.model,
110
112
  thinkingLevel: handle.appliedSelection.thinkingLevel, state: "idle", createdAt: time, updatedAt: time,
113
+ ...(handle.placement ? { writer: handle.placement.writer, base: { ...handle.placement.base } } : {}),
111
114
  handle, operation: Promise.resolve(), disposed: false, turns: new Map(), order: [],
112
115
  };
113
116
  this.sessions.set(entry.id, entry);
@@ -130,6 +133,8 @@ export class SessionRegistry {
130
133
  async prompt(id, text) {
131
134
  const entry = this.require(id);
132
135
  return this.serial(entry, async () => {
136
+ if (entry.handle.isLost?.())
137
+ throw new RegistryError("writer_session_lost");
133
138
  if (entry.state === "running" || entry.abortPromise)
134
139
  throw new RegistryError("session_running");
135
140
  if (entry.state === "closing" || entry.state === "closed")
@@ -189,7 +194,7 @@ export class SessionRegistry {
189
194
  const result = await this.withTimeout(Promise.race([
190
195
  preflight.then((value) => ({ kind: "preflight", value })),
191
196
  operation.then(() => ({ kind: "operation" }), () => ({ kind: "operation" })),
192
- ]), this.options.preflightDeadlineMs, { kind: "timeout" });
197
+ ]), entry.handle.deadlines?.preflightMs ?? this.options.preflightDeadlineMs, { kind: "timeout" });
193
198
  if (result.kind === "timeout") {
194
199
  this.quarantineTimedOutPreflight(entry, turn, operation, previous);
195
200
  throw new RegistryError("prompt_timeout");
@@ -224,7 +229,7 @@ export class SessionRegistry {
224
229
  }
225
230
  return this.view(entry);
226
231
  });
227
- const result = await this.withTimeout(operation.then((session) => ({ kind: "completed", session }), () => ({ kind: "failed" })), this.options.abortDeadlineMs, { kind: "timeout" });
232
+ const result = await this.withTimeout(operation.then((session) => ({ kind: "completed", session }), () => ({ kind: "failed" })), entry.handle.deadlines?.abortMs ?? this.options.abortDeadlineMs, { kind: "timeout" });
228
233
  if (result.kind === "timeout")
229
234
  throw new RegistryError("abort_timeout");
230
235
  if (result.kind === "failed")
@@ -253,7 +258,7 @@ export class SessionRegistry {
253
258
  turn.completedAt = time;
254
259
  this.notifyTerminal(existing, turn, "close");
255
260
  const abortPromise = this.beginAbort(existing, turn);
256
- await this.withTimeout(abortPromise.then(() => undefined, () => undefined), this.options.closeCleanupDeadlineMs, undefined);
261
+ await this.withTimeout(abortPromise.then(() => undefined, () => undefined), existing.handle.deadlines?.cleanupMs ?? this.options.closeCleanupDeadlineMs, undefined);
257
262
  }
258
263
  existing.turn = undefined;
259
264
  existing.turnPromise = undefined;
@@ -289,7 +294,7 @@ export class SessionRegistry {
289
294
  const normalized = normalize(outcome.assistantText);
290
295
  turn.state = "completed";
291
296
  turn.assistantText = normalized.text;
292
- turn.truncated = normalized.truncated;
297
+ turn.truncated = normalized.truncated || outcome.truncated === true;
293
298
  }
294
299
  else if (outcome.status === "aborted")
295
300
  turn.state = "aborted";
@@ -394,7 +399,7 @@ export class SessionRegistry {
394
399
  async disposeHandle(handle) {
395
400
  try {
396
401
  const cleanup = Promise.resolve(handle.dispose()).then(() => undefined, () => undefined);
397
- await this.withTimeout(cleanup, this.options.closeCleanupDeadlineMs, undefined);
402
+ await this.withTimeout(cleanup, handle.deadlines?.cleanupMs ?? this.options.closeCleanupDeadlineMs, undefined);
398
403
  }
399
404
  catch { /* Cleanup errors must never expose adapter internals. */ }
400
405
  }
package/dist/setup-cli.js CHANGED
@@ -81,7 +81,7 @@ export function parseSetupArgs(argv) {
81
81
  customizeSkill = value;
82
82
  continue;
83
83
  }
84
- if (["--repo", "--mode", "--accept-host-writes", "--writers", "--lifecycle", "--profiles", "--image", "--network", "--cache"].includes(argument)) {
84
+ if (["--repo", "--mode", "--accept-host-writes", "--writers", "--lifecycle", "--profiles", "--image", "--network", "--cache", "--tags"].includes(argument)) {
85
85
  if (modeFlags.has(argument))
86
86
  throw new Error("repo_argument_repeated");
87
87
  modeFlags.add(argument);
@@ -124,8 +124,13 @@ export function parseSetupArgs(argv) {
124
124
  throw new Error("network_invalid");
125
125
  container.network = value;
126
126
  break;
127
+ case "--tags":
128
+ if (value !== "none" && value !== "reachable")
129
+ throw new Error("tags_invalid");
130
+ container.tags = value;
131
+ break;
127
132
  case "--cache":
128
- if (value !== "none" && value !== "go")
133
+ if (value !== "none" && value !== "go" && value !== "uv")
129
134
  throw new Error("cache_invalid");
130
135
  container.cache = value;
131
136
  break;
@@ -33,8 +33,6 @@ export const WAKE_TIMEOUT_MARGIN_SECONDS = 60;
33
33
  export const WAKE_MATCHER = [
34
34
  "mcp__pi-session-mcp__pi_session_prompt",
35
35
  "mcp__pi-session-mcp__pi_turn_get",
36
- "mcp__pi-coding-.*__pi_session_prompt",
37
- "mcp__pi-coding-.*__pi_turn_get",
38
36
  ].join("|");
39
37
  /** A command referencing this program path marks an entry as pi-session-mcp's. */
40
38
  const WAKE_PROGRAM = /\/dist\/wake-main\.js(?=$|[\s"'])/;
@@ -60,7 +60,13 @@ export async function changeExecutionMode(configPath, request, dependencies = {}
60
60
  return done("failed", "container_profiles_required");
61
61
  if (request.mode === "container" && !await runtimeUsable())
62
62
  return done("failed", "container_runtime_unavailable");
63
- const entry = mode === "host" ? { mode, acceptHostWrites: true } : mode === "container" ? { ...request.container, mode } : { mode };
63
+ const entry = mode === "host" ? { mode, acceptHostWrites: true } : mode === "container" ? (() => {
64
+ const existing = current.workspaces[request.alias];
65
+ const previousBase = existing?.mode === "container" ? existing.base : undefined;
66
+ const requestedBase = request.baseRef?.replace(/^refs\/heads\//, "");
67
+ const base = requestedBase ?? previousBase ?? request.container?.base;
68
+ return { ...request.container, mode, ...(base === undefined ? {} : { base }) };
69
+ })() : { mode };
64
70
  let proposed;
65
71
  try {
66
72
  proposed = executionModesSchema.parse({ schemaVersion: 1, workspaces: { ...raw.workspaces, [request.alias]: entry } });
@@ -24,7 +24,9 @@ export declare const MCP_SERVER_SHUTDOWN_GRACE_MS = 2000;
24
24
  /** Runs only an explicit executable/argv pair, with bounded output and termination. */
25
25
  export declare function createProcessRunner(options?: ProcessRunnerOptions): CommandRunner;
26
26
  /** Generalized bounded stdio transport for a generated writer wrapper. */
27
- export declare function createCommandMcpStdioLauncher(command: readonly [string, ...string[]], env: NodeJS.ProcessEnv): (signal: AbortSignal) => Promise<McpStdioProcess>;
27
+ export declare function createCommandMcpStdioLauncher(command: readonly [string, ...string[]], env: NodeJS.ProcessEnv, options?: {
28
+ inheritFds?: readonly number[];
29
+ }): (signal: AbortSignal) => Promise<McpStdioProcess>;
28
30
  export interface McpLauncherOptions {
29
31
  readonly nodePath: string;
30
32
  readonly entryPath: string;
@@ -56,7 +56,7 @@ export function createProcessRunner(options = {}) {
56
56
  if (!table)
57
57
  return { exitCode: 126, stdout: "", stderr: "" };
58
58
  return new Promise((resolve) => {
59
- const child = spawn(command, [...args], { stdio: ["ignore", "pipe", "pipe"], env: runOptions?.env ? { ...(options.env ?? process.env), ...runOptions.env } : options.env, cwd: runOptions?.cwd, detached: process.platform !== "win32" });
59
+ const child = spawn(command, [...args], { stdio: ["ignore", "pipe", "pipe", ...(runOptions?.inheritFds ?? [])], env: runOptions?.env ? { ...(options.env ?? process.env), ...runOptions.env } : options.env, cwd: runOptions?.cwd, detached: process.platform !== "win32" });
60
60
  let stdout = "";
61
61
  let stderr = "";
62
62
  let overflow = false;
@@ -132,9 +132,9 @@ export function createProcessRunner(options = {}) {
132
132
  } };
133
133
  }
134
134
  /** Generalized bounded stdio transport for a generated writer wrapper. */
135
- export function createCommandMcpStdioLauncher(command, env) {
135
+ export function createCommandMcpStdioLauncher(command, env, options) {
136
136
  return async (signal) => {
137
- const child = spawn(command[0], command.slice(1), { stdio: ["pipe", "pipe", "pipe"], env, detached: process.platform !== "win32" });
137
+ const child = spawn(command[0], command.slice(1), { stdio: ["pipe", "pipe", "pipe", ...(options?.inheritFds ?? [])], env, detached: process.platform !== "win32" });
138
138
  let buffer = "", closed = false, overflow = false;
139
139
  child.stdout.on("data", (chunk) => { if (!overflow) {
140
140
  buffer += chunk.toString("utf8");
@@ -415,7 +415,8 @@ export async function acquireSetupGuardian(lockFd, timeoutMs, signal, validateFe
415
415
  } });
416
416
  const runner = {
417
417
  run(command, args, runSignal, runOptions) {
418
- if (runOptions?.env !== undefined || released || unsafe || pending || runSignal?.aborted || !child.connected)
418
+ // The guardian child cannot pass descriptors through; refuse instead of dropping a caller's lease.
419
+ if (runOptions?.env !== undefined || runOptions?.inheritFds !== undefined || released || unsafe || pending || runSignal?.aborted || !child.connected)
419
420
  return Promise.resolve({ exitCode: 126, stdout: "", stderr: "" });
420
421
  const id = nextId++;
421
422
  return new Promise((resolve) => {
@@ -6,8 +6,11 @@ import { type OwnershipStore } from "./setup-ownership.js";
6
6
  import { type SetupResult } from "./setup-result.js";
7
7
  import { type WriterRegistrationAdapter } from "./client-adapters/writer-registration.js";
8
8
  import { verifyMcpCapabilities } from "./client-adapters/mcp-verifier.js";
9
+ import { acquireWriterLease } from "./writer-lease.js";
10
+ import { ensureWriterAbsent } from "./writer-fence.js";
9
11
  import * as lifecycle from "./writer-lifecycle.js";
10
12
  import * as acceptance from "./writer-acceptance.js";
13
+ export declare const SETUP_LEASE_WAIT_MS = 10000;
11
14
  export type WriterOperation = "collect" | "reset" | "recreate" | "finish" | "remove" | "verify";
12
15
  export interface WriterOptions {
13
16
  apply: boolean;
@@ -29,6 +32,8 @@ export interface WriterSetupDependencies {
29
32
  verify?: typeof verifyMcpCapabilities;
30
33
  launch?: typeof createCommandMcpStdioLauncher;
31
34
  lock?: typeof acquireOverrideLock;
35
+ lease?: typeof acquireWriterLease;
36
+ ensureAbsent?: typeof ensureWriterAbsent;
32
37
  containerRuntime?: () => Promise<boolean>;
33
38
  }
34
39
  /** Serializes planning, destructive-work refusal, mode commit and writer reconciliation. */