@cjhyy/code-shell-server 0.9.43 → 0.9.46
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/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 +13 -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 +8 -0
- package/dist/mobile-remote/remote-host-manager.js +23 -8
- package/dist/mobile-remote/resident-agent.js +17 -4
- package/dist/mobile-remote/room-manager.d.ts +23 -13
- package/dist/mobile-remote/room-manager.js +102 -20
- package/dist/mobile-remote/room-model-tracing.d.ts +18 -0
- package/dist/mobile-remote/room-model-tracing.js +196 -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/directory-bookmarks.js +62 -29
- package/dist/panels/http.d.ts +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/runtime.d.ts +1 -0
- package/dist/panels/runtime.js +54 -6
- package/dist/panels/tool-jobs.d.ts +2 -0
- package/dist/panels/tool-jobs.js +21 -1
- 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
package/dist/links/types.d.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
|
-
export type { LinkAuthorization, LinkAuthMode, LinkAuthorizationInput, LinkAuthorizationResponse, LinkAuthorizationStep, LinkConnectionInput, LinkErrorCode, LinkSnapshot, MaskedLinkConnection, TokenConnectionInput, } from "@cjhyy/code-shell-link";
|
|
1
|
+
export type { LinkAuthorization, LinkAuthorizationFailure, LinkAuthMode, LinkAuthorizationInput, LinkAuthorizationResponse, LinkAuthorizationStep, LinkConnectionInput, LinkErrorCode, LinkSnapshot, MaskedLinkConnection, TokenConnectionInput, } from "@cjhyy/code-shell-link";
|
|
2
2
|
export interface LinkOperationContext {
|
|
3
3
|
ownerId: string;
|
|
4
4
|
/** Rechecked after every external await and immediately before persistence. */
|
|
5
5
|
authorize: () => boolean | Promise<boolean>;
|
|
6
|
+
/** Native Host request lifetime; never accepted from the remote request body. */
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
}
|
|
9
|
+
/** Trusted Host integration. Secrets and registration settings never enter browser inputs. */
|
|
10
|
+
export interface HostLinkOAuthAdapter {
|
|
11
|
+
providerId: string;
|
|
12
|
+
methodId: string;
|
|
13
|
+
authModeId: string;
|
|
14
|
+
label: string;
|
|
15
|
+
authorize(input: {
|
|
16
|
+
id: string;
|
|
17
|
+
label: string;
|
|
18
|
+
previous: import("@cjhyy/code-shell-core").Credential | null;
|
|
19
|
+
signal: AbortSignal;
|
|
20
|
+
onAuthorizationUrl(url: string, expiresAt: string): Promise<void>;
|
|
21
|
+
}): Promise<import("@cjhyy/code-shell-core").Credential>;
|
|
22
|
+
/** The adapter owns its callback listener; the native browser must not bind it again. */
|
|
23
|
+
openAuthorizationUrl(url: string): Promise<void>;
|
|
24
|
+
/** Called after local authority is removed; must not delete a subsequently reconnected record. */
|
|
25
|
+
disconnect?(credential: import("@cjhyy/code-shell-core").Credential): Promise<void>;
|
|
6
26
|
}
|
|
@@ -14,8 +14,10 @@ import type { ResidentAgentEvent } from "./resident-agent.js";
|
|
|
14
14
|
* item.type "reasoning" → [] (thinking noise)
|
|
15
15
|
* item.type "command_execution" → tool(Bash) on start, tool_result on complete
|
|
16
16
|
* item.type "mcp_tool_call" → tool(server__tool) on start, tool_result on complete
|
|
17
|
+
* (result.content text blocks, or error.message on failure)
|
|
17
18
|
* item.type "web_search" → tool(WebSearch) on start
|
|
18
|
-
* item.type "file_change" → tool(Edit) on
|
|
19
|
+
* item.type "file_change" → tool(Edit) + tool_result on complete (only the
|
|
20
|
+
* completion carries `changes[]` {path, kind})
|
|
19
21
|
* {type:"turn.completed", usage} → turn_end
|
|
20
22
|
* {type:"turn.failed", error} → error
|
|
21
23
|
* {type:"error", message} → error
|
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
* item.type "reasoning" → [] (thinking noise)
|
|
14
14
|
* item.type "command_execution" → tool(Bash) on start, tool_result on complete
|
|
15
15
|
* item.type "mcp_tool_call" → tool(server__tool) on start, tool_result on complete
|
|
16
|
+
* (result.content text blocks, or error.message on failure)
|
|
16
17
|
* item.type "web_search" → tool(WebSearch) on start
|
|
17
|
-
* item.type "file_change" → tool(Edit) on
|
|
18
|
+
* item.type "file_change" → tool(Edit) + tool_result on complete (only the
|
|
19
|
+
* completion carries `changes[]` {path, kind})
|
|
18
20
|
* {type:"turn.completed", usage} → turn_end
|
|
19
21
|
* {type:"turn.failed", error} → error
|
|
20
22
|
* {type:"error", message} → error
|
|
@@ -33,6 +35,9 @@ export function parseCodexJsonLine(line) {
|
|
|
33
35
|
catch {
|
|
34
36
|
return [];
|
|
35
37
|
}
|
|
38
|
+
// A valid JSON scalar (e.g. `null`) is not an event; ignore it like noise.
|
|
39
|
+
if (!msg || typeof msg !== "object")
|
|
40
|
+
return [];
|
|
36
41
|
if (msg.type === "turn.completed")
|
|
37
42
|
return [{ type: "turn_end", reason: "completed" }];
|
|
38
43
|
if (msg.type === "turn.failed") {
|
|
@@ -71,7 +76,7 @@ export function parseCodexJsonLine(line) {
|
|
|
71
76
|
return [{
|
|
72
77
|
type: "tool_result",
|
|
73
78
|
id,
|
|
74
|
-
summary:
|
|
79
|
+
summary: mcpResultText(item).slice(0, 400),
|
|
75
80
|
isError: item.status === "failed" || Boolean(item.is_error),
|
|
76
81
|
}];
|
|
77
82
|
}
|
|
@@ -88,16 +93,24 @@ export function parseCodexJsonLine(line) {
|
|
|
88
93
|
return phase === "started"
|
|
89
94
|
? [{ type: "tool", id, tool: "WebSearch", summary: String(item.query ?? ""), input: { query: String(item.query ?? "") } }]
|
|
90
95
|
: [];
|
|
91
|
-
case "file_change":
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
input: item.kind !== undefined ? { path: String(item.path ?? ""), kind: item.kind } : { path: String(item.path ?? "") },
|
|
99
|
-
}]
|
|
96
|
+
case "file_change": {
|
|
97
|
+
if (phase !== "completed")
|
|
98
|
+
return [];
|
|
99
|
+
const changes = Array.isArray(item.changes)
|
|
100
|
+
? item.changes
|
|
101
|
+
.filter((c) => c && typeof c === "object" && typeof c.path === "string")
|
|
102
|
+
.map((c) => ({ path: c.path, kind: c.kind }))
|
|
100
103
|
: [];
|
|
104
|
+
return [
|
|
105
|
+
{ type: "tool", id, tool: "Edit", summary: changes.map((c) => c.path).join(", "), input: { changes } },
|
|
106
|
+
{
|
|
107
|
+
type: "tool_result",
|
|
108
|
+
id,
|
|
109
|
+
summary: changes.map((c) => `${typeof c.kind === "string" ? c.kind : "update"} ${c.path}`).join("\n").slice(0, 400),
|
|
110
|
+
isError: item.status === "failed",
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
}
|
|
101
114
|
default:
|
|
102
115
|
return [];
|
|
103
116
|
}
|
|
@@ -122,6 +135,27 @@ export function extractThreadId(line) {
|
|
|
122
135
|
}
|
|
123
136
|
return msg.type === "thread.started" && typeof msg.thread_id === "string" ? msg.thread_id : undefined;
|
|
124
137
|
}
|
|
138
|
+
/** Readable MCP outcome: the failure message, else the result's text content
|
|
139
|
+
* blocks, else its structured content (an older plain-string result passes). */
|
|
140
|
+
function mcpResultText(item) {
|
|
141
|
+
const failure = errText(item.error);
|
|
142
|
+
if (failure)
|
|
143
|
+
return failure;
|
|
144
|
+
const result = item.result ?? item.output;
|
|
145
|
+
if (result === undefined || result === null)
|
|
146
|
+
return "";
|
|
147
|
+
if (typeof result !== "object")
|
|
148
|
+
return String(result);
|
|
149
|
+
if (Array.isArray(result.content)) {
|
|
150
|
+
const text = result.content
|
|
151
|
+
.map((block) => (block?.type === "text" && typeof block.text === "string" ? block.text : ""))
|
|
152
|
+
.filter(Boolean)
|
|
153
|
+
.join("\n");
|
|
154
|
+
if (text)
|
|
155
|
+
return text;
|
|
156
|
+
}
|
|
157
|
+
return JSON.stringify(result.structured_content ?? result) ?? "";
|
|
158
|
+
}
|
|
125
159
|
function errText(e) {
|
|
126
160
|
if (typeof e === "string")
|
|
127
161
|
return e;
|
|
@@ -44,6 +44,9 @@ export async function dispatchMobileChatTurn(input) {
|
|
|
44
44
|
id: input.runId,
|
|
45
45
|
params: {
|
|
46
46
|
task: text,
|
|
47
|
+
...(input.modelTraceSource && /^panel:[a-z][a-z0-9-]{0,63}$/.test(input.modelTraceSource)
|
|
48
|
+
? { modelTraceSource: input.modelTraceSource }
|
|
49
|
+
: {}),
|
|
47
50
|
cwd,
|
|
48
51
|
sessionId: input.sessionId,
|
|
49
52
|
...(input.projectId ? { projectId: input.projectId } : {}),
|
|
@@ -9,6 +9,7 @@ const MAX_NAME_LENGTH = 512;
|
|
|
9
9
|
const MAX_PATH_LENGTH = 32_768;
|
|
10
10
|
const MAX_MESSAGE_LENGTH = 512 * 1024;
|
|
11
11
|
const MAX_HISTORY_LIMIT = 500;
|
|
12
|
+
const MAX_ASK_USER_ANSWERS = 32;
|
|
12
13
|
/** Runtime validation at the WebSocket boundary; TypeScript types do not validate JSON. */
|
|
13
14
|
export function parseMobileClientEvent(value) {
|
|
14
15
|
if (!isRecord(value) || typeof value.type !== "string")
|
|
@@ -29,6 +30,9 @@ export function parseMobileClientEvent(value) {
|
|
|
29
30
|
valid =
|
|
30
31
|
boundedOptionalEmptyString(event.text, MAX_MESSAGE_LENGTH) &&
|
|
31
32
|
optionalBoundedString(event.sessionId, MAX_ID_LENGTH) &&
|
|
33
|
+
(event.modelTraceSource === undefined ||
|
|
34
|
+
(typeof event.modelTraceSource === "string" &&
|
|
35
|
+
/^panel:[a-z][a-z0-9-]{0,63}$/.test(event.modelTraceSource))) &&
|
|
32
36
|
optionalBoundedString(event.clientMessageId, MAX_ID_LENGTH) &&
|
|
33
37
|
optionalAttachments(event.attachments);
|
|
34
38
|
break;
|
|
@@ -246,12 +250,21 @@ function isAttachment(value) {
|
|
|
246
250
|
? boundedString(value.dataUrl, MAX_MESSAGE_LENGTH)
|
|
247
251
|
: value.transport === "upload" && boundedString(value.uploadId, MAX_ID_LENGTH);
|
|
248
252
|
}
|
|
253
|
+
function isAnswerRecord(value) {
|
|
254
|
+
if (!isRecord(value))
|
|
255
|
+
return false;
|
|
256
|
+
const entries = Object.entries(value);
|
|
257
|
+
return (entries.length <= MAX_ASK_USER_ANSWERS &&
|
|
258
|
+
entries.every(([question, answer]) => question.length <= MAX_MESSAGE_LENGTH &&
|
|
259
|
+
boundedOptionalEmptyString(answer, MAX_MESSAGE_LENGTH)));
|
|
260
|
+
}
|
|
249
261
|
function isApprovalDecision(value) {
|
|
250
262
|
if (!isRecord(value))
|
|
251
263
|
return false;
|
|
252
264
|
if (value.behavior === "allow") {
|
|
253
265
|
return ((value.answer === undefined ||
|
|
254
266
|
boundedOptionalEmptyString(value.answer, MAX_MESSAGE_LENGTH)) &&
|
|
267
|
+
(value.answers === undefined || isAnswerRecord(value.answers)) &&
|
|
255
268
|
(value.updatedInput === undefined || isRecord(value.updatedInput)));
|
|
256
269
|
}
|
|
257
270
|
return value.behavior === "deny" && boundedOptionalEmptyString(value.message, MAX_MESSAGE_LENGTH);
|
|
@@ -16,7 +16,16 @@
|
|
|
16
16
|
export interface TranscriptHistoryItem {
|
|
17
17
|
kind: string;
|
|
18
18
|
text?: unknown;
|
|
19
|
+
/** Image blocks of a raw model-facing user input (it has no displayText). */
|
|
20
|
+
images?: unknown;
|
|
19
21
|
event?: unknown;
|
|
20
22
|
}
|
|
21
23
|
export type SessionTranscriptReader = (sessionId: string) => Promise<readonly TranscriptHistoryItem[]>;
|
|
22
|
-
|
|
24
|
+
/** The client's user-input display projection (Web transcriptUserDisplay), injected. */
|
|
25
|
+
export type UserInputDisplay = (data: {
|
|
26
|
+
content: unknown;
|
|
27
|
+
}) => {
|
|
28
|
+
text: string;
|
|
29
|
+
attachments: readonly unknown[];
|
|
30
|
+
};
|
|
31
|
+
export declare function buildSessionHistory(sessionId: string, getTranscript: SessionTranscriptReader, displayUserInput?: UserInputDisplay): Promise<unknown[]>;
|
|
@@ -1,23 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
* Build a session's history as a flat list of events the phone's streamReducer
|
|
3
|
-
* understands. We REUSE the desktop's transcript reader (transcriptToFoldItems
|
|
4
|
-
* via its getSessionTranscript) so there is one transcript parser, then project
|
|
5
|
-
* its FoldItems into reducer events:
|
|
6
|
-
* - { kind: "stream", event } → the raw StreamEvent (reducer eats it directly)
|
|
7
|
-
* - { kind: "user", text } → a synthetic { type: "user_message", text }
|
|
8
|
-
*
|
|
9
|
-
* The phone feeds these through the SAME reducer used for the live stream, so
|
|
10
|
-
* replay and live are identical (design §4).
|
|
11
|
-
*
|
|
12
|
-
* The transcript reader itself stays in the desktop package (it depends on
|
|
13
|
-
* renderer FoldItem types), so it is injected here instead of imported.
|
|
14
|
-
*/
|
|
15
|
-
export async function buildSessionHistory(sessionId, getTranscript) {
|
|
1
|
+
export async function buildSessionHistory(sessionId, getTranscript, displayUserInput) {
|
|
16
2
|
const items = await getTranscript(sessionId);
|
|
17
3
|
const events = [];
|
|
18
4
|
for (const item of items) {
|
|
19
5
|
if (item.kind === "user") {
|
|
20
|
-
events.push(
|
|
6
|
+
events.push(userHistoryEvent(item, displayUserInput));
|
|
21
7
|
}
|
|
22
8
|
else if (item.kind === "stream") {
|
|
23
9
|
events.push(item.event);
|
|
@@ -25,3 +11,25 @@ export async function buildSessionHistory(sessionId, getTranscript) {
|
|
|
25
11
|
}
|
|
26
12
|
return events;
|
|
27
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* A user input with image blocks but no displayText (e.g. an old paired
|
|
16
|
+
* chat.send) persisted the engine's model-facing text, including the
|
|
17
|
+
* attached-image path envelope and tool hint. Project it through the client's
|
|
18
|
+
* durable-replay display rules instead of showing that as the user's text.
|
|
19
|
+
*/
|
|
20
|
+
function userHistoryEvent(item, displayUserInput) {
|
|
21
|
+
const images = typeof item.images === "number" && item.images > 0 ? item.images : 0;
|
|
22
|
+
if (!images || typeof item.text !== "string" || !displayUserInput)
|
|
23
|
+
return { type: "user_message", text: item.text };
|
|
24
|
+
const display = displayUserInput({
|
|
25
|
+
content: [
|
|
26
|
+
{ type: "text", text: item.text },
|
|
27
|
+
...Array.from({ length: images }, () => ({ type: "image", source: {} })),
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
type: "user_message",
|
|
32
|
+
text: display.text,
|
|
33
|
+
...(display.attachments.length ? { attachments: display.attachments } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -126,6 +126,14 @@ export declare class RemoteHostManager extends EventEmitter {
|
|
|
126
126
|
* every authenticated mobile socket. Unauthenticated sockets are skipped so
|
|
127
127
|
* a half-paired client never sees session output. */
|
|
128
128
|
broadcastRaw(payload: string): void;
|
|
129
|
+
/**
|
|
130
|
+
* Queue one frame unless the peer already stopped reading. A paused tab or
|
|
131
|
+
* stalled network keeps its socket OPEN while ws buffers every live frame in
|
|
132
|
+
* Host memory; retire that socket instead so the client reconnects and
|
|
133
|
+
* recovers through its normal resume path. One large frame on a drained
|
|
134
|
+
* socket is still delivered. Returns false when retired.
|
|
135
|
+
*/
|
|
136
|
+
private sendBounded;
|
|
129
137
|
private releaseSocket;
|
|
130
138
|
private bindSocketDevice;
|
|
131
139
|
/** Call after revoking/removing a trusted device to release both transports immediately. */
|
|
@@ -8,6 +8,8 @@ import { serveMobile } from "./mobile-static.js";
|
|
|
8
8
|
import { PairingTokenManager } from "./pairing.js";
|
|
9
9
|
import { parseMobileClientEvent } from "./mobile-client-event-validator.js";
|
|
10
10
|
import { isRelayOrigin } from "../remote-relay/protocol.js";
|
|
11
|
+
/** Outbound bytes one paired socket may leave unread before it is retired. */
|
|
12
|
+
const MAX_SOCKET_BACKLOG_BYTES = 4 * 1024 * 1024;
|
|
11
13
|
/**
|
|
12
14
|
* Pick the Mac's real LAN IPv4 so a phone on the same Wi-Fi can reach the
|
|
13
15
|
* remote host. We bind to a concrete LAN address (NOT 0.0.0.0) per the design's
|
|
@@ -442,7 +444,7 @@ export class RemoteHostManager extends EventEmitter {
|
|
|
442
444
|
const payload = JSON.stringify(event);
|
|
443
445
|
for (const client of this.wss?.clients ?? []) {
|
|
444
446
|
if (client.readyState === client.OPEN && this.authed.get(client) === deviceId) {
|
|
445
|
-
|
|
447
|
+
this.sendBounded(client, payload);
|
|
446
448
|
}
|
|
447
449
|
}
|
|
448
450
|
}
|
|
@@ -464,12 +466,8 @@ export class RemoteHostManager extends EventEmitter {
|
|
|
464
466
|
this.viewers.get(client) === viewerId) {
|
|
465
467
|
// A selected viewer cannot accumulate an unbounded outbound page queue
|
|
466
468
|
// by issuing new reads while refusing to consume prior replies.
|
|
467
|
-
if (client
|
|
468
|
-
this.releaseSocket(client);
|
|
469
|
-
client.terminate();
|
|
469
|
+
if (!this.sendBounded(client, payload))
|
|
470
470
|
return;
|
|
471
|
-
}
|
|
472
|
-
client.send(payload);
|
|
473
471
|
}
|
|
474
472
|
}
|
|
475
473
|
}
|
|
@@ -477,7 +475,7 @@ export class RemoteHostManager extends EventEmitter {
|
|
|
477
475
|
sendRawToDevice(deviceId, payload) {
|
|
478
476
|
for (const client of this.wss?.clients ?? []) {
|
|
479
477
|
if (client.readyState === client.OPEN && this.authed.get(client) === deviceId) {
|
|
480
|
-
|
|
478
|
+
this.sendBounded(client, payload);
|
|
481
479
|
}
|
|
482
480
|
}
|
|
483
481
|
}
|
|
@@ -487,10 +485,27 @@ export class RemoteHostManager extends EventEmitter {
|
|
|
487
485
|
broadcastRaw(payload) {
|
|
488
486
|
for (const client of this.wss?.clients ?? []) {
|
|
489
487
|
if (client.readyState === client.OPEN && this.authed.has(client)) {
|
|
490
|
-
|
|
488
|
+
this.sendBounded(client, payload);
|
|
491
489
|
}
|
|
492
490
|
}
|
|
493
491
|
}
|
|
492
|
+
/**
|
|
493
|
+
* Queue one frame unless the peer already stopped reading. A paused tab or
|
|
494
|
+
* stalled network keeps its socket OPEN while ws buffers every live frame in
|
|
495
|
+
* Host memory; retire that socket instead so the client reconnects and
|
|
496
|
+
* recovers through its normal resume path. One large frame on a drained
|
|
497
|
+
* socket is still delivered. Returns false when retired.
|
|
498
|
+
*/
|
|
499
|
+
sendBounded(client, payload) {
|
|
500
|
+
if (client.bufferedAmount > 0 &&
|
|
501
|
+
client.bufferedAmount + Buffer.byteLength(payload) > MAX_SOCKET_BACKLOG_BYTES) {
|
|
502
|
+
this.releaseSocket(client);
|
|
503
|
+
client.terminate();
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
client.send(payload);
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
494
509
|
releaseSocket(ws) {
|
|
495
510
|
const deviceId = this.authed.get(ws);
|
|
496
511
|
const viewerId = this.viewers.get(ws);
|
|
@@ -49,6 +49,9 @@ export function parseStreamJsonLine(line) {
|
|
|
49
49
|
catch {
|
|
50
50
|
return [];
|
|
51
51
|
}
|
|
52
|
+
// A valid but non-record line (e.g. `null`) carries no event.
|
|
53
|
+
if (!msg || typeof msg !== "object" || Array.isArray(msg))
|
|
54
|
+
return [];
|
|
52
55
|
const out = [];
|
|
53
56
|
if (msg.type === "control_request" && msg.request?.subtype === "can_use_tool") {
|
|
54
57
|
return [
|
|
@@ -159,6 +162,15 @@ export class ResidentAgentProcess {
|
|
|
159
162
|
if (/error/i.test(text))
|
|
160
163
|
this.opts.onEvent({ type: "error", error: text.trim().slice(0, 400) });
|
|
161
164
|
});
|
|
165
|
+
let exited = false;
|
|
166
|
+
const finish = (code, signal) => {
|
|
167
|
+
if (exited)
|
|
168
|
+
return;
|
|
169
|
+
exited = true;
|
|
170
|
+
if (this.child === child)
|
|
171
|
+
this.child = undefined;
|
|
172
|
+
this.opts.onEvent({ type: "exit", code, signal });
|
|
173
|
+
};
|
|
162
174
|
child.on("error", (err) => {
|
|
163
175
|
const isMissing = err.code === "ENOENT";
|
|
164
176
|
this.opts.onEvent({
|
|
@@ -167,11 +179,12 @@ export class ResidentAgentProcess {
|
|
|
167
179
|
? `未找到命令 "${this.opts.command}"。请先安装 Claude Code CLI 并确保它在 PATH 中。`
|
|
168
180
|
: err.message,
|
|
169
181
|
});
|
|
182
|
+
// A failed spawn (ENOENT/EACCES/missing cwd) emits `error` without
|
|
183
|
+
// `exit`. Retire that child so the room can start a replacement.
|
|
184
|
+
if (child.pid === undefined)
|
|
185
|
+
finish(null, null);
|
|
170
186
|
});
|
|
171
|
-
child.on("exit", (code, signal) =>
|
|
172
|
-
this.child = undefined;
|
|
173
|
-
this.opts.onEvent({ type: "exit", code, signal });
|
|
174
|
-
});
|
|
187
|
+
child.on("exit", (code, signal) => finish(code, signal));
|
|
175
188
|
}
|
|
176
189
|
/** Feed one user turn. Returns false if the process isn't running. */
|
|
177
190
|
send(text) {
|
|
@@ -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;
|