@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.
Files changed (83) hide show
  1. package/dist/attachment-service.d.ts +2 -0
  2. package/dist/attachment-service.js +40 -27
  3. package/dist/bin/code-shell-link-cleanup.d.ts +2 -0
  4. package/dist/bin/code-shell-link-cleanup.js +3 -0
  5. package/dist/desktop-web/http-api.d.ts +3 -0
  6. package/dist/desktop-web/http-api.js +16 -1
  7. package/dist/hub/configuration.d.ts +6 -3
  8. package/dist/hub/configuration.js +19 -6
  9. package/dist/hub/mcp-probe.d.ts +4 -24
  10. package/dist/hub/mcp-probe.js +18 -389
  11. package/dist/hub/model-probe.d.ts +2 -20
  12. package/dist/hub/model-probe.js +2 -193
  13. package/dist/hub/run-replay.d.ts +7 -1
  14. package/dist/hub/run-replay.js +10 -2
  15. package/dist/hub/session-management.js +18 -53
  16. package/dist/hub/skills-management.js +6 -8
  17. package/dist/hub/uploads.d.ts +10 -0
  18. package/dist/hub/uploads.js +107 -15
  19. package/dist/index.desktop-web.d.ts +1 -0
  20. package/dist/index.desktop-web.js +1 -0
  21. package/dist/index.links.d.ts +3 -0
  22. package/dist/index.links.js +2 -0
  23. package/dist/index.panels.d.ts +1 -0
  24. package/dist/index.panels.js +1 -0
  25. package/dist/index.serve.d.ts +1 -0
  26. package/dist/index.serve.js +1 -0
  27. package/dist/index.storage.d.ts +1 -0
  28. package/dist/index.storage.js +1 -0
  29. package/dist/links/catalog.js +2 -0
  30. package/dist/links/diagnostics.d.ts +60 -0
  31. package/dist/links/diagnostics.js +184 -0
  32. package/dist/links/http.d.ts +10 -8
  33. package/dist/links/http.js +144 -125
  34. package/dist/links/legacy-cleanup-cli.d.ts +3 -0
  35. package/dist/links/legacy-cleanup-cli.js +64 -0
  36. package/dist/links/providers/defaults.d.ts +5 -0
  37. package/dist/links/providers/defaults.js +782 -0
  38. package/dist/links/providers/index.d.ts +7 -0
  39. package/dist/links/providers/index.js +22 -0
  40. package/dist/links/remote-catalog.d.ts +10 -3
  41. package/dist/links/remote-catalog.js +76 -29
  42. package/dist/links/runtime/operation.d.ts +9 -0
  43. package/dist/links/runtime/operation.js +16 -0
  44. package/dist/links/service.d.ts +18 -2
  45. package/dist/links/service.js +593 -62
  46. package/dist/links/types.d.ts +21 -1
  47. package/dist/mobile-remote/codex-parse.d.ts +3 -1
  48. package/dist/mobile-remote/codex-parse.js +45 -11
  49. package/dist/mobile-remote/mobile-chat-turn.d.ts +1 -0
  50. package/dist/mobile-remote/mobile-chat-turn.js +3 -0
  51. package/dist/mobile-remote/mobile-client-event-validator.js +13 -0
  52. package/dist/mobile-remote/mobile-history.d.ts +10 -1
  53. package/dist/mobile-remote/mobile-history.js +24 -16
  54. package/dist/mobile-remote/remote-host-manager.d.ts +8 -0
  55. package/dist/mobile-remote/remote-host-manager.js +23 -8
  56. package/dist/mobile-remote/resident-agent.js +17 -4
  57. package/dist/mobile-remote/room-manager.d.ts +23 -13
  58. package/dist/mobile-remote/room-manager.js +102 -20
  59. package/dist/mobile-remote/room-model-tracing.d.ts +18 -0
  60. package/dist/mobile-remote/room-model-tracing.js +196 -0
  61. package/dist/model-trace-history.d.ts +29 -0
  62. package/dist/model-trace-history.js +198 -0
  63. package/dist/model-trace-http.d.ts +14 -0
  64. package/dist/model-trace-http.js +105 -0
  65. package/dist/panels/agent-task-host.js +1 -0
  66. package/dist/panels/directory-bookmarks.js +62 -29
  67. package/dist/panels/http.d.ts +1 -0
  68. package/dist/panels/hub-binding.js +5 -5
  69. package/dist/panels/model-trace-channel.d.ts +21 -0
  70. package/dist/panels/model-trace-channel.js +110 -0
  71. package/dist/panels/model-traces.d.ts +39 -0
  72. package/dist/panels/model-traces.js +222 -0
  73. package/dist/panels/process-service.d.ts +12 -2
  74. package/dist/panels/process-service.js +49 -6
  75. package/dist/panels/runtime.d.ts +1 -0
  76. package/dist/panels/runtime.js +54 -6
  77. package/dist/panels/tool-jobs.d.ts +2 -0
  78. package/dist/panels/tool-jobs.js +21 -1
  79. package/dist/serve/cli.js +8 -1
  80. package/dist/serve/headless-server.js +68 -14
  81. package/dist/sessions-service.js +17 -27
  82. package/dist/worker-bridge-core.js +11 -0
  83. package/package.json +8 -7
