@cjhyy/code-shell-server 0.9.44 → 0.9.47
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/dist/attachment-service.d.ts +2 -0
- package/dist/attachment-service.js +40 -27
- package/dist/bin/code-shell-link-cleanup.d.ts +2 -0
- package/dist/bin/code-shell-link-cleanup.js +3 -0
- package/dist/desktop-web/http-api.d.ts +3 -0
- package/dist/desktop-web/http-api.js +16 -1
- package/dist/hub/configuration.d.ts +6 -3
- package/dist/hub/configuration.js +19 -6
- package/dist/hub/mcp-probe.d.ts +4 -24
- package/dist/hub/mcp-probe.js +18 -389
- package/dist/hub/model-probe.d.ts +2 -20
- package/dist/hub/model-probe.js +2 -193
- package/dist/hub/run-replay.d.ts +7 -1
- package/dist/hub/run-replay.js +10 -2
- package/dist/hub/session-management.js +18 -53
- package/dist/hub/skills-management.js +6 -8
- package/dist/hub/uploads.d.ts +10 -0
- package/dist/hub/uploads.js +107 -15
- package/dist/index.desktop-web.d.ts +1 -0
- package/dist/index.desktop-web.js +1 -0
- package/dist/index.links.d.ts +3 -0
- package/dist/index.links.js +2 -0
- package/dist/index.panels.d.ts +1 -0
- package/dist/index.panels.js +1 -0
- package/dist/index.serve.d.ts +1 -0
- package/dist/index.serve.js +1 -0
- package/dist/index.storage.d.ts +1 -0
- package/dist/index.storage.js +1 -0
- package/dist/links/catalog.js +2 -0
- package/dist/links/diagnostics.d.ts +60 -0
- package/dist/links/diagnostics.js +184 -0
- package/dist/links/http.d.ts +10 -8
- package/dist/links/http.js +144 -125
- package/dist/links/legacy-cleanup-cli.d.ts +3 -0
- package/dist/links/legacy-cleanup-cli.js +64 -0
- package/dist/links/providers/defaults.d.ts +5 -0
- package/dist/links/providers/defaults.js +782 -0
- package/dist/links/providers/index.d.ts +7 -0
- package/dist/links/providers/index.js +22 -0
- package/dist/links/remote-catalog.d.ts +10 -3
- package/dist/links/remote-catalog.js +76 -29
- package/dist/links/runtime/operation.d.ts +9 -0
- package/dist/links/runtime/operation.js +16 -0
- package/dist/links/service.d.ts +18 -2
- package/dist/links/service.js +593 -62
- package/dist/links/types.d.ts +21 -1
- package/dist/mobile-remote/codex-parse.d.ts +3 -1
- package/dist/mobile-remote/codex-parse.js +45 -11
- package/dist/mobile-remote/codex-room-agent.d.ts +0 -6
- package/dist/mobile-remote/codex-room-agent.js +30 -32
- package/dist/mobile-remote/mobile-chat-turn.d.ts +1 -0
- package/dist/mobile-remote/mobile-chat-turn.js +3 -0
- package/dist/mobile-remote/mobile-client-event-validator.js +21 -0
- package/dist/mobile-remote/mobile-history.d.ts +10 -1
- package/dist/mobile-remote/mobile-history.js +24 -16
- package/dist/mobile-remote/remote-host-manager.d.ts +10 -0
- package/dist/mobile-remote/remote-host-manager.js +50 -13
- package/dist/mobile-remote/resident-agent.js +38 -22
- package/dist/mobile-remote/room-manager.d.ts +23 -13
- package/dist/mobile-remote/room-manager.js +106 -22
- package/dist/mobile-remote/room-model-tracing.d.ts +18 -0
- package/dist/mobile-remote/room-model-tracing.js +196 -0
- package/dist/mobile-remote/room-process-stop.d.ts +3 -0
- package/dist/mobile-remote/room-process-stop.js +28 -0
- package/dist/model-trace-history.d.ts +29 -0
- package/dist/model-trace-history.js +198 -0
- package/dist/model-trace-http.d.ts +14 -0
- package/dist/model-trace-http.js +105 -0
- package/dist/panels/agent-task-host.js +1 -0
- package/dist/panels/agent-task-service.d.ts +2 -1
- package/dist/panels/agent-task-service.js +10 -3
- package/dist/panels/directory-bookmarks.js +62 -29
- package/dist/panels/execution-gate.d.ts +19 -1
- package/dist/panels/execution-gate.js +33 -5
- package/dist/panels/http.d.ts +1 -0
- package/dist/panels/hub-automations.js +1 -0
- package/dist/panels/hub-binding.js +5 -5
- package/dist/panels/model-trace-channel.d.ts +21 -0
- package/dist/panels/model-trace-channel.js +110 -0
- package/dist/panels/model-traces.d.ts +39 -0
- package/dist/panels/model-traces.js +222 -0
- package/dist/panels/process-service.d.ts +12 -2
- package/dist/panels/process-service.js +49 -6
- package/dist/panels/resources/uploads.js +15 -6
- package/dist/panels/runtime.d.ts +1 -0
- package/dist/panels/runtime.js +66 -8
- package/dist/panels/task-cookies.js +8 -16
- package/dist/panels/tool-jobs.d.ts +2 -0
- package/dist/panels/tool-jobs.js +28 -3
- package/dist/serve/cli.js +8 -1
- package/dist/serve/headless-server.js +68 -14
- package/dist/sessions-service.js +17 -27
- package/dist/worker-bridge-core.js +11 -0
- package/package.json +8 -7
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { stopRoomProcess } from "./room-process-stop.js";
|
|
2
3
|
import { createInterface } from "node:readline";
|
|
3
4
|
import { pathWithCommonBins } from "./path-bins.js";
|
|
4
5
|
/**
|
|
@@ -49,6 +50,9 @@ export function parseStreamJsonLine(line) {
|
|
|
49
50
|
catch {
|
|
50
51
|
return [];
|
|
51
52
|
}
|
|
53
|
+
// A valid but non-record line (e.g. `null`) carries no event.
|
|
54
|
+
if (!msg || typeof msg !== "object" || Array.isArray(msg))
|
|
55
|
+
return [];
|
|
52
56
|
const out = [];
|
|
53
57
|
if (msg.type === "control_request" && msg.request?.subtype === "can_use_tool") {
|
|
54
58
|
return [
|
|
@@ -148,18 +152,34 @@ export class ResidentAgentProcess {
|
|
|
148
152
|
if (child.stdout) {
|
|
149
153
|
const rl = createInterface({ input: child.stdout });
|
|
150
154
|
rl.on("line", (line) => {
|
|
155
|
+
if (this.child !== child)
|
|
156
|
+
return;
|
|
151
157
|
for (const ev of parseStreamJsonLine(line))
|
|
152
158
|
this.opts.onEvent(ev);
|
|
153
159
|
});
|
|
154
160
|
}
|
|
155
161
|
child.stderr?.on("data", (chunk) => {
|
|
162
|
+
if (this.child !== child)
|
|
163
|
+
return;
|
|
156
164
|
// claude prints non-fatal warnings to stderr; surface as error events
|
|
157
165
|
// only if they look like real errors (cheap heuristic).
|
|
158
166
|
const text = String(chunk);
|
|
159
167
|
if (/error/i.test(text))
|
|
160
168
|
this.opts.onEvent({ type: "error", error: text.trim().slice(0, 400) });
|
|
161
169
|
});
|
|
170
|
+
let exited = false;
|
|
171
|
+
const finish = (code, signal) => {
|
|
172
|
+
if (exited)
|
|
173
|
+
return;
|
|
174
|
+
exited = true;
|
|
175
|
+
if (this.child !== child)
|
|
176
|
+
return;
|
|
177
|
+
this.child = undefined;
|
|
178
|
+
this.opts.onEvent({ type: "exit", code, signal });
|
|
179
|
+
};
|
|
162
180
|
child.on("error", (err) => {
|
|
181
|
+
if (this.child !== child)
|
|
182
|
+
return;
|
|
163
183
|
const isMissing = err.code === "ENOENT";
|
|
164
184
|
this.opts.onEvent({
|
|
165
185
|
type: "error",
|
|
@@ -167,11 +187,24 @@ export class ResidentAgentProcess {
|
|
|
167
187
|
? `未找到命令 "${this.opts.command}"。请先安装 Claude Code CLI 并确保它在 PATH 中。`
|
|
168
188
|
: err.message,
|
|
169
189
|
});
|
|
190
|
+
// A failed spawn (ENOENT/EACCES/missing cwd) emits `error` without
|
|
191
|
+
// `exit`. Retire that child so the room can start a replacement.
|
|
192
|
+
if (child.pid === undefined)
|
|
193
|
+
finish(null, null);
|
|
170
194
|
});
|
|
171
|
-
child.on("
|
|
172
|
-
this.child
|
|
173
|
-
|
|
195
|
+
child.stdin?.on("error", (error) => {
|
|
196
|
+
if (this.child !== child)
|
|
197
|
+
return;
|
|
198
|
+
// Pipe errors are emitted by stdin, separately from ChildProcess.error.
|
|
199
|
+
// Retire this room immediately; its eventual exit must not close a retry.
|
|
200
|
+
stopRoomProcess(child);
|
|
201
|
+
this.opts.onEvent({
|
|
202
|
+
type: "error",
|
|
203
|
+
error: `Claude Code input pipe closed${error.code ? ` (${error.code})` : ""}`,
|
|
204
|
+
});
|
|
205
|
+
finish(null, null);
|
|
174
206
|
});
|
|
207
|
+
child.on("close", (code, signal) => finish(code, signal));
|
|
175
208
|
}
|
|
176
209
|
/** Feed one user turn. Returns false if the process isn't running. */
|
|
177
210
|
send(text) {
|
|
@@ -193,24 +226,7 @@ export class ResidentAgentProcess {
|
|
|
193
226
|
stop() {
|
|
194
227
|
const child = this.child;
|
|
195
228
|
this.child = undefined;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// undefined/0; the `> 1` also rules out the (theoretical) pid-1 case so this
|
|
199
|
-
// negative-pid group kill is consistent with killProcessGroup's guard.
|
|
200
|
-
if (!child?.pid || child.pid <= 1) {
|
|
201
|
-
child?.kill("SIGTERM");
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
try {
|
|
205
|
-
process.kill(-child.pid, "SIGTERM");
|
|
206
|
-
}
|
|
207
|
-
catch {
|
|
208
|
-
try {
|
|
209
|
-
child.kill("SIGTERM");
|
|
210
|
-
}
|
|
211
|
-
catch {
|
|
212
|
-
/* already gone */
|
|
213
|
-
}
|
|
214
|
-
}
|
|
229
|
+
if (child)
|
|
230
|
+
stopRoomProcess(child);
|
|
215
231
|
}
|
|
216
232
|
}
|
|
@@ -14,18 +14,19 @@ export type RoomPermissionMode = "default" | "acceptEdits" | "bypassPermissions"
|
|
|
14
14
|
* `answersByQuestion` maps question text → answer string.
|
|
15
15
|
*/
|
|
16
16
|
export declare function buildAskUserUpdatedInput(input: unknown, answersByQuestion: Record<string, string>): Record<string, unknown>;
|
|
17
|
-
|
|
18
|
-
* Parse an AskUserQuestion input into the first question's prompt/header/options
|
|
19
|
-
* so the UI can render a choice card. The room UI answers one question at a time
|
|
20
|
-
* (the first); returns undefined for a non-AskUser / malformed input (no
|
|
21
|
-
* questions), which the caller treats as "fall back to auto-allow".
|
|
22
|
-
*/
|
|
23
|
-
export declare function askUserPrompt(input: unknown): {
|
|
17
|
+
type AskUserQuestionPrompt = {
|
|
24
18
|
question: string;
|
|
25
19
|
header?: string;
|
|
26
20
|
options: string[];
|
|
27
21
|
multiSelect: boolean;
|
|
28
|
-
}
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Parse an AskUserQuestion input into the first question's prompt/header/options
|
|
25
|
+
* so the UI can render a choice card. Returns undefined for a non-AskUser /
|
|
26
|
+
* malformed input (no questions), which the caller treats as "fall back to
|
|
27
|
+
* auto-allow".
|
|
28
|
+
*/
|
|
29
|
+
export declare function askUserPrompt(input: unknown): AskUserQuestionPrompt | undefined;
|
|
29
30
|
/** Which external CLI backs the room. "claude-code" drives `claude` (long-lived
|
|
30
31
|
* stdin process with per-tool approval); "codex" drives `codex exec` (one
|
|
31
32
|
* process per turn, resumed by thread id, no per-tool approval — the sandbox
|
|
@@ -109,6 +110,11 @@ export interface RoomAgentFactory {
|
|
|
109
110
|
export interface RoomManagerOptions {
|
|
110
111
|
rootDir: string;
|
|
111
112
|
createAgent: RoomAgentFactory;
|
|
113
|
+
/** Explicit Host-owned model history target; never derived from room/client input. */
|
|
114
|
+
modelTracing?: {
|
|
115
|
+
storageDir: string;
|
|
116
|
+
parameters?: (room: Readonly<RoomMeta>) => Record<string, unknown>;
|
|
117
|
+
};
|
|
112
118
|
/** Locate an already-running external CLI transcript without starting a CLI.
|
|
113
119
|
* Returning null means the transcript is absent. The returned identity is
|
|
114
120
|
* checked again here so a stale or mismatched locator fails closed. */
|
|
@@ -125,11 +131,8 @@ export interface RoomManagerOptions {
|
|
|
125
131
|
displayName?: string;
|
|
126
132
|
input: unknown;
|
|
127
133
|
description?: string;
|
|
128
|
-
askUser?: {
|
|
129
|
-
|
|
130
|
-
header?: string;
|
|
131
|
-
options: string[];
|
|
132
|
-
multiSelect: boolean;
|
|
134
|
+
askUser?: AskUserQuestionPrompt & {
|
|
135
|
+
questions?: AskUserQuestionPrompt[];
|
|
133
136
|
};
|
|
134
137
|
}) => void;
|
|
135
138
|
/** Called after a room's resident process exits or the room is explicitly
|
|
@@ -146,6 +149,10 @@ export interface RoomManagerOptions {
|
|
|
146
149
|
export declare class RoomManager {
|
|
147
150
|
private readonly opts;
|
|
148
151
|
private agents;
|
|
152
|
+
/** Permission mode each live agent was spawned with. The mode is a spawn-time
|
|
153
|
+
* CLI argument, so room metadata alone cannot prove what a process runs. */
|
|
154
|
+
private agentModes;
|
|
155
|
+
private modelTraces;
|
|
149
156
|
/** Rooms whose visible output is currently sourced from the external CLI's
|
|
150
157
|
* transcript tail. While present, the resident agent's stdout mirror is
|
|
151
158
|
* suppressed so the same CLI output is not pushed twice. */
|
|
@@ -280,6 +287,7 @@ export declare class RoomManager {
|
|
|
280
287
|
behavior: "allow";
|
|
281
288
|
updatedInput?: unknown;
|
|
282
289
|
answer?: string;
|
|
290
|
+
answers?: Record<string, string>;
|
|
283
291
|
} | {
|
|
284
292
|
behavior: "deny";
|
|
285
293
|
message: string;
|
|
@@ -289,6 +297,8 @@ export declare class RoomManager {
|
|
|
289
297
|
open(id: string): {
|
|
290
298
|
status: RoomOpenStatus;
|
|
291
299
|
};
|
|
300
|
+
/** Stop a live agent whose spawn-time permission mode differs from `mode`. */
|
|
301
|
+
private retireAgentUnlessMode;
|
|
292
302
|
private onAgentEvent;
|
|
293
303
|
/** Post a user message: persist it, ensure agent running, feed it. */
|
|
294
304
|
send(id: string, text: string, attachments?: InputAttachmentMeta[]): boolean;
|
|
@@ -2,6 +2,7 @@ import { chmodSync, closeSync, existsSync, fchmodSync, fstatSync, fsyncSync, lst
|
|
|
2
2
|
import { readRoomHistoryPage, scanRoomHistory, } from "./room-history.js";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { createRoomModelTrace } from "./room-model-tracing.js";
|
|
5
6
|
/**
|
|
6
7
|
* The exact shape createRoom() generates: `room_<base36>_<base36>`. Anything
|
|
7
8
|
* else — path separators, `..`, NUL, empty — is rejected before it can reach a
|
|
@@ -58,6 +59,26 @@ function readBoundedUtf8File(path, maxBytes) {
|
|
|
58
59
|
closeSync(fd);
|
|
59
60
|
}
|
|
60
61
|
}
|
|
62
|
+
/** Room metadata names are capped at this many UTF-16 units by parseRoomMeta. */
|
|
63
|
+
const MAX_ROOM_NAME_LENGTH = 512;
|
|
64
|
+
/**
|
|
65
|
+
* Default room name: the cwd's last path segment. Windows drive and UNC paths
|
|
66
|
+
* also split on backslashes. Bounded to the metadata name limit, without
|
|
67
|
+
* leaving half of a surrogate pair, so the reader accepts what we write.
|
|
68
|
+
*/
|
|
69
|
+
function defaultRoomName(cwd) {
|
|
70
|
+
const windows = /^(?:[A-Za-z]:[\\/]|\\\\)/.test(cwd);
|
|
71
|
+
const segment = cwd
|
|
72
|
+
.split(windows ? /[\\/]/ : "/")
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.pop() ?? "room";
|
|
75
|
+
if (segment.length <= MAX_ROOM_NAME_LENGTH)
|
|
76
|
+
return segment;
|
|
77
|
+
const end = /[\uD800-\uDBFF]/.test(segment[MAX_ROOM_NAME_LENGTH - 1])
|
|
78
|
+
? MAX_ROOM_NAME_LENGTH - 1
|
|
79
|
+
: MAX_ROOM_NAME_LENGTH;
|
|
80
|
+
return segment.slice(0, end);
|
|
81
|
+
}
|
|
61
82
|
function parseRoomMeta(value, expectedId) {
|
|
62
83
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
63
84
|
return undefined;
|
|
@@ -67,7 +88,7 @@ function parseRoomMeta(value, expectedId) {
|
|
|
67
88
|
(expectedId !== undefined && raw.id !== expectedId) ||
|
|
68
89
|
typeof raw.name !== "string" ||
|
|
69
90
|
!raw.name ||
|
|
70
|
-
raw.name.length >
|
|
91
|
+
raw.name.length > MAX_ROOM_NAME_LENGTH ||
|
|
71
92
|
raw.name.includes("\0") ||
|
|
72
93
|
typeof raw.cwd !== "string" ||
|
|
73
94
|
!raw.cwd ||
|
|
@@ -175,29 +196,41 @@ export function buildAskUserUpdatedInput(input, answersByQuestion) {
|
|
|
175
196
|
}
|
|
176
197
|
return { ...obj, answers };
|
|
177
198
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
* (the first); returns undefined for a non-AskUser / malformed input (no
|
|
182
|
-
* questions), which the caller treats as "fall back to auto-allow".
|
|
183
|
-
*/
|
|
184
|
-
export function askUserPrompt(input) {
|
|
185
|
-
const obj = (input ?? {});
|
|
186
|
-
const q0 = (Array.isArray(obj.questions) ? obj.questions[0] : undefined);
|
|
187
|
-
if (!q0 || typeof q0.question !== "string")
|
|
199
|
+
function parseAskUserQuestion(value) {
|
|
200
|
+
const q = value;
|
|
201
|
+
if (!q || typeof q.question !== "string")
|
|
188
202
|
return undefined;
|
|
189
|
-
const options = Array.isArray(
|
|
190
|
-
?
|
|
203
|
+
const options = Array.isArray(q.options)
|
|
204
|
+
? q.options
|
|
191
205
|
.map((o) => o?.label)
|
|
192
206
|
.filter((l) => typeof l === "string")
|
|
193
207
|
: [];
|
|
194
208
|
return {
|
|
195
|
-
question:
|
|
196
|
-
header: typeof
|
|
209
|
+
question: q.question,
|
|
210
|
+
header: typeof q.header === "string" ? q.header : undefined,
|
|
197
211
|
options,
|
|
198
|
-
multiSelect:
|
|
212
|
+
multiSelect: q.multiSelect === true,
|
|
199
213
|
};
|
|
200
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Parse an AskUserQuestion input into the first question's prompt/header/options
|
|
217
|
+
* so the UI can render a choice card. Returns undefined for a non-AskUser /
|
|
218
|
+
* malformed input (no questions), which the caller treats as "fall back to
|
|
219
|
+
* auto-allow".
|
|
220
|
+
*/
|
|
221
|
+
export function askUserPrompt(input) {
|
|
222
|
+
const obj = (input ?? {});
|
|
223
|
+
return parseAskUserQuestion(Array.isArray(obj.questions) ? obj.questions[0] : undefined);
|
|
224
|
+
}
|
|
225
|
+
/** Every well-formed question of one AskUserQuestion request, in order. One
|
|
226
|
+
* control request carries them all and is answered exactly once, so the UI
|
|
227
|
+
* must collect every answer before responding. */
|
|
228
|
+
function askUserQuestions(input) {
|
|
229
|
+
const obj = (input ?? {});
|
|
230
|
+
if (!Array.isArray(obj.questions))
|
|
231
|
+
return [];
|
|
232
|
+
return obj.questions.flatMap((q) => parseAskUserQuestion(q) ?? []);
|
|
233
|
+
}
|
|
201
234
|
const ROOM_ATTACHMENT_START = "<codeshell-image-attachments>";
|
|
202
235
|
const ROOM_ATTACHMENT_END = "</codeshell-image-attachments>";
|
|
203
236
|
function roomAttachmentSummary(attachments) {
|
|
@@ -240,6 +273,10 @@ export function stripRoomAttachmentBlock(text) {
|
|
|
240
273
|
export class RoomManager {
|
|
241
274
|
opts;
|
|
242
275
|
agents = new Map();
|
|
276
|
+
/** Permission mode each live agent was spawned with. The mode is a spawn-time
|
|
277
|
+
* CLI argument, so room metadata alone cannot prove what a process runs. */
|
|
278
|
+
agentModes = new WeakMap();
|
|
279
|
+
modelTraces = new Map();
|
|
243
280
|
/** Rooms whose visible output is currently sourced from the external CLI's
|
|
244
281
|
* transcript tail. While present, the resident agent's stdout mirror is
|
|
245
282
|
* suppressed so the same CLI output is not pushed twice. */
|
|
@@ -443,7 +480,7 @@ export class RoomManager {
|
|
|
443
480
|
const id = `room_${this.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
444
481
|
const meta = {
|
|
445
482
|
id,
|
|
446
|
-
name: input.name ?? input.cwd
|
|
483
|
+
name: input.name ?? defaultRoomName(input.cwd),
|
|
447
484
|
cwd: input.cwd,
|
|
448
485
|
kind: input.kind ?? "claude-code",
|
|
449
486
|
permissionMode: input.permissionMode ?? "default",
|
|
@@ -762,6 +799,9 @@ export class RoomManager {
|
|
|
762
799
|
this.writeRoomMeta(meta.id, { ...meta, permissionMode: mode });
|
|
763
800
|
this.close(meta.id); // stop the old-mode process so open() respawns fresh
|
|
764
801
|
}
|
|
802
|
+
// Metadata alone cannot prove the live process mode (a concurrent takeover
|
|
803
|
+
// may have rewritten it), so also compare the agent's spawn-time mode.
|
|
804
|
+
this.retireAgentUnlessMode(meta.id, mode);
|
|
765
805
|
const { status } = this.open(meta.id);
|
|
766
806
|
return { roomId: meta.id, status };
|
|
767
807
|
}
|
|
@@ -828,6 +868,9 @@ export class RoomManager {
|
|
|
828
868
|
linkedSessionMode: undefined,
|
|
829
869
|
};
|
|
830
870
|
this.writeRoomMeta(roomId, controllableMeta);
|
|
871
|
+
// Another view may have taken over and reopened this session under a
|
|
872
|
+
// different mode. Never report default while feeding that process.
|
|
873
|
+
this.retireAgentUnlessMode(roomId, controllableMeta.permissionMode);
|
|
831
874
|
const { status } = this.open(roomId);
|
|
832
875
|
if (status !== "running") {
|
|
833
876
|
throw new Error("linked session room is unavailable");
|
|
@@ -847,8 +890,10 @@ export class RoomManager {
|
|
|
847
890
|
catch {
|
|
848
891
|
throw new Error("linked session transcript is unavailable");
|
|
849
892
|
}
|
|
850
|
-
if (!resolvedTarget
|
|
851
|
-
|
|
893
|
+
if (!resolvedTarget) {
|
|
894
|
+
throw new Error("linked session transcript was not found for the requested session");
|
|
895
|
+
}
|
|
896
|
+
if (typeof resolvedTarget.externalSessionId !== "string" ||
|
|
852
897
|
typeof resolvedTarget.cwd !== "string" ||
|
|
853
898
|
resolvedTarget.externalSessionId !== target.externalSessionId ||
|
|
854
899
|
resolvedTarget.kind !== target.kind ||
|
|
@@ -894,8 +939,10 @@ export class RoomManager {
|
|
|
894
939
|
agent.respondControl(requestId, decision);
|
|
895
940
|
return true;
|
|
896
941
|
}
|
|
942
|
+
// `answers` (keyed by question text) answers every question of a
|
|
943
|
+
// multi-question request; the legacy single `answer` maps to the first.
|
|
897
944
|
const prompt = askUserPrompt(pending);
|
|
898
|
-
const answersByQuestion = prompt ? { [prompt.question]: decision.answer ?? "" } : {};
|
|
945
|
+
const answersByQuestion = decision.answers ?? (prompt ? { [prompt.question]: decision.answer ?? "" } : {});
|
|
899
946
|
agent.respondControl(requestId, {
|
|
900
947
|
behavior: "allow",
|
|
901
948
|
updatedInput: buildAskUserUpdatedInput(pending, answersByQuestion),
|
|
@@ -931,6 +978,7 @@ export class RoomManager {
|
|
|
931
978
|
this.onAgentEvent(id, event);
|
|
932
979
|
});
|
|
933
980
|
this.agents.set(id, agent);
|
|
981
|
+
this.agentModes.set(agent, meta.permissionMode);
|
|
934
982
|
try {
|
|
935
983
|
agent.start();
|
|
936
984
|
}
|
|
@@ -953,6 +1001,12 @@ export class RoomManager {
|
|
|
953
1001
|
}
|
|
954
1002
|
return { status: "running" };
|
|
955
1003
|
}
|
|
1004
|
+
/** Stop a live agent whose spawn-time permission mode differs from `mode`. */
|
|
1005
|
+
retireAgentUnlessMode(id, mode) {
|
|
1006
|
+
const agent = this.agents.get(id);
|
|
1007
|
+
if (agent && this.agentModes.get(agent) !== mode)
|
|
1008
|
+
this.close(id);
|
|
1009
|
+
}
|
|
956
1010
|
onAgentEvent(id, event) {
|
|
957
1011
|
// Buffered while send() is deciding acceptance — replayed right after the
|
|
958
1012
|
// user turn is appended, so a synchronous agent reply cannot precede it.
|
|
@@ -962,6 +1016,10 @@ export class RoomManager {
|
|
|
962
1016
|
this.deferredEmits.set(id, queued);
|
|
963
1017
|
return;
|
|
964
1018
|
}
|
|
1019
|
+
const trace = this.modelTraces.get(id);
|
|
1020
|
+
trace?.observe(event);
|
|
1021
|
+
if (trace?.ended)
|
|
1022
|
+
this.modelTraces.delete(id);
|
|
965
1023
|
if (this.transcriptFollowedRooms.has(id) &&
|
|
966
1024
|
(event.type === "text" ||
|
|
967
1025
|
event.type === "tool" ||
|
|
@@ -1015,8 +1073,10 @@ export class RoomManager {
|
|
|
1015
1073
|
// `answers` record the CLI requires. Auto-allowing the unanswered input
|
|
1016
1074
|
// is what made claude report "The user did not answer the questions".
|
|
1017
1075
|
if (event.toolName === "AskUserQuestion") {
|
|
1018
|
-
const
|
|
1019
|
-
if (
|
|
1076
|
+
const prompt = askUserPrompt(event.input);
|
|
1077
|
+
if (prompt) {
|
|
1078
|
+
const questions = askUserQuestions(event.input);
|
|
1079
|
+
const askUser = questions.length > 1 ? { ...prompt, questions } : prompt;
|
|
1020
1080
|
this.pendingAskUser.set(`${id}:${event.requestId}`, event.input);
|
|
1021
1081
|
this.append(id, {
|
|
1022
1082
|
from: "agent",
|
|
@@ -1109,6 +1169,25 @@ export class RoomManager {
|
|
|
1109
1169
|
// Rejected: nothing about this turn reaches history or the phone.
|
|
1110
1170
|
return false;
|
|
1111
1171
|
}
|
|
1172
|
+
if (this.opts.modelTracing) {
|
|
1173
|
+
try {
|
|
1174
|
+
let trace = this.modelTraces.get(id);
|
|
1175
|
+
if (!trace || trace.ended) {
|
|
1176
|
+
trace = createRoomModelTrace({
|
|
1177
|
+
storageDir: this.opts.modelTracing.storageDir,
|
|
1178
|
+
workspacePath: meta.cwd,
|
|
1179
|
+
roomId: id,
|
|
1180
|
+
kind: meta.kind ?? "claude",
|
|
1181
|
+
parameters: this.opts.modelTracing.parameters?.(meta),
|
|
1182
|
+
});
|
|
1183
|
+
this.modelTraces.set(id, trace);
|
|
1184
|
+
}
|
|
1185
|
+
trace.submit(agentText);
|
|
1186
|
+
}
|
|
1187
|
+
catch {
|
|
1188
|
+
// Observability cannot change an already accepted external submission.
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1112
1191
|
this.append(id, {
|
|
1113
1192
|
from: "user",
|
|
1114
1193
|
type: "text",
|
|
@@ -1131,6 +1210,8 @@ export class RoomManager {
|
|
|
1131
1210
|
// throttled write.
|
|
1132
1211
|
this.flushLastActive(id);
|
|
1133
1212
|
const agent = this.agents.get(id);
|
|
1213
|
+
this.modelTraces.get(id)?.finish("cancelled");
|
|
1214
|
+
this.modelTraces.delete(id);
|
|
1134
1215
|
try {
|
|
1135
1216
|
agent?.stop();
|
|
1136
1217
|
}
|
|
@@ -1147,6 +1228,9 @@ export class RoomManager {
|
|
|
1147
1228
|
closeAll() {
|
|
1148
1229
|
const ids = new Set([...this.agents.keys(), ...this.transcriptFollowedRooms]);
|
|
1149
1230
|
let firstError;
|
|
1231
|
+
for (const trace of this.modelTraces.values())
|
|
1232
|
+
trace.finish("cancelled");
|
|
1233
|
+
this.modelTraces.clear();
|
|
1150
1234
|
for (const agent of this.agents.values()) {
|
|
1151
1235
|
try {
|
|
1152
1236
|
agent.stop();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ModelTraceStatus } from "@cjhyy/code-shell-core/internal";
|
|
2
|
+
import type { ResidentAgentEvent } from "./resident-agent.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resident Claude accepts overlapping inputs without request IDs in its output.
|
|
5
|
+
* A batch preserves observed order without inventing a prompt-to-response map.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createRoomModelTrace(options: {
|
|
8
|
+
storageDir: string;
|
|
9
|
+
workspacePath: string;
|
|
10
|
+
roomId: string;
|
|
11
|
+
kind: string;
|
|
12
|
+
parameters?: Record<string, unknown>;
|
|
13
|
+
}): {
|
|
14
|
+
readonly ended: boolean;
|
|
15
|
+
submit(input: string): void;
|
|
16
|
+
observe(event: ResidentAgentEvent): void;
|
|
17
|
+
finish: (status: ModelTraceStatus) => void;
|
|
18
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { recordExternalModelTrace, recordModelTraceEvent, withModelTraceContext, ModelTraceStore, } from "@cjhyy/code-shell-core/internal";
|
|
3
|
+
const MAX_TEXT = 128 * 1024;
|
|
4
|
+
const MAX_INPUTS = 128;
|
|
5
|
+
/**
|
|
6
|
+
* Resident Claude accepts overlapping inputs without request IDs in its output.
|
|
7
|
+
* A batch preserves observed order without inventing a prompt-to-response map.
|
|
8
|
+
*/
|
|
9
|
+
export function createRoomModelTrace(options) {
|
|
10
|
+
const context = {
|
|
11
|
+
storageDir: options.storageDir,
|
|
12
|
+
workspacePath: options.workspacePath,
|
|
13
|
+
sessionId: undefined,
|
|
14
|
+
runId: undefined,
|
|
15
|
+
parentSessionId: undefined,
|
|
16
|
+
ephemeral: false,
|
|
17
|
+
source: `external-room-${options.kind}`,
|
|
18
|
+
purpose: "external_room_batch",
|
|
19
|
+
};
|
|
20
|
+
const startedAt = Date.now();
|
|
21
|
+
const traceId = randomUUID();
|
|
22
|
+
const store = new ModelTraceStore({ storageDir: options.storageDir });
|
|
23
|
+
const policyRevision = store.getConfig().policyRevision;
|
|
24
|
+
const contentEnabled = () => {
|
|
25
|
+
try {
|
|
26
|
+
const config = store.getConfig();
|
|
27
|
+
return (capture &&
|
|
28
|
+
config.enabled &&
|
|
29
|
+
config.captureContent &&
|
|
30
|
+
config.policyRevision === policyRevision);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const common = { ...context, traceId, provider: options.kind, model: "CLI default", startedAt };
|
|
37
|
+
let recordId;
|
|
38
|
+
let capture = false;
|
|
39
|
+
let pending = 0;
|
|
40
|
+
let batchFailed = false;
|
|
41
|
+
let batchCancelled = false;
|
|
42
|
+
let submissionCount = 0;
|
|
43
|
+
let ended = false;
|
|
44
|
+
let text = "";
|
|
45
|
+
let error;
|
|
46
|
+
const messages = [];
|
|
47
|
+
const tools = new Map();
|
|
48
|
+
const write = (status, endedAt) => {
|
|
49
|
+
const result = recordExternalModelTrace({
|
|
50
|
+
...common,
|
|
51
|
+
id: recordId,
|
|
52
|
+
status,
|
|
53
|
+
endedAt,
|
|
54
|
+
input: !recordId || capture
|
|
55
|
+
? {
|
|
56
|
+
messages,
|
|
57
|
+
parameters: {
|
|
58
|
+
...options.parameters,
|
|
59
|
+
roomId: options.roomId,
|
|
60
|
+
submissionCount,
|
|
61
|
+
omittedInputCount: Math.max(0, submissionCount - messages.length),
|
|
62
|
+
outputAttribution: "accepted-room-batch-visible-output",
|
|
63
|
+
limitation: "Overlapping submissions have no per-request CLI output identity; internal model requests are opaque.",
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
: undefined,
|
|
67
|
+
output: capture && endedAt !== undefined ? { text, stopReason: status } : undefined,
|
|
68
|
+
error,
|
|
69
|
+
});
|
|
70
|
+
if (result && !recordId) {
|
|
71
|
+
recordId = result.id;
|
|
72
|
+
capture = result.inputState !== "not-recorded";
|
|
73
|
+
if (!capture)
|
|
74
|
+
messages.length = 0;
|
|
75
|
+
}
|
|
76
|
+
if (!result && !recordId)
|
|
77
|
+
messages.length = 0;
|
|
78
|
+
};
|
|
79
|
+
const span = (input) => withModelTraceContext(context, () => recordModelTraceEvent({
|
|
80
|
+
...input,
|
|
81
|
+
traceId,
|
|
82
|
+
...(!contentEnabled()
|
|
83
|
+
? {
|
|
84
|
+
inputState: "not-recorded",
|
|
85
|
+
outputState: "not-recorded",
|
|
86
|
+
error: input.error ? "External room operation failed" : undefined,
|
|
87
|
+
}
|
|
88
|
+
: {}),
|
|
89
|
+
}));
|
|
90
|
+
const finish = (status) => {
|
|
91
|
+
if (ended)
|
|
92
|
+
return;
|
|
93
|
+
ended = true;
|
|
94
|
+
const endedAt = Date.now();
|
|
95
|
+
for (const tool of tools.values())
|
|
96
|
+
span({ ...tool, endedAt, status: status === "failed" ? "failed" : "cancelled" });
|
|
97
|
+
tools.clear();
|
|
98
|
+
if (recordId)
|
|
99
|
+
write(status, endedAt);
|
|
100
|
+
messages.length = 0;
|
|
101
|
+
text = "";
|
|
102
|
+
};
|
|
103
|
+
return {
|
|
104
|
+
get ended() {
|
|
105
|
+
return ended;
|
|
106
|
+
},
|
|
107
|
+
submit(input) {
|
|
108
|
+
if (ended)
|
|
109
|
+
return;
|
|
110
|
+
submissionCount++;
|
|
111
|
+
pending++;
|
|
112
|
+
if ((!recordId || contentEnabled()) && messages.length < MAX_INPUTS)
|
|
113
|
+
messages.push({ role: "user", content: input.slice(0, MAX_TEXT) });
|
|
114
|
+
write("running");
|
|
115
|
+
},
|
|
116
|
+
observe(event) {
|
|
117
|
+
if (ended || !recordId)
|
|
118
|
+
return;
|
|
119
|
+
switch (event.type) {
|
|
120
|
+
case "text":
|
|
121
|
+
if (contentEnabled())
|
|
122
|
+
text = (text + event.text).slice(0, MAX_TEXT);
|
|
123
|
+
else if (capture && !text.endsWith("[Content capture paused]"))
|
|
124
|
+
text = (text + "\n[Content capture paused]").slice(0, MAX_TEXT);
|
|
125
|
+
break;
|
|
126
|
+
case "tool": {
|
|
127
|
+
if (tools.size >= MAX_INPUTS)
|
|
128
|
+
break;
|
|
129
|
+
const tool = span({
|
|
130
|
+
kind: "tool",
|
|
131
|
+
name: event.tool,
|
|
132
|
+
startedAt: Date.now(),
|
|
133
|
+
status: event.id ? "running" : "observed",
|
|
134
|
+
...(event.id ? {} : { endedAt: Date.now() }),
|
|
135
|
+
input: event.input,
|
|
136
|
+
});
|
|
137
|
+
if (tool && event.id)
|
|
138
|
+
tools.set(event.id, tool);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case "tool_result": {
|
|
142
|
+
const tool = event.id ? tools.get(event.id) : undefined;
|
|
143
|
+
if (tool) {
|
|
144
|
+
span({
|
|
145
|
+
...tool,
|
|
146
|
+
endedAt: Date.now(),
|
|
147
|
+
status: event.isError ? "failed" : "completed",
|
|
148
|
+
output: event.summary,
|
|
149
|
+
outputState: undefined,
|
|
150
|
+
});
|
|
151
|
+
tools.delete(event.id);
|
|
152
|
+
}
|
|
153
|
+
else
|
|
154
|
+
span({
|
|
155
|
+
kind: "tool",
|
|
156
|
+
name: "External CLI tool result (identity unavailable)",
|
|
157
|
+
startedAt: Date.now(),
|
|
158
|
+
status: event.isError ? "failed" : "completed",
|
|
159
|
+
output: event.summary,
|
|
160
|
+
});
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
case "approval_request":
|
|
164
|
+
span({
|
|
165
|
+
kind: "approval",
|
|
166
|
+
name: event.toolName,
|
|
167
|
+
startedAt: Date.now(),
|
|
168
|
+
status: "requested",
|
|
169
|
+
input: event.input,
|
|
170
|
+
});
|
|
171
|
+
break;
|
|
172
|
+
case "error":
|
|
173
|
+
error = contentEnabled() ? event.error.slice(0, MAX_TEXT) : "External room CLI failed";
|
|
174
|
+
finish("failed");
|
|
175
|
+
break;
|
|
176
|
+
case "turn_end":
|
|
177
|
+
span({
|
|
178
|
+
kind: "lifecycle",
|
|
179
|
+
name: "External room CLI turn boundary",
|
|
180
|
+
startedAt: Date.now(),
|
|
181
|
+
status: event.reason,
|
|
182
|
+
});
|
|
183
|
+
batchFailed ||= /error|fail/i.test(event.reason);
|
|
184
|
+
batchCancelled ||= /abort|cancel|interrupt/i.test(event.reason);
|
|
185
|
+
pending = Math.max(0, pending - 1);
|
|
186
|
+
if (pending === 0)
|
|
187
|
+
finish(batchFailed ? "failed" : batchCancelled ? "cancelled" : "completed");
|
|
188
|
+
break;
|
|
189
|
+
case "exit":
|
|
190
|
+
finish(event.signal ? "cancelled" : "failed");
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
finish,
|
|
195
|
+
};
|
|
196
|
+
}
|