@cjhyy/code-shell-server 0.9.51 → 0.10.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 (93) hide show
  1. package/dist/account/cli.d.ts +23 -0
  2. package/dist/account/cli.js +249 -0
  3. package/dist/account/cloud-contract.d.ts +39 -0
  4. package/dist/account/cloud-contract.js +134 -0
  5. package/dist/account/cloud-types.d.ts +47 -0
  6. package/dist/account/cloud-types.js +1 -0
  7. package/dist/account/feature-service.d.ts +47 -0
  8. package/dist/account/feature-service.js +184 -0
  9. package/dist/account/http.d.ts +17 -0
  10. package/dist/account/http.js +158 -0
  11. package/dist/account/links.d.ts +13 -0
  12. package/dist/account/links.js +137 -0
  13. package/dist/account/manager.d.ts +96 -0
  14. package/dist/account/manager.js +976 -0
  15. package/dist/account/native-lock.d.ts +6 -0
  16. package/dist/account/native-lock.js +26 -0
  17. package/dist/account/native-store.d.ts +32 -0
  18. package/dist/account/native-store.js +162 -0
  19. package/dist/account/profile.d.ts +4 -0
  20. package/dist/account/profile.js +46 -0
  21. package/dist/account/store.d.ts +34 -0
  22. package/dist/account/store.js +118 -0
  23. package/dist/account/types.d.ts +138 -0
  24. package/dist/account/types.js +2 -0
  25. package/dist/desktop-web/http-api.d.ts +4 -0
  26. package/dist/desktop-web/http-api.js +12 -4
  27. package/dist/environment-identity.d.ts +1 -1
  28. package/dist/environment-identity.js +2 -1
  29. package/dist/headless-worker-env.d.ts +2 -0
  30. package/dist/headless-worker-env.js +15 -0
  31. package/dist/hub/approval-lease.js +6 -1
  32. package/dist/hub/configuration.d.ts +11 -0
  33. package/dist/hub/configuration.js +34 -1
  34. package/dist/hub/model-parameter-metadata.d.ts +26 -0
  35. package/dist/hub/model-parameter-metadata.js +92 -0
  36. package/dist/hub/run-replay.js +1 -1
  37. package/dist/hub/session-management.js +1 -1
  38. package/dist/index.account.d.ts +12 -0
  39. package/dist/index.account.js +12 -0
  40. package/dist/index.mobile-remote.d.ts +2 -0
  41. package/dist/index.mobile-remote.js +1 -0
  42. package/dist/index.worker.d.ts +1 -0
  43. package/dist/index.worker.js +1 -0
  44. package/dist/mobile-remote/account-relay-access.d.ts +59 -0
  45. package/dist/mobile-remote/account-relay-access.js +174 -0
  46. package/dist/mobile-remote/approval-clock.d.ts +3 -0
  47. package/dist/mobile-remote/approval-clock.js +15 -0
  48. package/dist/mobile-remote/codex-room-agent.d.ts +6 -0
  49. package/dist/mobile-remote/codex-room-agent.js +17 -1
  50. package/dist/mobile-remote/mobile-chat-turn.d.ts +4 -0
  51. package/dist/mobile-remote/mobile-chat-turn.js +3 -0
  52. package/dist/mobile-remote/mobile-client-event-validator.js +47 -2
  53. package/dist/mobile-remote/mobile-upload-service.d.ts +9 -0
  54. package/dist/mobile-remote/mobile-upload-service.js +25 -0
  55. package/dist/mobile-remote/remote-host-manager.d.ts +21 -4
  56. package/dist/mobile-remote/remote-host-manager.js +142 -25
  57. package/dist/mobile-remote/resident-agent.d.ts +14 -0
  58. package/dist/mobile-remote/resident-agent.js +38 -11
  59. package/dist/mobile-remote/room-history.d.ts +7 -0
  60. package/dist/mobile-remote/room-history.js +56 -1
  61. package/dist/mobile-remote/room-manager.d.ts +216 -9
  62. package/dist/mobile-remote/room-manager.js +700 -65
  63. package/dist/mobile-remote/room-process-stop.d.ts +15 -0
  64. package/dist/mobile-remote/room-process-stop.js +41 -0
  65. package/dist/mobile-remote/trusted-device-store.d.ts +12 -0
  66. package/dist/mobile-remote/trusted-device-store.js +54 -1
  67. package/dist/mobile-remote/turn-queue.d.ts +183 -0
  68. package/dist/mobile-remote/turn-queue.js +422 -0
  69. package/dist/mobile-remote/types.d.ts +1 -1
  70. package/dist/panels/agent-task-host.js +6 -12
  71. package/dist/panels/agent-task-service.d.ts +2 -0
  72. package/dist/panels/agent-task-service.js +7 -0
  73. package/dist/panels/browser-capture.d.ts +1 -15
  74. package/dist/panels/browser-capture.js +1 -35
  75. package/dist/panels/directory-picker.d.ts +48 -0
  76. package/dist/panels/directory-picker.js +247 -0
  77. package/dist/panels/http.d.ts +15 -15
  78. package/dist/panels/management-http.d.ts +9 -9
  79. package/dist/panels/process-service.d.ts +5 -4
  80. package/dist/panels/process-service.js +54 -16
  81. package/dist/panels/runtime.js +148 -30
  82. package/dist/panels/tool-jobs-storage.d.ts +4 -0
  83. package/dist/panels/tool-jobs-storage.js +111 -34
  84. package/dist/panels/types.d.ts +1 -97
  85. package/dist/remote-relay/connector.d.ts +8 -0
  86. package/dist/remote-relay/connector.js +29 -2
  87. package/dist/remote-relay/protocol.d.ts +21 -0
  88. package/dist/remote-relay/protocol.js +31 -0
  89. package/dist/serve/cli.js +6 -0
  90. package/dist/serve/headless-server.js +24 -19
  91. package/dist/sessions-service.d.ts +4 -0
  92. package/dist/sessions-service.js +14 -1
  93. package/package.json +10 -6
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
- import { releaseStdioAfterExit, stopRoomProcess } from "./room-process-stop.js";
2
+ import { releaseStdioAfterExit, stopRoomProcess, stopRoomProcessAndWait, waitForRoomProcessSpawn, } from "./room-process-stop.js";
3
3
  import { createInterface } from "node:readline";