@@ -0,0 +1,222 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { isAbsolute } from "node:path";
3
+ import { recordExternalModelTrace, } from "@cjhyy/code-shell-core/internal";
4
+ import { PanelBridgeError } from "./bridge-contract.js";
5
+ export const panelModelTraceMethods = ["modelTraces.start", "modelTraces.finish"];
6
+ export const panelModelTraceLimits = Object.freeze({
7
+ maxParamsBytes: 256 * 1024,
8
+ maxActivePerOwner: 64,
9
+ maxActive: 2048,
10
+ maxCallsPerMinute: 240,
11
+ maxLifetimeMs: 24 * 60 * 60 * 1000,
12
+ });
13
+ function invalid(message = "Invalid Panel model observation") {
14
+ throw new PanelBridgeError("INVALID_ARGUMENT", message);
15
+ }
16
+ function object(value, keys) {
17
+ if (!value || typeof value !== "object" || Array.isArray(value))
18
+ invalid();
19
+ if (Object.keys(value).some((key) => !keys.includes(key)))
20
+ invalid();
21
+ return value;
22
+ }
23
+ function label(value, optional = false) {
24
+ if (optional && value === undefined)
25
+ return undefined;
26
+ if (typeof value !== "string" || !value.trim() || value.length > 200 || /[\x00-\x1f]/.test(value))
27
+ invalid();
28
+ return value;
29
+ }
30
+ function usage(value) {
31
+ if (value === undefined)
32
+ return undefined;
33
+ const input = object(value, [
34
+ "promptTokens",
35
+ "completionTokens",
36
+ "totalTokens",
37
+ "cacheReadTokens",
38
+ "cacheCreationTokens",
39
+ ]);
40
+ for (const key of ["promptTokens", "completionTokens", "totalTokens"])
41
+ if (input[key] === undefined)
42
+ invalid("Model usage requires token counts");
43
+ for (const number of Object.values(input))
44
+ if (!Number.isSafeInteger(number) || Number(number) < 0)
45
+ invalid("Invalid model usage");
46
+ return input;
47
+ }
48
+ /** Write-only Panel observations share the Host's policy, sanitization and history store. */
49
+ export class PanelModelTraceService {
50
+ options;
51
+ active = new Map();
52
+ calls = new Map();
53
+ constructor(options = {}) {
54
+ this.options = options;
55
+ }
56
+ call(owner, method, params) {
57
+ if (!owner.ownerKey ||
58
+ !/^[a-z][a-z0-9-]{0,63}$/.test(owner.appId) ||
59
+ !owner.revision ||
60
+ !isAbsolute(owner.workspacePath))
61
+ invalid("Invalid Host model observation owner");
62
+ if (!panelModelTraceMethods.includes(method))
63
+ invalid("Unsupported model observation method");
64
+ const encoded = JSON.stringify(params);
65
+ if (!encoded || Buffer.byteLength(encoded) > panelModelTraceLimits.maxParamsBytes)
66
+ throw new PanelBridgeError("PARAMS_TOO_LARGE", "Panel model observation exceeds the limit");
67
+ const now = this.now();
68
+ this.prune(now);
69
+ const calls = (this.calls.get(owner.ownerKey) ?? []).filter((time) => now - time < 60_000);
70
+ if (calls.length >= panelModelTraceLimits.maxCallsPerMinute)
71
+ throw new PanelBridgeError("RATE_LIMITED", "Panel model observation rate limit exceeded");
72
+ calls.push(now);
73
+ this.calls.set(owner.ownerKey, calls);
74
+ if (method === "modelTraces.start") {
75
+ const value = object(params, [
76
+ "provider",
77
+ "model",
78
+ "purpose",
79
+ "input",
80
+ "inputState",
81
+ "stream",
82
+ ]);
83
+ const provider = label(value.provider);
84
+ const model = label(value.model);
85
+ const purpose = label(value.purpose, true) ?? "model";
86
+ if (value.stream !== undefined && typeof value.stream !== "boolean")
87
+ invalid();
88
+ if (value.inputState !== undefined && value.inputState !== "truncated")
89
+ invalid();
90
+ const body = object(value.input, ["systemPrompt", "messages", "tools", "parameters"]);
91
+ if (body.systemPrompt !== undefined && typeof body.systemPrompt !== "string")
92
+ invalid();
93
+ for (const key of ["messages", "tools"])
94
+ if (body[key] !== undefined && !Array.isArray(body[key]))
95
+ invalid();
96
+ if (body.parameters !== undefined &&
97
+ (!body.parameters || typeof body.parameters !== "object" || Array.isArray(body.parameters)))
98
+ invalid();
99
+ if (this.active.size >= panelModelTraceLimits.maxActive ||
100
+ [...this.active.values()].filter((item) => item.ownerKey === owner.ownerKey).length >=
101
+ panelModelTraceLimits.maxActivePerOwner)
102
+ throw new PanelBridgeError("RATE_LIMITED", "Too many active Panel model observations");
103
+ const input = {
104
+ storageDir: this.options.storageDir,
105
+ workspacePath: owner.workspacePath,
106
+ ephemeral: false,
107
+ sessionId: owner.sessionId,
108
+ parentSessionId: undefined,
109
+ runId: owner.traceId,
110
+ source: `panel:${owner.appId}`,
111
+ purpose: `panel/${purpose}`,
112
+ provider,
113
+ model,
114
+ startedAt: now,
115
+ status: "running",
116
+ stream: value.stream,
117
+ coverage: "logical-input",
118
+ inputState: value.inputState,
119
+ input: {
120
+ systemPrompt: body.systemPrompt ?? "",
121
+ messages: body.messages ?? [],
122
+ tools: body.tools ?? [],
123
+ parameters: {
124
+ ...body.parameters,
125
+ codeshellPanel: {
126
+ appId: owner.appId,
127
+ revision: owner.revision,
128
+ ...(owner.taskId ? { taskId: owner.taskId } : {}),
129
+ },
130
+ },
131
+ },
132
+ };
133
+ const record = recordExternalModelTrace(input);
134
+ const id = record?.id ?? randomUUID();
135
+ // Retain only already-sanitized content. Disabled recording never resumes mid-call.
136
+ this.active.set(id, {
137
+ ownerKey: owner.ownerKey,
138
+ ownerIdentity: JSON.stringify([
139
+ owner.appId,
140
+ owner.revision,
141
+ owner.workspacePath,
142
+ owner.sessionId,
143
+ ]),
144
+ recordId: record?.id,
145
+ startedAt: now,
146
+ input: { ...input, input: undefined },
147
+ });
148
+ return { id, recorded: !!record };
149
+ }
150
+ const value = object(params, ["id", "status", "output", "outputState", "error", "usage"]);
151
+ if (typeof value.id !== "string" || !/^[a-f0-9-]{36}$/i.test(value.id))
152
+ invalid();
153
+ const observation = this.active.get(value.id);
154
+ if (!observation ||
155
+ observation.ownerKey !== owner.ownerKey ||
156
+ observation.ownerIdentity !==
157
+ JSON.stringify([owner.appId, owner.revision, owner.workspacePath, owner.sessionId]))
158
+ throw new PanelBridgeError("REVOKED", "Model observation is unavailable for this Panel owner");
159
+ if (!["completed", "failed", "cancelled"].includes(String(value.status)))
160
+ invalid();
161
+ if (value.outputState !== undefined && value.outputState !== "truncated")
162
+ invalid();
163
+ const output = value.output === undefined
164
+ ? undefined
165
+ : object(value.output, ["text", "thinking", "toolCalls", "stopReason", "data"]);
166
+ for (const key of ["text", "thinking", "stopReason"])
167
+ if (output?.[key] !== undefined && typeof output[key] !== "string")
168
+ invalid();
169
+ if (output?.toolCalls !== undefined && !Array.isArray(output.toolCalls))
170
+ invalid();
171
+ if (value.error !== undefined && typeof value.error !== "string")
172
+ invalid();
173
+ const tokens = usage(value.usage);
174
+ this.active.delete(value.id);
175
+ const record = observation.recordId
176
+ ? recordExternalModelTrace({
177
+ ...observation.input,
178
+ id: observation.recordId,
179
+ endedAt: now,
180
+ status: value.status,
181
+ output,
182
+ outputState: value.outputState,
183
+ error: value.error,
184
+ usage: tokens,
185
+ })
186
+ : null;
187
+ return { recorded: !!record };
188
+ }
189
+ cancelOwner(ownerKey, status = "cancelled") {
190
+ for (const [id, observation] of this.active)
191
+ if (observation.ownerKey === ownerKey)
192
+ this.close(id, observation, status);
193
+ this.calls.delete(ownerKey);
194
+ }
195
+ closeAll() {
196
+ for (const [id, observation] of this.active)
197
+ this.close(id, observation, "cancelled");
198
+ this.calls.clear();
199
+ }
200
+ close(id, observation, status) {
201
+ this.active.delete(id);
202
+ if (observation.recordId)
203
+ recordExternalModelTrace({
204
+ ...observation.input,
205
+ id: observation.recordId,
206
+ endedAt: this.now(),
207
+ status,
208
+ error: status === "failed" ? "Panel process exited before the model call finished" : undefined,
209
+ });
210
+ }
211
+ now() {
212
+ return (this.options.now ?? Date.now)();
213
+ }
214
+ prune(now) {
215
+ for (const [id, observation] of this.active)
216
+ if (now - observation.startedAt > panelModelTraceLimits.maxLifetimeMs)
217
+ this.close(id, observation, "cancelled");
218
+ for (const [owner, calls] of this.calls)
219
+ if (calls.every((time) => now - time >= 60_000))
220
+ this.calls.delete(owner);
221
+ }
222
+ }
@@ -1,3 +1,4 @@
1
+ import type { PanelModelTraceService, PanelModelTraceOwner } from "./model-traces.js";
1
2
  export { processLimits } from "./process-state.js";
