@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
|
@@ -214,12 +214,21 @@ export class ResourceUploadIngest {
|
|
|
214
214
|
}
|
|
215
215
|
async cleanupExpired() {
|
|
216
216
|
await this.initialize();
|
|
217
|
-
for (const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
217
|
+
for (const key of [...this.records.keys()]) {
|
|
218
|
+
// Another upload may have renewed while cleanup waited on an earlier
|
|
219
|
+
// entry. Only the current expired record can cancel a running finish.
|
|
220
|
+
const record = this.records.get(key);
|
|
221
|
+
if (!record || record.expiresAt > this.options.now())
|
|
222
|
+
continue;
|
|
223
|
+
this.finishes.get(key)?.abort();
|
|
224
|
+
await this.locked(key, async () => {
|
|
225
|
+
// A write already holding this lock may acknowledge a chunk and renew
|
|
226
|
+
// the lease before cleanup enters. Never delete that renewed upload.
|
|
227
|
+
const current = this.records.get(key);
|
|
228
|
+
if (current && current.expiresAt <= this.options.now())
|
|
229
|
+
await this.remove(current);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
223
232
|
}
|
|
224
233
|
async begin(scope, raw) {
|
|
225
234
|
await this.authorize(scope);
|
package/dist/panels/runtime.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export interface PanelRuntimeOptions {
|
|
|
34
34
|
bindingCwd?: string;
|
|
35
35
|
projectPackages?: boolean;
|
|
36
36
|
dataDir: string;
|
|
37
|
+
traceStorageDir?: string;
|
|
37
38
|
host: "hub" | "desktop";
|
|
38
39
|
/** Host authority only, never a field accepted from Panel HTTP requests. */
|
|
39
40
|
projectScope?: () => Promise<PanelProjectScopeBinding | undefined>;
|
package/dist/panels/runtime.js
CHANGED
|
@@ -10,6 +10,7 @@ import { panelRuntimeApiVersion, panelProcessMethods, panelResourceMethods, pane
|
|
|
10
10
|
import { PanelResourceService } from "./resources/service.js";
|
|
11
11
|
import { servePanelResource } from "./resources/http.js";
|
|
12
12
|
import { panelConnections, panelConnectionIds, materializePanelConnections, } from "./connections.js";
|
|
13
|
+
import { PanelModelTraceService, panelModelTraceMethods, panelModelTraceLimits, } from "./model-traces.js";
|
|
13
14
|
import { PanelRuntimeServices } from "./runtime-services.js";
|
|
14
15
|
import { PanelToolJobService, toolJobLimits, } from "./tool-jobs.js";
|
|
15
16
|
import { createPanelToolExecutor } from "./tool-executor.js";
|
|
@@ -26,6 +27,7 @@ const MAX_FILE = 16 * 1024 * 1024;
|
|
|
26
27
|
const GRANT_TTL = 30 * 60_000;
|
|
27
28
|
const METHODS = [
|
|
28
29
|
"context.get",
|
|
30
|
+
...panelModelTraceMethods,
|
|
29
31
|
"storage.get",
|
|
30
32
|
"storage.getSnapshot",
|
|
31
33
|
"storage.compareAndSet",
|
|
@@ -80,6 +82,7 @@ const MIME = {
|
|
|
80
82
|
".html": "text/html; charset=utf-8",
|
|
81
83
|
".js": "text/javascript; charset=utf-8",
|
|
82
84
|
".mjs": "text/javascript; charset=utf-8",
|
|
85
|
+
".wasm": "application/wasm",
|
|
83
86
|
".css": "text/css; charset=utf-8",
|
|
84
87
|
".json": "application/json",
|
|
85
88
|
".md": "text/markdown; charset=utf-8",
|
|
@@ -420,6 +423,10 @@ export function createPanelRuntime(options) {
|
|
|
420
423
|
const assets = new Map();
|
|
421
424
|
const now = options.now ?? Date.now;
|
|
422
425
|
const services = new PanelRuntimeServices({ dataDir: options.dataDir });
|
|
426
|
+
const modelTraces = new PanelModelTraceService({
|
|
427
|
+
storageDir: options.traceStorageDir,
|
|
428
|
+
now: options.now,
|
|
429
|
+
});
|
|
423
430
|
const installed = options.listInstalled ??
|
|
424
431
|
(options.projectPackages
|
|
425
432
|
? async (appId) => (await inspectProjectPanelApps(options.bindingCwd ?? options.cwd, appId)).apps
|
|
@@ -522,9 +529,31 @@ export function createPanelRuntime(options) {
|
|
|
522
529
|
return app;
|
|
523
530
|
}
|
|
524
531
|
const processes = new PanelAppProcessService({
|
|
532
|
+
modelTraces,
|
|
533
|
+
modelTraceOwner: (owner) => {
|
|
534
|
+
const task = toolOwners.get(owner.guestId);
|
|
535
|
+
if (task)
|
|
536
|
+
return {
|
|
537
|
+
appId: owner.appId,
|
|
538
|
+
revision: owner.revision,
|
|
539
|
+
workspacePath: options.cwd,
|
|
540
|
+
traceId: task.taskId,
|
|
541
|
+
taskId: task.taskId,
|
|
542
|
+
};
|
|
543
|
+
const grant = byGuest.get(owner.guestId);
|
|
544
|
+
if (!grant || grant.revision !== owner.revision)
|
|
545
|
+
return undefined;
|
|
546
|
+
return {
|
|
547
|
+
appId: owner.appId,
|
|
548
|
+
revision: owner.revision,
|
|
549
|
+
workspacePath: options.cwd,
|
|
550
|
+
sessionId: typeof grant.context.sessionId === "string" ? grant.context.sessionId : undefined,
|
|
551
|
+
};
|
|
552
|
+
},
|
|
525
553
|
acquireExecution: (owner) => panelExecutionGate.enter({
|
|
526
554
|
appId: owner.appId,
|
|
527
555
|
projectPath: options.bindingCwd ?? options.cwd,
|
|
556
|
+
holder: { kind: "process" },
|
|
528
557
|
}),
|
|
529
558
|
approvalScope: "guest",
|
|
530
559
|
resolvePackageEntry: async (owner, name) => {
|
|
@@ -751,7 +780,7 @@ export function createPanelRuntime(options) {
|
|
|
751
780
|
resources,
|
|
752
781
|
owner: (job, send) => {
|
|
753
782
|
const guestId = nextToolOwner--;
|
|
754
|
-
toolOwners.set(guestId, job.scope);
|
|
783
|
+
toolOwners.set(guestId, { ...job.scope, taskId: job.id });
|
|
755
784
|
return {
|
|
756
785
|
guestId,
|
|
757
786
|
appId: job.scope.appId,
|
|
@@ -826,6 +855,7 @@ export function createPanelRuntime(options) {
|
|
|
826
855
|
assets.delete(grant.asset);
|
|
827
856
|
byGuest.delete(grant.guestId);
|
|
828
857
|
processes.revokeGuest(grant.guestId);
|
|
858
|
+
modelTraces.cancelOwner(`grant:${grant.id}`);
|
|
829
859
|
agentTasks?.revokeInstance(grant.id);
|
|
830
860
|
for (const item of grant.confirmations.values())
|
|
831
861
|
item.finish(false);
|
|
@@ -912,7 +942,11 @@ export function createPanelRuntime(options) {
|
|
|
912
942
|
};
|
|
913
943
|
}
|
|
914
944
|
async function onPanelAction(scope, input) {
|
|
915
|
-
return panelExecutionGate.run({
|
|
945
|
+
return panelExecutionGate.run({
|
|
946
|
+
appId: scope.appId,
|
|
947
|
+
projectPath: options.bindingCwd ?? options.cwd,
|
|
948
|
+
holder: { kind: "panel-call" },
|
|
949
|
+
}, () => onPanelActionAdmitted(scope, input));
|
|
916
950
|
}
|
|
917
951
|
async function onPanelActionAdmitted(scope, input) {
|
|
918
952
|
const grant = grants.get(scope.instanceId);
|
|
@@ -951,6 +985,7 @@ export function createPanelRuntime(options) {
|
|
|
951
985
|
const releaseExecution = panelExecutionGate.enter({
|
|
952
986
|
appId: grant.app.id,
|
|
953
987
|
projectPath: options.bindingCwd ?? options.cwd,
|
|
988
|
+
holder: { kind: "agent-tool" },
|
|
954
989
|
});
|
|
955
990
|
const requestId = randomBytes(18).toString("base64url");
|
|
956
991
|
const value = await new Promise((done) => {
|
|
@@ -1089,6 +1124,8 @@ export function createPanelRuntime(options) {
|
|
|
1089
1124
|
locale: typeof input.locale === "string" ? input.locale.slice(0, 32) : "zh-CN",
|
|
1090
1125
|
apiVersion: panelRuntimeApiVersion,
|
|
1091
1126
|
capabilities: {
|
|
1127
|
+
packageAssets: { wasm: true, fetch: "package" },
|
|
1128
|
+
modelTraces: { writeOnly: true, coverage: "logical-input", ...panelModelTraceLimits },
|
|
1092
1129
|
...panelRuntimeCapabilities({
|
|
1093
1130
|
process: app.permissions.includes("process"),
|
|
1094
1131
|
cookieProcess: app.permissions.includes("process") &&
|
|
@@ -1189,7 +1226,9 @@ export function createPanelRuntime(options) {
|
|
|
1189
1226
|
return false;
|
|
1190
1227
|
if (method.startsWith("tools."))
|
|
1191
1228
|
return !!app.agent?.tools.length;
|
|
1192
|
-
return (method === "context.get" ||
|
|
1229
|
+
return (method === "context.get" ||
|
|
1230
|
+
method.startsWith("modelTraces.") ||
|
|
1231
|
+
app.permissions.includes(methodPermission(method)));
|
|
1193
1232
|
}),
|
|
1194
1233
|
limitations: panel.compatibility.reasons,
|
|
1195
1234
|
};
|
|
@@ -1276,7 +1315,11 @@ export function createPanelRuntime(options) {
|
|
|
1276
1315
|
}
|
|
1277
1316
|
}
|
|
1278
1317
|
async function call(grant, input) {
|
|
1279
|
-
return panelExecutionGate.run({
|
|
1318
|
+
return panelExecutionGate.run({
|
|
1319
|
+
appId: grant.app.id,
|
|
1320
|
+
projectPath: options.bindingCwd ?? options.cwd,
|
|
1321
|
+
holder: { kind: "panel-call" },
|
|
1322
|
+
}, () => callAdmitted(grant, input));
|
|
1280
1323
|
}
|
|
1281
1324
|
async function callAdmitted(grant, input) {
|
|
1282
1325
|
if (!(await authorized(grant)))
|
|
@@ -1302,6 +1345,14 @@ export function createPanelRuntime(options) {
|
|
|
1302
1345
|
grant.calls = history;
|
|
1303
1346
|
if (method === "context.get")
|
|
1304
1347
|
return grant.context;
|
|
1348
|
+
if (method.startsWith("modelTraces."))
|
|
1349
|
+
return modelTraces.call({
|
|
1350
|
+
ownerKey: `grant:${grant.id}`,
|
|
1351
|
+
appId: grant.app.id,
|
|
1352
|
+
revision: grant.revision,
|
|
1353
|
+
workspacePath: options.cwd,
|
|
1354
|
+
sessionId: typeof grant.context.sessionId === "string" ? grant.context.sessionId : undefined,
|
|
1355
|
+
}, method, params);
|
|
1305
1356
|
if (method.startsWith("tasks."))
|
|
1306
1357
|
return dispatchToolJobs(grant, method, params);
|
|
1307
1358
|
if (method.startsWith("tools.")) {
|
|
@@ -1600,6 +1651,7 @@ export function createPanelRuntime(options) {
|
|
|
1600
1651
|
return {
|
|
1601
1652
|
effect: method,
|
|
1602
1653
|
prompt: value.prompt,
|
|
1654
|
+
modelTraceSource: `panel:${grant.app.id}`,
|
|
1603
1655
|
sessionId: grant.context.sessionId,
|
|
1604
1656
|
...(value.displayText ? { displayText: value.displayText } : {}),
|
|
1605
1657
|
};
|
|
@@ -1692,6 +1744,7 @@ export function createPanelRuntime(options) {
|
|
|
1692
1744
|
},
|
|
1693
1745
|
async close() {
|
|
1694
1746
|
closed = true;
|
|
1747
|
+
modelTraces.closeAll();
|
|
1695
1748
|
for (const preparation of preparingTools)
|
|
1696
1749
|
preparation.controller.abort();
|
|
1697
1750
|
clearInterval(reaper);
|
|
@@ -1705,8 +1758,11 @@ export function createPanelRuntime(options) {
|
|
|
1705
1758
|
await taskCookies?.shutdown();
|
|
1706
1759
|
}
|
|
1707
1760
|
finally {
|
|
1708
|
-
|
|
1709
|
-
|
|
1761
|
+
await Promise.all([
|
|
1762
|
+
processes.close(),
|
|
1763
|
+
resources.shutdown(),
|
|
1764
|
+
Promise.resolve(agentTasks?.close()),
|
|
1765
|
+
]);
|
|
1710
1766
|
}
|
|
1711
1767
|
},
|
|
1712
1768
|
async handle(request, response) {
|
|
@@ -1936,7 +1992,7 @@ export function createPanelRuntime(options) {
|
|
|
1936
1992
|
// The embedding iframe must enable the same permission.
|
|
1937
1993
|
response.setHeader("Content-Security-Policy", "default-src 'none'; script-src " +
|
|
1938
1994
|
assetSource +
|
|
1939
|
-
"; style-src " +
|
|
1995
|
+
" 'wasm-unsafe-eval'; style-src " +
|
|
1940
1996
|
assetSource +
|
|
1941
1997
|
" 'unsafe-inline'; img-src " +
|
|
1942
1998
|
assetSource +
|
|
@@ -1944,7 +2000,9 @@ export function createPanelRuntime(options) {
|
|
|
1944
2000
|
assetSource +
|
|
1945
2001
|
" blob:; font-src " +
|
|
1946
2002
|
assetSource +
|
|
1947
|
-
" data:; connect-src
|
|
2003
|
+
" data:; connect-src " +
|
|
2004
|
+
assetSource +
|
|
2005
|
+
"; object-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'; sandbox allow-scripts allow-downloads; frame-ancestors " +
|
|
1948
2006
|
resourceOrigin);
|
|
1949
2007
|
response.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=(), display-capture=()");
|
|
1950
2008
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { chmod, lstat, mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { isAbsolute, join } from "node:path";
|
|
4
|
-
import { formatNetscapeCookies, parseCookieJar, } from "@cjhyy/code-shell-core";
|
|
4
|
+
import { formatNetscapeCookies, normalizeCookieDomain, cookieAppliesToHost, parseCookieJar, } from "@cjhyy/code-shell-core";
|
|
5
5
|
function targetUrl(raw) {
|
|
6
6
|
if (typeof raw !== "string" || raw.length > 2048)
|
|
7
7
|
throw new Error("Cookie selection requires an HTTPS URL");
|
|
@@ -16,16 +16,6 @@ function targetUrl(raw) {
|
|
|
16
16
|
throw new Error("Cookie selection requires an HTTPS URL");
|
|
17
17
|
return url;
|
|
18
18
|
}
|
|
19
|
-
function domain(raw) {
|
|
20
|
-
if (typeof raw !== "string")
|
|
21
|
-
return;
|
|
22
|
-
const value = raw.toLowerCase().replace(/^\./, "");
|
|
23
|
-
if (value.length > 253 ||
|
|
24
|
-
!value.includes(".") ||
|
|
25
|
-
!value.split(".").every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)))
|
|
26
|
-
return;
|
|
27
|
-
return value;
|
|
28
|
-
}
|
|
29
19
|
function beneath(host, root) {
|
|
30
20
|
return host === root || host.endsWith(`.${root}`);
|
|
31
21
|
}
|
|
@@ -86,7 +76,7 @@ export class PanelTaskCookieService {
|
|
|
86
76
|
]))
|
|
87
77
|
.digest("hex");
|
|
88
78
|
}
|
|
89
|
-
cookies(credential,
|
|
79
|
+
cookies(credential, host) {
|
|
90
80
|
const now = (this.options.now?.() ?? Date.now()) / 1000;
|
|
91
81
|
if (typeof credential.secret !== "string" ||
|
|
92
82
|
Buffer.byteLength(credential.secret) > 4 * 1024 * 1024)
|
|
@@ -106,8 +96,10 @@ export class PanelTaskCookieService {
|
|
|
106
96
|
!cookie.path.startsWith("/") ||
|
|
107
97
|
/[\x00-\x1f\x7f]/.test(cookie.path))))
|
|
108
98
|
return false;
|
|
109
|
-
|
|
110
|
-
|
|
99
|
+
// Account eligibility and actual Cookie scope are different fences:
|
|
100
|
+
// a login captured on www.example.com can carry .example.com cookies,
|
|
101
|
+
// but must not export host-only cookies from a sibling login host.
|
|
102
|
+
if (!cookieAppliesToHost(cookie, host))
|
|
111
103
|
return false;
|
|
112
104
|
if (cookie.expirationDate !== undefined &&
|
|
113
105
|
(typeof cookie.expirationDate !== "number" ||
|
|
@@ -120,10 +112,10 @@ export class PanelTaskCookieService {
|
|
|
120
112
|
async snapshot(scope, url) {
|
|
121
113
|
await this.options.authorize(scope);
|
|
122
114
|
const result = (await this.options.credentials(scope)).flatMap((credential) => {
|
|
123
|
-
const root =
|
|
115
|
+
const root = normalizeCookieDomain(credential.meta?.domain);
|
|
124
116
|
if (credential.type !== "cookie" || !root || !beneath(url.hostname.toLowerCase(), root))
|
|
125
117
|
return [];
|
|
126
|
-
const cookies = this.cookies(credential,
|
|
118
|
+
const cookies = this.cookies(credential, url.hostname);
|
|
127
119
|
if (!cookies.length)
|
|
128
120
|
return [];
|
|
129
121
|
return [
|
|
@@ -120,6 +120,8 @@ export declare class PanelToolJobService {
|
|
|
120
120
|
initialize(): Promise<void>;
|
|
121
121
|
private load;
|
|
122
122
|
private commit;
|
|
123
|
+
/** Make a job state visible to readers and subscribers. */
|
|
124
|
+
private publish;
|
|
123
125
|
private lookup;
|
|
124
126
|
/** Scoped existence check for transports merging read-only legacy history. */
|
|
125
127
|
has(rawScope: ToolJobScope, id: string): Promise<boolean>;
|
package/dist/panels/tool-jobs.js
CHANGED
|
@@ -179,11 +179,16 @@ export class PanelToolJobService {
|
|
|
179
179
|
};
|
|
180
180
|
}
|
|
181
181
|
executionScopes() {
|
|
182
|
+
const held = (scope, label) => ({
|
|
183
|
+
appId: scope.appId,
|
|
184
|
+
projectPath: scope.projectPath,
|
|
185
|
+
holder: { kind: "tool-job", ...(label ? { label } : {}) },
|
|
186
|
+
});
|
|
182
187
|
return [
|
|
183
|
-
...[...this.preparing.values()].map((item) => item.scope),
|
|
188
|
+
...[...this.preparing.values()].map((item) => held(item.scope)),
|
|
184
189
|
...[...this.jobs.values()]
|
|
185
190
|
.filter((job) => !TERMINAL.has(job.status) || this.active.has(job.id))
|
|
186
|
-
.map((job) => job.scope),
|
|
191
|
+
.map((job) => held(job.scope, job.entry.name)),
|
|
187
192
|
];
|
|
188
193
|
}
|
|
189
194
|
activeCount(projectPath) {
|
|
@@ -283,6 +288,10 @@ export class PanelToolJobService {
|
|
|
283
288
|
job.updatedAt = this.now();
|
|
284
289
|
job.sequence++;
|
|
285
290
|
await this.storage.write(job.id, job, toolJobLimits.maxRecordBytes);
|
|
291
|
+
this.publish(job);
|
|
292
|
+
}
|
|
293
|
+
/** Make a job state visible to readers and subscribers. */
|
|
294
|
+
publish(job) {
|
|
286
295
|
this.jobs.set(job.id, job);
|
|
287
296
|
try {
|
|
288
297
|
this.options.onEvent?.(publicJob(job));
|
|
@@ -670,7 +679,23 @@ export class PanelToolJobService {
|
|
|
670
679
|
next.result = result;
|
|
671
680
|
next.progress = { ...next.progress, fraction: 1 };
|
|
672
681
|
}
|
|
673
|
-
|
|
682
|
+
try {
|
|
683
|
+
await this.commit(next);
|
|
684
|
+
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
// The executor has exited; a job that stays "running" in memory would
|
|
687
|
+
// count as active forever and hide the outcome. Publish an explicit
|
|
688
|
+
// failure for this Host session instead (keeping any result), while
|
|
689
|
+
// the stale disk record is recovered as interrupted by a later Host.
|
|
690
|
+
const outcome = next.error ? `${next.status} (${next.error.message})` : next.status;
|
|
691
|
+
next.status = "failed";
|
|
692
|
+
next.error = {
|
|
693
|
+
code: "TOOL_JOB_STORAGE_FAILED",
|
|
694
|
+
message: `Task ${outcome} but its final state could not be saved: ${error instanceof Error ? error.message : String(error)}`.slice(0, 2000),
|
|
695
|
+
retryable: next.recovery === "retry",
|
|
696
|
+
};
|
|
697
|
+
this.publish(next);
|
|
698
|
+
}
|
|
674
699
|
});
|
|
675
700
|
}
|
|
676
701
|
async cancel(rawScope, id) {
|
package/dist/serve/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { join, resolve } from "node:path";
|
|
|
10
10
|
import { existsSync } from "node:fs";
|
|
11
11
|
import { startHeadlessServer } from "./headless-server.js";
|
|
12
12
|
import { startProjectControlServer } from "../project-runtime/control-server.js";
|
|
13
|
+
import { configureModelTracing } from "@cjhyy/code-shell-core/internal";
|
|
13
14
|
const SERVE_HELP = `Usage: code-shell-serve [options]
|
|
14
15
|
|
|
15
16
|
Run the CodeShell headless Web host for a workspace.
|
|
@@ -127,7 +128,7 @@ export function resolveWorkerEntry() {
|
|
|
127
128
|
* wrapper registered it; arena/pet stay desktop-only).
|
|
128
129
|
*/
|
|
129
130
|
export function resolveWorkerCapabilityModules() {
|
|
130
|
-
return `${import.meta.resolve("@cjhyy/code-shell-capability-coding")}#createCodingModule`;
|
|
131
|
+
return `${import.meta.resolve("@cjhyy/code-shell-capability-coding")}#createCodingModule,${import.meta.resolve("@cjhyy/code-shell-server/links")}#createLinkProviderModule`;
|
|
131
132
|
}
|
|
132
133
|
/** Locate the built browser app (packages/web `app` build), when present. */
|
|
133
134
|
export function resolveWebAppRoot() {
|
|
@@ -150,6 +151,12 @@ export async function runServeCli(argv = process.argv.slice(2)) {
|
|
|
150
151
|
return;
|
|
151
152
|
}
|
|
152
153
|
const parsed = parseServeArgs(argv);
|
|
154
|
+
if (parsed.runtime === "local") {
|
|
155
|
+
configureModelTracing({
|
|
156
|
+
storageDir: join(parsed.dataDir, "worker", "sessions", ".model-traces"),
|
|
157
|
+
source: "server",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
153
160
|
const staticRootDir = parsed.staticRootDir ?? resolveWebAppRoot();
|
|
154
161
|
const server = parsed.runtime === "docker"
|
|
155
162
|
? await startProjectControlServer({
|
|
@@ -43,6 +43,7 @@ import { createHubFiles } from "../hub/files.js";
|
|
|
43
43
|
import { createSessionHttpResponses } from "../session-http-responses.js";
|
|
44
44
|
import { WorkerBridgeCore, previewLine } from "../worker-bridge-core.js";
|
|
45
45
|
import { HostRuntime, interceptHostRequest } from "@cjhyy/code-shell-host";
|
|
46
|
+
import { createModelTraceHttp } from "../model-trace-http.js";
|
|
46
47
|
export async function startHeadlessServer(opts) {
|
|
47
48
|
const remoteLink = remoteLinkHostConfiguration(opts.remoteLink, opts.publicOrigin);
|
|
48
49
|
if (remoteLink && opts.authMode !== "hub")
|
|
@@ -131,7 +132,33 @@ export async function startHeadlessServer(opts) {
|
|
|
131
132
|
let configurationChanging = false;
|
|
132
133
|
let configurationReloadFailed = false;
|
|
133
134
|
const runUploads = new Map();
|
|
134
|
-
|
|
135
|
+
// Core also starts top-level runs that never pass through a host agent/run,
|
|
136
|
+
// e.g. the follow-up turn of an asynchronously answered question. Count them
|
|
137
|
+
// from their own top-level stream boundaries so archive/export/automation
|
|
138
|
+
// gates see the session as busy until the run's terminal turn_complete.
|
|
139
|
+
const streamRuns = new Map();
|
|
140
|
+
const isRunning = (sessionId) => [...runningSessions.values()].includes(sessionId) ||
|
|
141
|
+
(streamRuns.get(sessionId) ?? 0) > 0 ||
|
|
142
|
+
!!automationWorker?.ownsSession(sessionId);
|
|
143
|
+
const observeRunBoundary = (sessionId, event) => {
|
|
144
|
+
if (!event || event.agentId !== undefined)
|
|
145
|
+
return;
|
|
146
|
+
const before = isRunning(sessionId);
|
|
147
|
+
const count = streamRuns.get(sessionId) ?? 0;
|
|
148
|
+
if (event.type === "session_started")
|
|
149
|
+
streamRuns.set(sessionId, count + 1);
|
|
150
|
+
else if (event.type === "turn_complete" && count > 0) {
|
|
151
|
+
if (count === 1)
|
|
152
|
+
streamRuns.delete(sessionId);
|
|
153
|
+
else
|
|
154
|
+
streamRuns.set(sessionId, count - 1);
|
|
155
|
+
}
|
|
156
|
+
else
|
|
157
|
+
return;
|
|
158
|
+
const running = isRunning(sessionId);
|
|
159
|
+
if (running !== before)
|
|
160
|
+
notify("serve/sessionStatus", { sessionId, running });
|
|
161
|
+
};
|
|
135
162
|
let nextTabId = 1;
|
|
136
163
|
let nextWorkerRequestId = 1;
|
|
137
164
|
const outbound = new HubOutboundTransport({
|
|
@@ -207,6 +234,7 @@ export async function startHeadlessServer(opts) {
|
|
|
207
234
|
pendingWorkerResponses.clear();
|
|
208
235
|
pendingResponsesByTab.clear();
|
|
209
236
|
runningSessions.clear();
|
|
237
|
+
streamRuns.clear();
|
|
210
238
|
runOwners.clear();
|
|
211
239
|
runReplay.clear();
|
|
212
240
|
runInputs.clear();
|
|
@@ -269,6 +297,7 @@ export async function startHeadlessServer(opts) {
|
|
|
269
297
|
let event = params.event;
|
|
270
298
|
let deliveredInput;
|
|
271
299
|
const value = event;
|
|
300
|
+
observeRunBoundary(params.sessionId, value);
|
|
272
301
|
if (value?.type === "session_user_message" && typeof value.clientMessageId === "string") {
|
|
273
302
|
const input = [...runInputs.values()].find((candidate) => candidate.sessionId === params.sessionId &&
|
|
274
303
|
candidate.event.clientMessageId === value.clientMessageId);
|
|
@@ -421,9 +450,8 @@ export async function startHeadlessServer(opts) {
|
|
|
421
450
|
CODE_SHELL_WORKSPACE_ACCESS: undefined,
|
|
422
451
|
CODE_SHELL_DATA_ROOT: workerDataRoot,
|
|
423
452
|
CODE_SHELL_CREDENTIAL_ACCESS: "local",
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
: {}),
|
|
453
|
+
CODE_SHELL_CAPABILITY_MODULES: opts.workerCapabilityModules ??
|
|
454
|
+
`${import.meta.resolve("@cjhyy/code-shell-server/links")}#createLinkProviderModule`,
|
|
427
455
|
}),
|
|
428
456
|
log,
|
|
429
457
|
onStderr: (text) => log("worker.stderr", { text: previewLine(text) }),
|
|
@@ -486,6 +514,8 @@ export async function startHeadlessServer(opts) {
|
|
|
486
514
|
};
|
|
487
515
|
const configurationOptions = {
|
|
488
516
|
cwd: workspaceCwd,
|
|
517
|
+
traceStorageDir: join(sessionRootDir, ".model-traces"),
|
|
518
|
+
traceSource: "server",
|
|
489
519
|
isAuthorized: async (req) => !!(await hubAuth?.authenticate(req)),
|
|
490
520
|
ownerId: async (req) => (await hubAuth?.authenticate(req))?.id,
|
|
491
521
|
withMutation: withConfigurationMutation,
|
|
@@ -537,9 +567,8 @@ export async function startHeadlessServer(opts) {
|
|
|
537
567
|
CODE_SHELL_WORKSPACE_ACCESS: undefined,
|
|
538
568
|
CODE_SHELL_DATA_ROOT: workerDataRoot,
|
|
539
569
|
CODE_SHELL_CREDENTIAL_ACCESS: "local",
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
: {}),
|
|
570
|
+
CODE_SHELL_CAPABILITY_MODULES: opts.workerCapabilityModules ??
|
|
571
|
+
`${import.meta.resolve("@cjhyy/code-shell-server/links")}#createLinkProviderModule`,
|
|
543
572
|
}),
|
|
544
573
|
},
|
|
545
574
|
onChanged: () => notify("serve/configurationChanged", {}),
|
|
@@ -575,6 +604,13 @@ export async function startHeadlessServer(opts) {
|
|
|
575
604
|
const files = hubAuth
|
|
576
605
|
? createHubFiles({ cwd: workspaceCwd, isAuthorized: configurationOptions.isAuthorized })
|
|
577
606
|
: undefined;
|
|
607
|
+
const modelTraces = hubAuth
|
|
608
|
+
? createModelTraceHttp({
|
|
609
|
+
storageDir: join(sessionRootDir, ".model-traces"),
|
|
610
|
+
workspacePath: workspaceCwd,
|
|
611
|
+
isAuthorized: configurationOptions.isAuthorized,
|
|
612
|
+
})
|
|
613
|
+
: undefined;
|
|
578
614
|
const serveStatic = (req, res) => {
|
|
579
615
|
const root = opts.staticRootDir;
|
|
580
616
|
if (!root) {
|
|
@@ -681,11 +717,19 @@ export async function startHeadlessServer(opts) {
|
|
|
681
717
|
return;
|
|
682
718
|
if (await files.handle(req, res))
|
|
683
719
|
return;
|
|
720
|
+
if (await modelTraces.handle(req, res))
|
|
721
|
+
return;
|
|
684
722
|
const upload = /^\/api\/v1\/uploads\/([^/]+)$/.exec(pathname);
|
|
685
723
|
if (upload && req.method === "PUT") {
|
|
686
724
|
await uploads.accept(upload[1], session.id, req, res, async () => !!(await hubAuth.authenticate(req)));
|
|
687
725
|
return;
|
|
688
726
|
}
|
|
727
|
+
if (upload && req.method === "DELETE") {
|
|
728
|
+
// Removing a draft attachment releases its quota; idempotent for retries.
|
|
729
|
+
await uploads.discard(upload[1], session.id);
|
|
730
|
+
res.writeHead(204, { "cache-control": "no-store" }).end();
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
689
733
|
hubJson(res, 404, { error: "API endpoint not found" });
|
|
690
734
|
return;
|
|
691
735
|
}
|
|
@@ -828,7 +872,16 @@ export async function startHeadlessServer(opts) {
|
|
|
828
872
|
sendToTab(ws, hostQueryError(parsed.id ?? null, -32000, "too many pending agent worker requests for this tab"), "pending-limit");
|
|
829
873
|
return;
|
|
830
874
|
}
|
|
875
|
+
// Retryable admission gates run before an approval lease is submitted:
|
|
876
|
+
// a submitted lease is excluded from snapshots until the worker ACKs.
|
|
877
|
+
if (hubAuth && (configurationChanging || configurationReloadFailed)) {
|
|
878
|
+
sendToTab(ws, hostQueryError(parsed.id ?? null, -32009, configurationReloadFailed
|
|
879
|
+
? "请先重新保存配置或重启服务,再发送任务。"
|
|
880
|
+
: "配置正在保存,请稍后重新发送任务。"), "configuration-changing");
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
831
883
|
let approvalId;
|
|
884
|
+
let approvalRouted = false;
|
|
832
885
|
if (hubAuth && parsed.method === "agent/approve") {
|
|
833
886
|
const requestId = parsed.params?.requestId;
|
|
834
887
|
const approval = typeof requestId === "string" ? leases.get(requestId) : undefined;
|
|
@@ -865,12 +918,6 @@ export async function startHeadlessServer(opts) {
|
|
|
865
918
|
let preparedUploads;
|
|
866
919
|
let uploadsTransferred = false;
|
|
867
920
|
const preparingRun = !!hubAuth && parsed.method === "agent/run";
|
|
868
|
-
if (hubAuth && (configurationChanging || configurationReloadFailed)) {
|
|
869
|
-
sendToTab(ws, hostQueryError(parsed.id ?? null, -32009, configurationReloadFailed
|
|
870
|
-
? "请先重新保存配置或重启服务,再发送任务。"
|
|
871
|
-
: "配置正在保存,请稍后重新发送任务。"), "configuration-changing");
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
921
|
if (preparingRun)
|
|
875
922
|
preparingRuns++;
|
|
876
923
|
try {
|
|
@@ -973,6 +1020,8 @@ export async function startHeadlessServer(opts) {
|
|
|
973
1020
|
method: parsed.method,
|
|
974
1021
|
...(approvalId ? { approvalId } : {}),
|
|
975
1022
|
});
|
|
1023
|
+
// The worker response route now owns the submitted lease.
|
|
1024
|
+
approvalRouted = true;
|
|
976
1025
|
pendingResponsesByTab.set(tabId, (pendingResponsesByTab.get(tabId) ?? 0) + 1);
|
|
977
1026
|
workerMessage.id = workerRequestId;
|
|
978
1027
|
if (preparedUploads?.attachments.length) {
|
|
@@ -1023,6 +1072,11 @@ export async function startHeadlessServer(opts) {
|
|
|
1023
1072
|
preparingRuns--;
|
|
1024
1073
|
if (!uploadsTransferred)
|
|
1025
1074
|
preparedUploads?.release();
|
|
1075
|
+
if (approvalId && !approvalRouted) {
|
|
1076
|
+
// Rejected before dispatch: the worker never saw this decision.
|
|
1077
|
+
leases.retry(approvalId);
|
|
1078
|
+
notify("serve/approvalSnapshot", { approvals: leases.snapshot() });
|
|
1079
|
+
}
|
|
1026
1080
|
}
|
|
1027
1081
|
};
|
|
1028
1082
|
let queued = 0;
|
|
@@ -1305,7 +1359,7 @@ function replyToHostSessionQuery(message, sessionManager, workspaceCwd, isRunnin
|
|
|
1305
1359
|
data: {
|
|
1306
1360
|
state,
|
|
1307
1361
|
...(legacy ? { outputJournal: journal, legacyBaseComplete: legacy.complete } : {}),
|
|
1308
|
-
...(runReplay ? runReplay.snapshot(sessionId, transcript) : { transcript }),
|
|
1362
|
+
...(runReplay ? runReplay.snapshot(sessionId, transcript, !!legacy) : { transcript }),
|
|
1309
1363
|
running: isRunning(sessionId),
|
|
1310
1364
|
},
|
|
1311
1365
|
},
|
package/dist/sessions-service.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Uses core's canonical session root so CODE_SHELL_HOME matches the worker.
|
|
4
4
|
*/
|
|
5
5
|
import { SessionManager, sessionsRoot } from "@cjhyy/code-shell-core";
|
|
6
|
+
import { scanSessionDirectories, readSessionListState, SessionFileError, } from "@cjhyy/code-shell-host/session-files";
|
|
6
7
|
import * as fs from "node:fs/promises";
|
|
7
8
|
import * as path from "node:path";
|
|
8
9
|
const SAFE_ID = /^[A-Za-z0-9_.-]+$/;
|
|
@@ -175,6 +176,9 @@ export async function cleanupStaleQuickChatSessions(baseDir = sessionsRoot()) {
|
|
|
175
176
|
}
|
|
176
177
|
return [...ids, ...stagingDirs];
|
|
177
178
|
}
|
|
179
|
+
function compareDiskSessionPosition(a, b) {
|
|
180
|
+
return b.mtime - a.mtime || a.id.localeCompare(b.id);
|
|
181
|
+
}
|
|
178
182
|
function encodeDiskSessionCursor(cursor) {
|
|
179
183
|
return `v1:${cursor.mtime}:${cursor.id}`;
|
|
180
184
|
}
|
|
@@ -210,37 +214,18 @@ export async function listDiskSessions(opts, baseDir = sessionsRoot()) {
|
|
|
210
214
|
parentSessionId.includes(".."))) {
|
|
211
215
|
throw new Error("invalid parent session id");
|
|
212
216
|
}
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
entries = await fs.readdir(baseDir, { withFileTypes: true });
|
|
219
|
-
}
|
|
220
|
-
catch (e) {
|
|
221
|
-
if (e.code === "ENOENT")
|
|
222
|
-
return { sessions: [], nextCursor: null };
|
|
223
|
-
throw e;
|
|
224
|
-
}
|
|
225
|
-
const candidates = entries.filter((e) => e.isDirectory() && SAFE_ID.test(e.name) && !isInternalEphemeralSessionId(e.name));
|
|
226
|
-
const stats = await Promise.all(candidates.map(async (e) => {
|
|
227
|
-
try {
|
|
228
|
-
return { id: e.name, mtime: (await fs.stat(path.join(baseDir, e.name))).mtimeMs };
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
return null; // skip unreadable
|
|
232
|
-
}
|
|
233
|
-
}));
|
|
234
|
-
const dirs = stats.filter((d) => d !== null);
|
|
235
|
-
dirs.sort((a, b) => b.mtime - a.mtime || a.id.localeCompare(b.id));
|
|
217
|
+
// Preserve the public adapter's explicit relative-root form. Host receives
|
|
218
|
+
// one absolute root; it never guesses a workspace for the caller.
|
|
219
|
+
baseDir = path.resolve(baseDir);
|
|
220
|
+
const dirs = (await scanSessionDirectories(baseDir)).filter(({ id }) => !isInternalEphemeralSessionId(id));
|
|
221
|
+
dirs.sort(compareDiskSessionPosition);
|
|
236
222
|
const decodedCursor = decodeDiskSessionCursor(opts.cursor);
|
|
237
223
|
if (decodedCursor === null)
|
|
238
224
|
return { sessions: [], nextCursor: null };
|
|
239
225
|
const start = typeof decodedCursor === "number"
|
|
240
226
|
? Math.max(0, decodedCursor)
|
|
241
227
|
: (() => {
|
|
242
|
-
const index = dirs.findIndex((entry) => entry
|
|
243
|
-
(entry.mtime === decodedCursor.mtime && entry.id > decodedCursor.id));
|
|
228
|
+
const index = dirs.findIndex((entry) => compareDiskSessionPosition(entry, decodedCursor) > 0);
|
|
244
229
|
return index < 0 ? dirs.length : index;
|
|
245
230
|
})();
|
|
246
231
|
const sessions = [];
|
|
@@ -264,9 +249,14 @@ export async function listDiskSessions(opts, baseDir = sessionsRoot()) {
|
|
|
264
249
|
const { id, mtime } = dirs[i];
|
|
265
250
|
let state;
|
|
266
251
|
try {
|
|
267
|
-
state = JSON.parse(await
|
|
252
|
+
state = JSON.parse(await readSessionListState(baseDir, id, MAX_HISTORY_STATE_BYTES));
|
|
253
|
+
if (!state || typeof state !== "object" || Array.isArray(state))
|
|
254
|
+
continue;
|
|
268
255
|
}
|
|
269
|
-
catch {
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (error instanceof SessionFileError && error.code === "too_large") {
|
|
258
|
+
process.emitWarning(`Session ${id}: state.json exceeds the 2 MiB catalog metadata limit; entry omitted.`, { code: "SESSION_LIST_STATE_TOO_LARGE" });
|
|
259
|
+
}
|
|
270
260
|
continue;
|
|
271
261
|
}
|
|
272
262
|
if (state.ephemeral === true)
|