4
4
  import { pathWithCommonBins } from "./path-bins.js";
5
5
  /**
@@ -119,12 +119,15 @@ export function parseStreamJsonLine(line) {
119
119
  export class ResidentAgentProcess {
120
120
  opts;
121
121
  child;
122
+ /** The most recently spawned process, kept after stop() so a caller can
123
+ * wait for it to actually exit before starting a replacement. */
124
+ lastChild;
125
+ spawned;
122
126
  constructor(opts) {
123
127
  this.opts = opts;
124
128
  }
125
- start() {
126
- if (this.child)
127
- return;
129
+ /** Exact argv for one resident process. Pure → unit-testable. */
130
+ static argsFor(opts) {
128
131
  const args = [
129
132
  "--print",
130
133
  "--verbose",
@@ -135,13 +138,17 @@ export class ResidentAgentProcess {
135
138
  "--permission-prompt-tool",
136
139
  "stdio",
137
140
  ];
138
- if (this.opts.appendSystemPrompt) {
139
- args.push("--append-system-prompt", this.opts.appendSystemPrompt);
140
- }
141
- if (this.opts.resumeSessionId) {
142
- args.push("--resume", this.opts.resumeSessionId);
143
- }
144
- args.push("--permission-mode", this.opts.permissionMode);
141
+ if (opts.appendSystemPrompt)
142
+ args.push("--append-system-prompt", opts.appendSystemPrompt);
143
+ if (opts.resumeSessionId)
144
+ args.push("--resume", opts.resumeSessionId);
145
+ args.push("--permission-mode", opts.permissionMode);
146
+ return args;
147
+ }
148
+ start() {
149
+ if (this.child)
150
+ return;
151
+ const args = ResidentAgentProcess.argsFor(this.opts);
145
152
  const child = spawn(this.opts.command, args, {
146
153
  cwd: this.opts.cwd,
147
154
  env: { ...process.env, PATH: pathWithCommonBins() },
@@ -149,6 +156,10 @@ export class ResidentAgentProcess {
149
156
  stdio: ["pipe", "pipe", "pipe"],
150
157
  });
151
158
  this.child = child;
159
+ this.lastChild = child;
160
+ this.spawned = waitForRoomProcessSpawn(child);
161
+ // Observed by whenStarted(); an unobserved failed spawn is reported via onEvent.
162
+ this.spawned.catch(() => undefined);
152
163
  // Settle on `close` so trailing output is read, without waiting on grandchildren.
153
164
  releaseStdioAfterExit(child);
154
165
  if (child.stdout) {
@@ -231,4 +242,20 @@ export class ResidentAgentProcess {
231
242
  if (child)
232
243
  stopRoomProcess(child);
233
244
  }
245
+ /** Resolves once the spawned process exists; rejects when the spawn failed. */
246
+ whenStarted() {
247
+ return this.spawned ?? Promise.resolve();
248
+ }
249
+ /**
250
+ * Stop, then resolve true only after the last spawned process has exited.
251
+ * A process already retired by a pipe error is still waited for: it may be
252
+ * alive for up to the SIGKILL grace period.
253
+ */
254
+ stopAndWait(timeoutMs) {
255
+ const child = this.child ?? this.lastChild;
256
+ this.child = undefined;
257
+ if (!child)
258
+ return Promise.resolve(true);
259
+ return stopRoomProcessAndWait(child, timeoutMs);
260
+ }
234
261
  }
@@ -14,6 +14,13 @@ export interface RoomHistoryRequest {
14
14
  export declare function scanRoomHistory<T extends {
15
15
  seq: number;
16
16
  }>(file: string, parse: (value: unknown) => T | undefined, visit?: (message: T) => void): number;
17
+ /** Same strict snapshot proof as scanRoomHistory, yielding between bounded reads. */
18
+ export declare function scanRoomHistoryAsync<T extends {
19
+ seq: number;
20
+ }>(file: string, parse: (value: unknown) => T | undefined, visit?: (message: T) => void): Promise<{
21
+ sequence: number;
22
+ stamp: string;
23
+ }>;
17
24
  /** Durable immutable copies keep a frozen page boundary valid across Host restarts. */
18
25
  export declare function readRoomHistoryPage<T extends {
19
26
  seq: number;
@@ -1,12 +1,15 @@
1
1
  import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
2
2
  import { closeSync, constants, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, writeFileSync, writeSync, } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import { lstat, open } from "node:fs/promises";
4
5
  const MAX_BYTES = 256 * 1024 * 1024;
5
6
  const MAX_LINE = 4 * 1024 * 1024;
6
7
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
7
8
  const decoder = new TextDecoder("utf-8", { fatal: true });
8
9
  function regular(path) {
9
- const stat = lstatSync(path);
10
+ return assertRegular(lstatSync(path));
11
+ }
12
+ function assertRegular(stat) {
10
13
  if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) {
11
14
  throw new Error("room history must be a private regular file");
12
15
  }
@@ -99,6 +102,58 @@ export function scanRoomHistory(file, parse, visit) {
99
102
  closeSync(fd);
100
103
  }
101
104
  }
105
+ /** Same strict snapshot proof as scanRoomHistory, yielding between bounded reads. */
106
+ export async function scanRoomHistoryAsync(file, parse, visit) {
107
+ const initial = assertRegular(await lstat(file));
108
+ const handle = await open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
109
+ try {
110
+ const before = assertRegular(await handle.stat());
111
+ if (stamp(initial) !== stamp(before))
112
+ throw new Error("room history changed while opening");
113
+ if (before.size > MAX_BYTES)
114
+ throw new Error("room history exceeds 256 MiB; archive it before continuing");
115
+ let offset = 0, seq = 0, partialBytes = 0;
116
+ let parts = [];
117
+ while (offset < before.size) {
118
+ const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, before.size - offset));
119
+ const { bytesRead } = await handle.read(chunk, 0, chunk.length, offset);
120
+ if (!bytesRead)
121
+ throw new Error("room history was truncated while reading");
122
+ offset += bytesRead;
123
+ let from = 0;
124
+ while (from < bytesRead) {
125
+ const next = chunk.indexOf(0x0a, from);
126
+ const newline = next >= 0 && next < bytesRead ? next : -1;
127
+ const end = newline < 0 ? bytesRead : newline;
128
+ const piece = chunk.subarray(from, end);
129
+ partialBytes += piece.length;
130
+ if (partialBytes > MAX_LINE)
131
+ throw new Error("room history record exceeds size limit");
132
+ parts.push(piece);
133
+ if (newline < 0)
134
+ break;
135
+ const bytes = parts.length === 1 ? parts[0] : Buffer.concat(parts, partialBytes);
136
+ const message = parse(JSON.parse(decoder.decode(bytes)));
137
+ if (!message || message.seq !== seq + 1)
138
+ throw new Error(`room history is corrupt at sequence ${seq + 1}`);
139
+ seq = message.seq;
140
+ visit?.(message);
141
+ parts = [];
142
+ partialBytes = 0;
143
+ from = newline + 1;
144
+ }
145
+ }
146
+ if (partialBytes)
147
+ throw new Error("room history has an incomplete final record");
148
+ if (stamp(before) !== stamp(await handle.stat()) ||
149
+ stamp(before) !== stamp(assertRegular(await lstat(file))))
150
+ throw new Error("room history changed while scanning");
151
+ return { sequence: seq, stamp: stamp(before) };
152
+ }
153
+ finally {
154
+ await handle.close();
155
+ }
156
+ }
102
157
  function validateManifest(value, roomId, snapshotId) {
103
158
  if (!value ||
104
159
  value.version !== 1 ||
@@ -57,6 +57,54 @@ export interface RoomMeta {
57
57
  linkedSessionMode?: "observe-only";
58
58
  }
59
59
  export type RoomOpenStatus = "running" | "missing" | "observing";
60
+ /** Why a room cannot change its spawn-time permission mode right now. */
61
+ export type RoomBusyReason = "switching" | "retiring" | "turn" | "approval" | "queued";
62
+ export declare class RoomBusyError extends Error {
63
+ readonly reason: RoomBusyReason;
64
+ constructor(reason: RoomBusyReason);
65
+ }
66
+ /** Stable code of RoomInputAcceptedError; callers branch on it, never on text. */
67
+ export declare const ROOM_INPUT_ACCEPTED = "ROOM_INPUT_ACCEPTED";
68
+ /**
69
+ * The CLI accepted the input, but recording it (history, trace, echo) failed.
70
+ * The turn may be running: it must never be retried as if it were rejected.
71
+ */
72
+ export declare class RoomInputAcceptedError extends Error {
73
+ readonly code = "ROOM_INPUT_ACCEPTED";
74
+ constructor(cause: unknown);
75
+ }
76
+ /** The room or its process changed between planning and applying a mode change. */
77
+ export declare class RoomStateChangedError extends Error {
78
+ constructor(message?: string);
79
+ }
80
+ /**
81
+ * Read-only plan for opening an external CLI session under a mode. A Host may
82
+ * ask for authorization between plan and apply; apply re-validates every field.
83
+ */
84
+ export interface RoomSessionOpenPlan {
85
+ /** Existing room id, or an id reserved for the room apply() will create. */
86
+ roomId: string;
87
+ created: boolean;
88
+ sessionId: string;
89
+ /** Canonical directory the CLI process will run in. */
90
+ cwd: string;
91
+ kind: RoomKind;
92
+ /** Live process mode, else persisted mode; undefined for a new room. */
93
+ fromMode?: RoomPermissionMode;
94
+ toMode: RoomPermissionMode;
95
+ /** False when metadata and any live process already run in toMode. */
96
+ change: boolean;
97
+ /** True when a process is live now (an unchanged open will reuse it). */
98
+ live: boolean;
99
+ observing: boolean;
100
+ revision: number;
101
+ }
102
+ export interface RoomSessionOpenResult {
103
+ roomId: string;
104
+ status: RoomOpenStatus;
105
+ mode: RoomPermissionMode;
106
+ changed: boolean;
107
+ }
60
108
  export interface RoomMessage {
61
109
  seq: number;
62
110
  ts: number;
@@ -84,6 +132,15 @@ export interface RoomMessage {
84
132
  size: number;
85
133
  path: string;
86
134
  }>;
135
+ /** Exact mobile intent receipt, durable alongside its accepted user turn. */
136
+ clientMessageId?: string;
137
+ clientDeviceId?: string;
138
+ clientIntentHash?: string;
139
+ }
140
+ export interface RoomInputReceipt {
141
+ clientMessageId: string;
142
+ clientDeviceId: string;
143
+ clientIntentHash: string;
87
144
  }
88
145
  export declare function roomTurnText(text: string, attachments: InputAttachmentMeta[]): string;
89
146
  export declare function stripRoomAttachmentBlock(text: string): string;
@@ -96,6 +153,15 @@ export interface RoomAgent {
96
153
  send(text: string): boolean;
97
154
  isRunning(): boolean;
98
155
  stop(): void;
156
+ /** Stop, then resolve true only after the process has really exited. A
157
+ * replacement for the same external session must not start before that. */
158
+ stopAndWait?(timeoutMs: number): Promise<boolean>;
159
+ /** True while a turn process is alive. Codex spawns one child per turn and
160
+ * that child can outlive the parsed turn_end. Agents without this hook are
161
+ * tracked by accepted-input / turn_end accounting. */
162
+ isBusy?(): boolean;
163
+ /** Resolves once the spawned process exists; rejects for a failed spawn. */
164
+ whenStarted?(): Promise<void>;
99
165
  respondControl?(requestId: string, decision: {
100
166
  behavior: "allow";
101
167
  updatedInput?: unknown;
@@ -123,6 +189,19 @@ export interface RoomManagerOptions {
123
189
  * external session. Checked before any resident agent can start on it, so
124
190
  * desktop and phone entry points share one ownership gate. */
125
191
  assertExternalSessionAvailable?: (externalSessionId: string, kind: RoomKind) => void;
192
+ /** Host start policy, checked before ANY resident agent is created (open,
193
+ * send's implicit open, session open, takeover). Throwing refuses the start.
194
+ * Persisted metadata alone is never an authorization: the Host decides. */
195
+ authorizeStart?: (room: Readonly<RoomMeta>) => void;
196
+ /** Bound on waiting for a retired process during a mode handoff. */
197
+ processExitTimeoutMs?: number;
198
+ /** Called after an explicit close() (not after a process exit). */
199
+ onRoomClosed?: (roomId: string) => void;
200
+ /** Called when a room's process state changed without a caller to report
201
+ * it: a failed mode handoff after the old process exited, a failed first
202
+ * start, or a retired process finally proven gone. Hosts republish the
203
+ * room's authoritative state to every view. */
204
+ onRoomStateChanged?: (roomId: string) => void;
126
205
  /** Called whenever a room gains a new persisted message (push to phone). */
127
206
  onMessage: (roomId: string, msg: RoomMessage) => void;
128
207
  /** Called when a room's resident agent requests tool-use approval. For
@@ -156,6 +235,21 @@ export declare class RoomManager {
156
235
  /** Permission mode each live agent was spawned with. The mode is a spawn-time
157
236
  * CLI argument, so room metadata alone cannot prove what a process runs. */
158
237
  private agentModes;
238
+ /** Accepted inputs minus turn_end per room, for agents without isBusy(). A
239
+ * Claude resident process queues several inputs; each ends with a result. */
240
+ private turnCounts;
241
+ /** In-flight mode handoff per room. close() deletes it, so a handoff that is
242
+ * waiting for the old process can never reopen a room the user closed. */
243
+ private transitions;
244
+ /** Detached processes not yet proven exited. This is a process-safety fence,
245
+ * independent of any operation: close() and cancellation leave it in place,
246
+ * and no agent may start for the room until the process has really exited,
247
+ * so one external session never has two writers. */
248
+ private retiring;
249
+ private retiringProbes;
250
+ /** Bumped on every change to a room's process or mode, so a Host approval
251
+ * granted for one state cannot be applied to another. */
252
+ private revisions;
159
253
  private modelTraces;
160
254
  /** Rooms whose visible output is currently sourced from the external CLI's
161
255
  * transcript tail. While present, the resident agent's stdout mirror is
@@ -198,6 +292,9 @@ export declare class RoomManager {
198
292
  private readonly rootRealPath;
199
293
  private readonly rootIdentity;
200
294
  private readonly failedWrites;
295
+ private readonly preparingInputReceipts;
296
+ private readonly inputReceipts;
297
+ private hostTurnHooks;
201
298
  /** Drop all pending AskUser entries for a room. Called when its agent goes
202
299
  * away (exit/close) so a request that can no longer be answered doesn't leak
203
300
  * in the map (small memory leak) and its approval card doesn't hang until the
@@ -219,6 +316,8 @@ export declare class RoomManager {
219
316
  claudeSessionId?: string;
220
317
  linkedSessionMode?: "observe-only";
221
318
  }): RoomMeta;
319
+ private mintRoomId;
320
+ private createRoomWithId;
222
321
  listRooms(): RoomMeta[];
223
322
  getRoom(id: string): RoomMeta | undefined;
224
323
  private nextSeq;
@@ -250,14 +349,114 @@ export declare class RoomManager {
250
349
  /** Persist normalized transcript-tail messages through the same seq/history/
251
350
  * broadcast path as resident-agent output. */
252
351
  ingestTranscriptMessages(id: string, messages: Omit<RoomMessage, "seq" | "ts">[]): void;
352
+ /** Monotonic per-room state revision; see `revisions`. */
353
+ roomRevision(id: string): number;
354
+ private bumpRevision;
355
+ /** The room bound to an external session. Reuse must match BOTH id and kind:
356
+ * a codex thread id and a claude session id share the `claudeSessionId`
357
+ * field, so a bare id match could hand back the other CLI's room. */
358
+ findSessionRoom(sessionId: string, kind: RoomKind): RoomMeta | undefined;
359
+ /** Spawn-time mode of the room's live process, if one is running. */
360
+ liveMode(id: string): RoomPermissionMode | undefined;
361
+ /**
362
+ * Why the room cannot change its process now. A mode change stops the
363
+ * process, so it must not interrupt queued/active turns, a pending tool
364
+ * approval or AskUserQuestion, or a decision that already left the approval
365
+ * bridge but has not been consumed here yet (its entry stays in the pending
366
+ * maps until respondApproval consumes it).
367
+ */
368
+ roomBusyReason(id: string): RoomBusyReason | undefined;
369
+ /**
370
+ * The room's own execution state, independent of any Host-held pending
371
+ * turns: an active or queued CLI input (Codex: a live turn child), a mode
372
+ * handoff, or an unanswered approval. A Host scheduler waits on this.
373
+ */
374
+ private executionBusyReason;
375
+ /**
376
+ * True while a new input must not be handed to the room: it is executing
377
+ * or awaiting an approval, switching modes, or a retired process may still
378
+ * write. Host pending turns are deliberately not counted here.
379
+ */
380
+ isExecutionBusy(id: string): boolean;
381
+ /** Mode a new input would run under: the live process mode, else the stored one. */
382
+ effectiveMode(id: string): RoomPermissionMode | undefined;
253
383
  /**
254
- * Open (or create) the room bound to a claude session id, deduping by that id
255
- * so a given claude session maps to exactly one room. Returns the room id.
384
+ * Host pending-turn integration, installed once by the Host composition.
385
+ * `hasPendingTurns` blocks every mode change; `beforeClose` runs before an
386
+ * explicit close signals the process, so pending turns pause first.
256
387
  */
257
- openForSession(claudeSessionId: string, cwd: string, mode: RoomPermissionMode, kind?: RoomKind): {
388
+ setHostTurnHooks(hooks: {
389
+ hasPendingTurns?: (roomId: string) => boolean;
390
+ beforeClose?: (roomId: string) => void;
391
+ }): void;
392
+ /**
393
+ * Plan opening an external session's room under `mode` without changing
394
+ * anything. Throws for a cwd that differs from the room's bound directory,
395
+ * an occupied external session, or a busy room whose mode would change.
396
+ */
397
+ planSessionOpen(input: {
398
+ sessionId: string;
399
+ cwd: string;
400
+ kind: RoomKind;
401
+ mode: RoomPermissionMode;
402
+ }): RoomSessionOpenPlan;
403
+ /**
404
+ * Apply a plan from planSessionOpen. `authorize` runs before anything is
405
+ * changed and again right before the replacement process is created; a
406
+ * throw leaves metadata untouched (or restores it) and never starts a CLI.
407
+ * `revalidate` re-checks asynchronous Host facts after every wait for a
408
+ * retired process, before that final `authorize`, while the room's handoff
409
+ * is still owned by this call.
410
+ */
411
+ applySessionOpen(plan: RoomSessionOpenPlan, options?: {
412
+ authorize?: () => void;
413
+ revalidate?: () => Promise<void>;
414
+ }): Promise<RoomSessionOpenResult>;
415
+ /**
416
+ * Open (or create) the room bound to an external session id, deduping by
417
+ * that id so a given session maps to exactly one room. A different mode is
418
+ * applied by a guarded process handoff. Hosts that must authorize a mode
419
+ * use planSessionOpen/applySessionOpen directly.
420
+ */
421
+ openForSession(claudeSessionId: string, cwd: string, mode: RoomPermissionMode, kind?: RoomKind): Promise<{
258
422
  roomId: string;
259
423
  status: RoomOpenStatus;
260
- };
424
+ mode: RoomPermissionMode;
425
+ }>;
426
+ /**
427
+ * Re-probe a retired process of the session's room (see `retiring`), so a
428
+ * plan made right afterwards is refused only while that process is really
429
+ * still alive. Never starts anything.
430
+ */
431
+ settleSessionRoom(sessionId: string, kind: RoomKind): Promise<void>;
432
+ /** Resolves true once no retired process of the room can still be alive. */
433
+ private settleRetiring;
434
+ private probeRetired;
435
+ /** Synchronous entry points cannot wait: refuse, and re-probe in the
436
+ * background so the next attempt succeeds once the process is gone. */
437
+ private assertNotRetiring;
438
+ /** Request an exit probe; the shared probe publishes a proven exit once.
439
+ * A handoff that still owns the room reports its own outcome. */
440
+ private watchRetired;
441
+ private notifyRoomState;
442
+ /**
443
+ * Replace a room's process with one in `toMode`. permissionMode is a
444
+ * spawn-time CLI argument (`--permission-mode` / codex sandbox flags), so a
445
+ * live process cannot change it. Order matters:
446
+ * 1. refuse a busy room, before touching anything;
447
+ * 2. detach the old process, so its late output/exit belongs to nobody, and
448
+ * wait (bounded) until it has really exited — never two writers;
449
+ * 3. re-authorize, then persist the mode and start the replacement;
450
+ * 4. on any failure restore the previous mode. The old process is already
451
+ * gone at that point, so the room reports not running instead of
452
+ * pretending the old state came back, and every view is told so.
453
+ * The detached process stays in `retiring` until its exit is proven: a
454
+ * timeout, close() or cancellation ends this operation but not the fence.
455
+ * Transcript followers are not ended: the external session continues.
456
+ */
457
+ private transitionRoom;
458
+ /** Start (or reuse) the room's agent and wait until a spawned process exists. */
459
+ private startAgent;
261
460
  /** Bind an external CLI transcript to a room for observation only.
262
461
  *
263
462
  * This method never creates or starts a resident agent. The host must first
@@ -275,12 +474,12 @@ export declare class RoomManager {
275
474
  * This is the linked-session operation that may create/start an agent. The
276
475
  * external identity is revalidated at takeover time so stale links cannot
277
476
  * silently start a different session. */
278
- takeOverLinkedSession(roomId: string, externalSessionId: string, cwd: string, kind: RoomKind): {
477
+ takeOverLinkedSession(roomId: string, externalSessionId: string, cwd: string, kind: RoomKind): Promise<{
279
478
  roomId: string;
280
479
  status: "running";
281
480
  mode: RoomPermissionMode;
282
481
  cwd: string;
283
- };
482
+ }>;
284
483
  private requireLinkedSessionTarget;
285
484
  /**
286
485
  * Forward a phone-side approval decision to the room's resident agent.
@@ -301,13 +500,21 @@ export declare class RoomManager {
301
500
  open(id: string): {
302
501
  status: RoomOpenStatus;
303
502
  };
304
- /** Stop a live agent whose spawn-time permission mode differs from `mode`. */
305
- private retireAgentUnlessMode;
503
+ private openAgent;
306
504
  private onAgentEvent;
505
+ /** Prepare cold receipts outside the synchronous send path; concurrent intents share one scan. */
506
+ prepareInputReceipts(id: string): Promise<void>;
507
+ private buildInputReceipts;
508
+ private currentInputReceipts;
509
+ /** A replay never opens a new CLI, reclaims an attachment, or reuses a changed intent. */
510
+ hasAcceptedInput(id: string, receipt: RoomInputReceipt): boolean;
307
511
  /** Post a user message: persist it, ensure agent running, feed it. */
308
- send(id: string, text: string, attachments?: InputAttachmentMeta[]): boolean;
512
+ send(id: string, text: string, attachments?: InputAttachmentMeta[], receipt?: RoomInputReceipt): boolean;
513
+ private recordAcceptedInput;
309
514
  close(id: string): void;
310
515
  closeAll(): void;
516
+ /** True while the room's process runs, or a retired one is not yet proven
517
+ * gone (a view must not be told the room stopped while it may still write). */
311
518
  isOpen(id: string): boolean;
312
519
  /**
313
520
  * Delete rooms whose last activity is older than maxAgeMs (idle-based GC,