2
3
  export interface PanelProcessApprovalScope {
3
4
  appId: string;
@@ -13,6 +14,9 @@ export interface PanelProcessOwner {
13
14
  send(event: "process.output" | "process.exit", payload: Record<string, unknown>): void;
14
15
  }
15
16
  export interface PanelAppProcessServiceOptions {
17
+ /** Dedicated descriptor only for a verified installed package entry. */
18
+ modelTraces?: PanelModelTraceService;
19
+ modelTraceOwner?(owner: PanelProcessOwner): Omit<PanelModelTraceOwner, "ownerKey"> | undefined;
16
20
  /** Host lease from before async preparation until the actual process group has exited. */
17
21
  acquireExecution?(owner: PanelProcessOwner): () => void;
18
22
  env?: NodeJS.ProcessEnv;
@@ -159,7 +163,13 @@ export declare class PanelAppProcessService {
159
163
  }>;
160
164
  private sendInput;
161
165
  revokeGuest(guestId: number): void;
162
- /** Permanently shut down all guests, including operations awaiting approval. */
163
- close(): void;
166
+ /**
167
+ * Permanently shut down all guests, including operations awaiting approval.
168
+ * Resolves once every native process group has exited or been force-killed:
169
+ * escalation runs on a Host timer, so a Host that exits earlier would leave
170
+ * a process ignoring SIGTERM running.
171
+ */
172
+ close(): Promise<void>;
173
+ private shutDown;
164
174
  private terminate;
165
175
  }
@@ -4,6 +4,7 @@ import { constants, lstatSync, realpathSync } from "node:fs";
4
4
  import { access, realpath, stat } from "node:fs/promises";
5
5
  import { basename, delimiter, extname, join } from "node:path";
6
6
  import { killProcessGroup } from "@cjhyy/code-shell-core/extension";
7
+ import { PanelModelTraceChannel } from "./model-trace-channel.js";
7
8
  import { verifyProcessEntry } from "./process-entry.js";
8
9
  import { appendProcessEvent, ProcessReceipts, processLimits, sameProcessOwner, } from "./process-state.js";
9
10
  export { processLimits } from "./process-state.js";
@@ -231,7 +232,7 @@ export class PanelAppProcessService {
231
232
  options.assertCurrent?.();
232
233
  await this.authorize(owner, epoch);
233
234
  options.assertCurrent?.();
234
- const info = await stat(resolved);
235
+ const info = await stat(resolved, { bigint: true });
235
236
  options.assertCurrent?.();
236
237
  await this.authorize(owner, epoch);
237
238
  options.assertCurrent?.();
@@ -327,7 +328,7 @@ export class PanelAppProcessService {
327
328
  }
328
329
  // Final synchronous identity check preserves the existing handle API and avoids
329
330
  // an await between validating a cwd and spawning the child into that directory.
330
- const current = lstatSync(grant.path);
331
+ const current = lstatSync(grant.path, { bigint: true });
331
332
  if (!current.isDirectory() ||
332
333
  current.isSymbolicLink() ||
333
334
  current.dev !== grant.dev ||
@@ -518,12 +519,22 @@ export class PanelAppProcessService {
518
519
  if (!this.options.allowAuthorizedProcessWithoutPrompt)
519
520
  this.approvedExecutables.add(approvalKey);
520
521
  const processId = randomUUID();
522
+ const modelTraceOwner = entry && this.options.modelTraces ? this.options.modelTraceOwner?.(owner) : undefined;
523
+ // Never inherit a caller's descriptor claim for an ordinary executable.
524
+ delete env.CODESHELL_MODEL_TRACE_FD;
525
+ if (modelTraceOwner)
526
+ env.CODESHELL_MODEL_TRACE_FD = "3";
521
527
  const child = spawn(executable.path, args, {
522
528
  cwd,
523
529
  detached: process.platform !== "win32",
524
530
  env,
525
531
  shell: false,
526
- stdio: [input.stdin === "pipe" ? "pipe" : "ignore", "pipe", "pipe"],
532
+ stdio: [
533
+ input.stdin === "pipe" ? "pipe" : "ignore",
534
+ "pipe",
535
+ "pipe",
536
+ ...(modelTraceOwner ? ["pipe"] : []),
537
+ ],
527
538
  windowsHide: true,
528
539
  });
529
540
  const record = {
@@ -607,6 +618,28 @@ export class PanelAppProcessService {
607
618
  }),
608
619
  };
609
620
  this.processes.set(processId, running);
621
+ if (modelTraceOwner && this.options.modelTraces) {
622
+ const channel = new PanelModelTraceChannel({
623
+ service: this.options.modelTraces,
624
+ owner: {
625
+ ...modelTraceOwner,
626
+ ownerKey: `process:${processId}`,
627
+ traceId: modelTraceOwner.traceId ?? processId,
628
+ },
629
+ isAuthorized: async () => {
630
+ try {
631
+ await this.authorize(owner, epoch);
632
+ return this.processes.get(processId) === running && !running.termination;
633
+ }
634
+ catch {
635
+ return false;
636
+ }
637
+ },
638
+ });
639
+ running.modelTraceChannel = channel;
640
+ child.stdio[3]?.on("data", (chunk) => channel.write(chunk));
641
+ child.stdio[3]?.on("error", () => channel.abort("failed"));
642
+ }
610
643
  let checkingInputs = false;
611
644
  const inputChecks = fileArgumentHandles.some((handle) => this.fileArguments.get(handle)?.validate)
612
645
  ? setInterval(() => {
@@ -651,6 +684,7 @@ export class PanelAppProcessService {
651
684
  });
652
685
  }
653
686
  }
687
+ await running.modelTraceChannel?.finish(record.cancelRequested ? "cancelled" : "failed");
654
688
  finishExit({ code: finalCode, signal });
655
689
  this.retiring.delete(processId);
656
690
  if (this.processes.get(processId)?.child !== child)
@@ -879,10 +913,18 @@ export class PanelAppProcessService {
879
913
  }
880
914
  }
881
915
  }
