@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
@@ -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
- const isRunning = (sessionId) => [...runningSessions.values()].includes(sessionId) || !!automationWorker?.ownsSession(sessionId);
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
- ...(opts.workerCapabilityModules
425
- ? { CODE_SHELL_CAPABILITY_MODULES: opts.workerCapabilityModules }
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
- ...(opts.workerCapabilityModules
541
- ? { CODE_SHELL_CAPABILITY_MODULES: opts.workerCapabilityModules }
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
  },
@@ -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
- // Async fs throughout: this is an IPC handler on the Electron main thread
214
- // (sessions:listDisk). The previous synchronous statSync/readFileSync loops
215
- // never yielded the event loop and froze the UI while enumerating sessions.
216
- let entries;
217
- try {
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.mtime < decodedCursor.mtime ||
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 fs.readFile(path.join(baseDir, id, "state.json"), "utf8"));
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)
@@ -141,6 +141,17 @@ export class WorkerBridgeCore {
141
141
  this.settlePendingOnExit();
142
142
  this.opts.onSpawnError?.(err);
143
143
  });
144
+ // A write that overlaps worker exit fails asynchronously (EPIPE) on the
145
+ // stdin stream, after sendLine() already returned. Unlistened, that stream
146
+ // error is an uncaught exception that terminates the whole Host. The pipe
147
+ // is unusable either way: retire this exact child so its exit path
148
+ // settles pending requests like any other crash.
149
+ child.stdin.on("error", (err) => {
150
+ this.log("child.stdin.error", { error: String(err), pid: child.pid });
151
+ if (this.child === child && child.exitCode === null && child.signalCode === null) {
152
+ child.kill("SIGTERM");
153
+ }
154
+ });
144
155
  child.on("exit", (code, signal) => {
145
156
  // Close the readline interface bound to the dead child's stdout so it
146
157
  // (and its "line" listener) doesn't leak across restarts.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjhyy/code-shell-server",
3
- "version": "0.9.43",
3
+ "version": "0.9.46",
4
4
  "description": "CodeShell server transport layer — mobile remote HTTP/WS host, pairing, passcode gate, tunnel, rooms, uploads, and session/attachment services. Electron-free; precursor of the auth gateway.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -50,7 +50,8 @@
50
50
  "bin": {
51
51
  "code-shell-serve": "./dist/bin/code-shell-serve.js",
52
52
  "code-shell-settings-recovery": "./dist/bin/code-shell-settings-recovery.js",
53
- "code-shell-cloud-account": "./dist/bin/code-shell-cloud-account.js"
53
+ "code-shell-cloud-account": "./dist/bin/code-shell-cloud-account.js",
54
+ "code-shell-link-cleanup": "./dist/bin/code-shell-link-cleanup.js"
54
55
  },
55
56
  "files": [
56
57
  "dist"
@@ -61,14 +62,14 @@
61
62
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
62
63
  },
63
64
  "dependencies": {
64
- "@cjhyy/code-shell-capability-coding": "0.9.43",
65
- "@cjhyy/code-shell-core": "0.9.43",
66
- "@cjhyy/code-shell-web": "0.9.43",
65
+ "@cjhyy/code-shell-capability-coding": "0.9.46",
66
+ "@cjhyy/code-shell-core": "0.9.46",
67
+ "@cjhyy/code-shell-web": "0.9.46",
67
68
  "@modelcontextprotocol/sdk": "^1.31.0",
68
69
  "@types/ws": "^8.18.1",
69
70
  "ws": "^8.21.0",
70
- "@cjhyy/code-shell-link": "0.9.43",
71
- "@cjhyy/code-shell-host": "0.9.43"
71
+ "@cjhyy/code-shell-link": "0.9.46",
72
+ "@cjhyy/code-shell-host": "0.9.46"
72
73
  },
73
74
  "engines": {
74
75
  "node": ">=20.10"