882
- /** Permanently shut down all guests, including operations awaiting approval. */
916
+ /**
917
+ * Permanently shut down all guests, including operations awaiting approval.
918
+ * Resolves once every native process group has exited or been force-killed:
919
+ * escalation runs on a Host timer, so a Host that exits earlier would leave
920
+ * a process ignoring SIGTERM running.
921
+ */
883
922
  close() {
884
- if (this.closed)
885
- return;
923
+ if (!this.closed)
924
+ this.shutDown();
925
+ return Promise.all([...this.retiring.values()].map((running) => running.termination ?? Promise.resolve())).then(() => undefined);
926
+ }
927
+ shutDown() {
886
928
  this.closed = true;
887
929
  const guests = new Set([
888
930
  ...this.guestEpochs.keys(),
@@ -901,6 +943,7 @@ export class PanelAppProcessService {
901
943
  terminate(running) {
902
944
  if (running.termination)
903
945
  return;
946
+ running.modelTraceChannel?.abort();
904
947
  running.record.cancelRequested = true;
905
948
  running.record.status = "stopping";
906
949
  const pid = running.child.pid;
@@ -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>;
@@ -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,6 +529,27 @@ 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,
@@ -751,7 +779,7 @@ export function createPanelRuntime(options) {
751
779
  resources,
752
780
  owner: (job, send) => {
753
781
  const guestId = nextToolOwner--;
754
- toolOwners.set(guestId, job.scope);
782
+ toolOwners.set(guestId, { ...job.scope, taskId: job.id });
755
783
  return {
756
784
  guestId,
757
785
  appId: job.scope.appId,
@@ -826,6 +854,7 @@ export function createPanelRuntime(options) {
826
854
  assets.delete(grant.asset);
827
855
  byGuest.delete(grant.guestId);
828
856
  processes.revokeGuest(grant.guestId);
857
+ modelTraces.cancelOwner(`grant:${grant.id}`);
829
858
  agentTasks?.revokeInstance(grant.id);
830
859
  for (const item of grant.confirmations.values())
831
860
  item.finish(false);
@@ -1089,6 +1118,8 @@ export function createPanelRuntime(options) {
1089
1118
  locale: typeof input.locale === "string" ? input.locale.slice(0, 32) : "zh-CN",
1090
1119
  apiVersion: panelRuntimeApiVersion,
1091
1120
  capabilities: {
1121
+ packageAssets: { wasm: true, fetch: "package" },
1122
+ modelTraces: { writeOnly: true, coverage: "logical-input", ...panelModelTraceLimits },
1092
1123
  ...panelRuntimeCapabilities({
1093
1124
  process: app.permissions.includes("process"),
1094
1125
  cookieProcess: app.permissions.includes("process") &&
@@ -1189,7 +1220,9 @@ export function createPanelRuntime(options) {
1189
1220
  return false;
1190
1221
  if (method.startsWith("tools."))
1191
1222
  return !!app.agent?.tools.length;
1192
- return (method === "context.get" || app.permissions.includes(methodPermission(method)));
1223
+ return (method === "context.get" ||
1224
+ method.startsWith("modelTraces.") ||
1225
+ app.permissions.includes(methodPermission(method)));
1193
1226
  }),
1194
1227
  limitations: panel.compatibility.reasons,
1195
1228
  };
@@ -1302,6 +1335,14 @@ export function createPanelRuntime(options) {
1302
1335
  grant.calls = history;
1303
1336
  if (method === "context.get")
1304
1337
  return grant.context;
1338
+ if (method.startsWith("modelTraces."))
1339
+ return modelTraces.call({
1340
+ ownerKey: `grant:${grant.id}`,
1341
+ appId: grant.app.id,
1342
+ revision: grant.revision,
1343
+ workspacePath: options.cwd,
1344
+ sessionId: typeof grant.context.sessionId === "string" ? grant.context.sessionId : undefined,
1345
+ }, method, params);
1305
1346
  if (method.startsWith("tasks."))
1306
1347
  return dispatchToolJobs(grant, method, params);
1307
1348
  if (method.startsWith("tools.")) {
@@ -1600,6 +1641,7 @@ export function createPanelRuntime(options) {
1600
1641
  return {
1601
1642
  effect: method,
1602
1643
  prompt: value.prompt,
1644
+ modelTraceSource: `panel:${grant.app.id}`,
1603
1645
  sessionId: grant.context.sessionId,
1604
1646
  ...(value.displayText ? { displayText: value.displayText } : {}),
1605
1647
  };
@@ -1692,6 +1734,7 @@ export function createPanelRuntime(options) {
1692
1734
  },
1693
1735
  async close() {
1694
1736
  closed = true;
1737
+ modelTraces.closeAll();
1695
1738
  for (const preparation of preparingTools)
1696
1739
  preparation.controller.abort();
1697
1740
  clearInterval(reaper);
@@ -1705,8 +1748,11 @@ export function createPanelRuntime(options) {
1705
1748
  await taskCookies?.shutdown();
1706
1749
  }
1707
1750
  finally {
1708
- processes.close();
1709
- await Promise.all([resources.shutdown(), Promise.resolve(agentTasks?.close())]);
1751
+ await Promise.all([
1752
+ processes.close(),
1753
+ resources.shutdown(),
1754
+ Promise.resolve(agentTasks?.close()),
1755
+ ]);
1710
1756
  }
1711
1757
  },
1712
1758
  async handle(request, response) {
@@ -1936,7 +1982,7 @@ export function createPanelRuntime(options) {
1936
1982
  // The embedding iframe must enable the same permission.
1937
1983
  response.setHeader("Content-Security-Policy", "default-src 'none'; script-src " +
1938
1984
  assetSource +
1939
- "; style-src " +
1985
+ " 'wasm-unsafe-eval'; style-src " +
1940
1986
  assetSource +
1941
1987
  " 'unsafe-inline'; img-src " +
1942
1988
  assetSource +
@@ -1944,7 +1990,9 @@ export function createPanelRuntime(options) {
1944
1990
  assetSource +
1945
1991
  " blob:; font-src " +
1946
1992
  assetSource +
1947
- " data:; connect-src 'none'; object-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'; sandbox allow-scripts allow-downloads; frame-ancestors " +
1993
+ " data:; connect-src " +
1994
+ assetSource +
1995
+ "; object-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'; sandbox allow-scripts allow-downloads; frame-ancestors " +
1948
1996
  resourceOrigin);
1949
1997
  response.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=(), display-capture=()");
1950
1998
  }
@@ -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>;
@@ -283,6 +283,10 @@ export class PanelToolJobService {
283
283
  job.updatedAt = this.now();
284
284
  job.sequence++;
285
285
  await this.storage.write(job.id, job, toolJobLimits.maxRecordBytes);
286
+ this.publish(job);
287
+ }
288
+ /** Make a job state visible to readers and subscribers. */
289
+ publish(job) {
286
290
  this.jobs.set(job.id, job);
287
291
  try {
288
292
  this.options.onEvent?.(publicJob(job));
@@ -670,7 +674,23 @@ export class PanelToolJobService {
670
674
  next.result = result;
671
675
  next.progress = { ...next.progress, fraction: 1 };
672
676
  }
673
- await this.commit(next);
677
+ try {
678
+ await this.commit(next);
679
+ }
680
+ catch (error) {
681
+ // The executor has exited; a job that stays "running" in memory would
682
+ // count as active forever and hide the outcome. Publish an explicit
683
+ // failure for this Host session instead (keeping any result), while
684
+ // the stale disk record is recovered as interrupted by a later Host.
685
+ const outcome = next.error ? `${next.status} (${next.error.message})` : next.status;
686
+ next.status = "failed";
687
+ next.error = {
688
+ code: "TOOL_JOB_STORAGE_FAILED",
689
+ message: `Task ${outcome} but its final state could not be saved: ${error instanceof Error ? error.message : String(error)}`.slice(0, 2000),
690
+ retryable: next.recovery === "retry",
691
+ };
692
+ this.publish(next);
693
+ }
674
694
  });
675
695
  }
676
696
  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({