@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
@@ -1,4 +1,5 @@
1
- import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpathSync, } from "node:fs";
1
+ import { SessionFileError, readSessionFileSync, sessionListStorage, } from "@cjhyy/code-shell-host/session-files";
2
+ import { listSessionEntries } from "@cjhyy/code-shell-core/internal";
2
3
  import { join, resolve } from "node:path";
3
4
  import { SessionManager } from "@cjhyy/code-shell-core";
4
5
  import { replayTranscript, transcriptUserDisplay } from "@cjhyy/code-shell-web";
@@ -21,62 +22,25 @@ function safeSessionId(sessionId) {
21
22
  }
22
23
  function boundedSessionFile(rootDir, sessionId, filename, limit) {
23
24
  safeSessionId(sessionId);
24
- let directory;
25
- let descriptor;
26
25
  try {
27
- const root = realpathSync(rootDir);
28
- const path = join(root, sessionId);
29
- const info = lstatSync(path);
30
- if (!info.isDirectory() || info.isSymbolicLink())
31
- throw new SessionRequestError(403, "会话存储位置无效。");
32
- directory = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_DIRECTORY ?? 0));
33
- const openedDirectory = fstatSync(directory);
34
- if (openedDirectory.dev !== info.dev || openedDirectory.ino !== info.ino)
35
- throw new SessionRequestError(409, "会话存储正在变化,请重试。");
36
- descriptor = openSync(process.platform === "linux"
37
- ? `/proc/self/fd/${directory}/${filename}`
38
- : join(path, filename), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
39
- const file = fstatSync(descriptor);
40
- if (!file.isFile())
41
- throw new SessionRequestError(403, "会话记录不是普通文件。");
42
- if (file.size > limit)
43
- throw new SessionRequestError(413, filename === "transcript.jsonl"
44
- ? "对话记录超过浏览器可读取的 32 MB 上限,请从服务器会话备份中获取完整记录。"
45
- : "会话状态记录过大,无法在浏览器读取。");
46
- // Snapshot exactly the checked prefix. An append during a running turn can
47
- // be picked up by the live overlay without growing this allocation.
48
- const buffer = Buffer.alloc(file.size);
49
- let bytes = 0;
50
- while (bytes < buffer.length) {
51
- const count = readSync(descriptor, buffer, bytes, buffer.length - bytes, bytes);
52
- if (!count)
53
- break;
54
- bytes += count;
55
- }
56
- const currentDirectory = lstatSync(path);
57
- const currentFile = lstatSync(join(path, filename));
58
- if (currentDirectory.isSymbolicLink() ||
59
- currentFile.isSymbolicLink() ||
60
- currentDirectory.dev !== info.dev ||
61
- currentDirectory.ino !== info.ino ||
62
- currentFile.dev !== file.dev ||
63
- currentFile.ino !== file.ino)
64
- throw new SessionRequestError(409, "会话存储正在变化,请重试。");
65
- return buffer.subarray(0, bytes).toString("utf8");
26
+ return readSessionFileSync(resolve(rootDir), sessionId, filename, limit);
66
27
  }
67
28
  catch (error) {
29
+ if (error instanceof SessionFileError) {
30
+ if (error.code === "too_large")
31
+ throw new SessionRequestError(413, filename === "transcript.jsonl"
32
+ ? "对话记录超过浏览器可读取的 32 MB 上限,请从服务器会话备份中获取完整记录。"
33
+ : "会话状态记录过大,无法在浏览器读取。");
34
+ if (error.code === "changed")
35
+ throw new SessionRequestError(409, "会话存储正在变化,请重试。");
36
+ throw new SessionRequestError(403, "会话存储位置无效。");
37
+ }
68
38
  if (error.code === "ENOENT")
69
39
  throw new SessionRequestError(404, "会话记录不存在或已移动。");
70
40
  if (["ELOOP", "ENOTDIR"].includes(error.code ?? ""))
71
41
  throw new SessionRequestError(403, "不能读取符号链接中的会话记录。");
72
42
  throw error;
73
43
  }
74
- finally {
75
- if (descriptor !== undefined)
76
- closeSync(descriptor);
77
- if (directory !== undefined)
78
- closeSync(directory);
79
- }
80
44
  }
81
45
  /** Synchronous, side-effect-free transcript snapshot used by HTTP exports and WS replay. */
82
46
  export function readHubTranscript(sessionRootDir, sessionId) {
@@ -120,7 +84,8 @@ export function readHubSessionState(sessionRootDir, sessionId) {
120
84
  }
121
85
  }
122
86
  export function createHubSessions(options) {
123
- const manager = new SessionManager(options.sessionRootDir);
87
+ const sessionRootDir = resolve(options.sessionRootDir);
88
+ const manager = new SessionManager(sessionRootDir);
124
89
  const titles = createSessionTitlesStore(join(options.dataDir, "session-titles.json"), {
125
90
  strictMutations: true,
126
91
  });
@@ -130,7 +95,7 @@ export function createHubSessions(options) {
130
95
  safeSessionId(sessionId);
131
96
  try {
132
97
  // Reading state alone avoids loading a large transcript for a rename.
133
- const state = readHubSessionState(options.sessionRootDir, sessionId);
98
+ const state = readHubSessionState(sessionRootDir, sessionId);
134
99
  if (!state ||
135
100
  state.sessionId !== sessionId ||
136
101
  typeof state.cwd !== "string" ||
@@ -190,7 +155,7 @@ export function createHubSessions(options) {
190
155
  }
191
156
  const customTitles = await titles.listTitles();
192
157
  const search = query.toLocaleLowerCase();
193
- const entries = manager.list(limit + 1, {
158
+ const entries = listSessionEntries(sessionRootDir, limit + 1, {
194
159
  cwd,
195
160
  rootsOnly: true,
196
161
  ...(archived !== "all" ? { archived: archived === "true" } : {}),
@@ -200,7 +165,7 @@ export function createHubSessions(options) {
200
165
  .toLocaleLowerCase()
201
166
  .includes(search)
202
167
  : undefined,
203
- });
168
+ }, sessionListStorage(sessionRootDir));
204
169
  const page = entries.slice(0, limit);
205
170
  const last = page.at(-1);
206
171
  return {
@@ -249,7 +214,7 @@ export function createHubSessions(options) {
249
214
  throw new SessionRequestError(429, "正在导出的对话过多,请稍后重试。");
250
215
  activeExports++;
251
216
  exporting = true;
252
- const transcript = readHubTranscript(options.sessionRootDir, sessionId);
217
+ const transcript = readHubTranscript(sessionRootDir, sessionId);
253
218
  const format = url.searchParams.get("format") ?? "markdown";
254
219
  if (format !== "json" && format !== "markdown")
255
220
  throw new SessionRequestError(400, "不支持这个导出格式。");
@@ -1,8 +1,8 @@
1
1
  import { randomBytes, createHash } from "node:crypto";
2
2
  import { promises as fs } from "node:fs";
3
3
  import { basename, dirname, join } from "node:path";
4
- import { SettingsManager, scanSkills, invalidateSkillCache, userHome, } from "@cjhyy/code-shell-core";
5
- import { computeEffectiveDisabledLists } from "@cjhyy/code-shell-core/internal";
4
+ import { scanSkills, invalidateSkillCache, userHome } from "@cjhyy/code-shell-core";
5
+ import { readSkillInventory } from "@cjhyy/code-shell-host";
6
6
  import { assertSafeSkillName, assertOwnedSkillDirectory, readSkillBundle, readBoundedSkillFile, validateSkillMarkdown, skillRoot, stageSkillDirectory, commitSkillDirectory, editSkillMarkdown, removeOwnedSkill, SkillConflictError, inspectRepo, getRefCommit, downloadSkillTree, parseSkillSourceMeta, SKILL_META_FILE, } from "@cjhyy/code-shell-core/internal/skills";
7
7
  import { HubConfigurationError } from "./configuration.js";
8
8
  const ROOT = "/api/v1/skills";
@@ -59,11 +59,9 @@ export function createHubSkills(options) {
59
59
  return skill;
60
60
  }
61
61
  function snapshot() {
62
- invalidateSkillCache();
63
- const manager = new SettingsManager(options.cwd, "full");
64
- const disabled = computeEffectiveDisabledLists(manager, options.cwd);
65
- const enabled = new Set(scanSkills(options.cwd, disabled).map((entry) => entry.name));
66
- const skills = scanSkills(options.cwd).map((skill) => {
62
+ const inventory = readSkillInventory(options.cwd, { refresh: true });
63
+ const disabled = inventory;
64
+ const skills = inventory.skills.map(({ definition: skill, enabled }) => {
67
65
  let editable = false;
68
66
  let revision = createHash("sha256").update(skill.content).digest("hex");
69
67
  let readOnlyReason;
@@ -89,7 +87,7 @@ export function createHubSkills(options) {
89
87
  name: skill.name,
90
88
  description: skill.description,
91
89
  source: skill.source,
92
- enabled: enabled.has(skill.name),
90
+ enabled,
93
91
  editable,
94
92
  removable: editable,
95
93
  revision,
@@ -13,10 +13,20 @@ export declare class HubUploads {
13
13
  private readonly uploads;
14
14
  private readonly active;
15
15
  private closed;
16
+ /** This Host's own spool directory, held by a renewed lease while it runs. */
17
+ private readonly spool;
18
+ private releaseLease?;
16
19
  constructor(root: string, cwd: string);
17
20
  ready(): Promise<void>;
21
+ private reclaimAbandoned;
18
22
  accept(id: string, owner: string, req: IncomingMessage, res: ServerResponse, stillAuthorized: () => Promise<boolean>): Promise<void>;
19
23
  prepare(ids: unknown, owner: string, sessionId: unknown): Promise<PreparedHubUploads>;
24
+ /**
25
+ * Drop an owner's unsent upload, for example when it is removed from the
26
+ * draft. An upload being claimed by a send stays put. Returns whether the
27
+ * upload was discarded.
28
+ */
29
+ discard(id: string, owner: string): Promise<boolean>;
20
30
  sweep(): Promise<void>;
21
31
  close(): Promise<void>;
22
32
  }
@@ -2,9 +2,18 @@ import { randomUUID } from "node:crypto";
2
2
  import { constants } from "node:fs";
3
3
  import { chmod, lstat, mkdir, open, readdir, rm } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
- import { markAttachmentsSent, stageFileBytes, stageImageBytes, } from "../attachment-service.js";
5
+ import { lockSync } from "@cjhyy/code-shell-core/internal";
6
+ import { isStageableImageMime, markAttachmentsSent, MAX_STAGED_IMAGE_BYTES, stageFileBytes, stageImageBytes, } from "../attachment-service.js";
6
7
  const MAX_BYTES = 20 * 1024 * 1024;
7
8
  const TTL_MS = 15 * 60 * 1000;
9
+ /** Instance leases are renewed every 10s; a lease idle for 30s belongs to a dead Host. */
10
+ const LEASE_STALE_MS = 30_000;
11
+ const LEASE_UPDATE_MS = 10_000;
12
+ const UUID = "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}";
13
+ const LEGACY_SPOOL = new RegExp(`^${UUID}\\.upload$`);
14
+ const INSTANCE_ENTRY = new RegExp(`^(instance-${UUID})(?:\\.lock)?$`);
15
+ /** Image MIME types go through image staging; everything else is staged as a file. */
16
+ const routesToImageStaging = (mimeType) => mimeType.startsWith("image/") && mimeType !== "image/svg+xml";
8
17
  const SAFE_ID = /^[a-zA-Z0-9_-]{8,80}$/;
9
18
  // Match the session/history boundary so existing dotted session IDs can attach files.
10
19
  const SAFE_SESSION_ID = /^(?!.*\.\.)[A-Za-z0-9_-][A-Za-z0-9_.-]{0,127}$/;
@@ -30,9 +39,13 @@ export class HubUploads {
30
39
  uploads = new Map();
31
40
  active = new Set();
32
41
  closed = false;
42
+ /** This Host's own spool directory, held by a renewed lease while it runs. */
43
+ spool;
44
+ releaseLease;
33
45
  constructor(root, cwd) {
34
46
  this.root = root;
35
47
  this.cwd = cwd;
48
+ this.spool = join(root, `instance-${randomUUID()}`);
36
49
  }
37
50
  async ready() {
38
51
  await mkdir(this.root, { recursive: true, mode: 0o700 });
@@ -40,10 +53,55 @@ export class HubUploads {
40
53
  if (!info.isDirectory() || info.isSymbolicLink())
41
54
  throw new Error("unsafe upload directory");
42
55
  await chmod(this.root, 0o700);
43
- // A pending draft cannot survive a restart; staged attachments live in the workspace.
56
+ // A pending draft cannot survive a restart; staged attachments live in the
57
+ // workspace. Several Hosts (a duplicate launch that has not bound its port
58
+ // yet, or another port) can share this root, so reclaim only spools that
59
+ // are provably unused instead of everything present.
60
+ await this.reclaimAbandoned();
61
+ await mkdir(this.spool, { mode: 0o700 });
62
+ this.releaseLease = lockSync(this.spool, {
63
+ stale: LEASE_STALE_MS,
64
+ update: LEASE_UPDATE_MS,
65
+ realpath: false,
66
+ retries: 0,
67
+ // A lost lease only risks another Host reclaiming these drafts; sends then
68
+ // fail with "upload not found", which the client already handles.
69
+ onCompromised: () => { },
70
+ });
71
+ }
72
+ async reclaimAbandoned() {
73
+ const instances = new Set();
44
74
  for (const name of await readdir(this.root)) {
45
- if (/^[a-f0-9-]{36}\.upload$/.test(name))
46
- await rm(join(this.root, name), { force: true });
75
+ const instance = INSTANCE_ENTRY.exec(name)?.[1];
76
+ if (instance) {
77
+ instances.add(instance);
78
+ continue;
79
+ }
80
+ // Hosts before instance leases spooled into the root. An older live Host
81
+ // rejects anything past the TTL, so only expired files are safe to remove.
82
+ if (!LEGACY_SPOOL.test(name))
83
+ continue;
84
+ const path = join(this.root, name);
85
+ const entry = await lstat(path).catch(() => undefined);
86
+ if (entry?.isFile() && Date.now() - entry.mtimeMs >= TTL_MS)
87
+ await rm(path, { force: true });
88
+ }
89
+ for (const instance of instances) {
90
+ const path = join(this.root, instance);
91
+ let release;
92
+ try {
93
+ // Succeeds only when no live Host renews this instance's lease.
94
+ release = lockSync(path, { stale: LEASE_STALE_MS, realpath: false, retries: 0 });
95
+ }
96
+ catch {
97
+ continue;
98
+ }
99
+ try {
100
+ await rm(path, { recursive: true, force: true });
101
+ }
102
+ finally {
103
+ release();
104
+ }
47
105
  }
48
106
  }
49
107
  async accept(id, owner, req, res, stillAuthorized) {
@@ -70,6 +128,21 @@ export class HubUploads {
70
128
  return reject(400, "empty or invalid upload");
71
129
  if (size > MAX_BYTES)
72
130
  return reject(413, "attachment exceeds 20 MiB");
131
+ const mimeType = String(req.headers["content-type"] ?? "application/octet-stream")
132
+ .split(";", 1)[0]
133
+ .trim()
134
+ .toLowerCase();
135
+ if (mimeType.length > 128 || !/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/.test(mimeType)) {
136
+ return reject(400, "invalid content type");
137
+ }
138
+ // Admit only what the send path can stage, so a draft never looks
139
+ // uploaded and then fails on every Send.
140
+ if (routesToImageStaging(mimeType)) {
141
+ if (!isStageableImageMime(mimeType))
142
+ return reject(415, "unsupported image type");
143
+ if (size > MAX_STAGED_IMAGE_BYTES)
144
+ return reject(413, "image exceeds 10 MiB");
145
+ }
73
146
  if ([...this.uploads.values()].reduce((sum, entry) => sum + entry.size, size) >
74
147
  128 * 1024 * 1024) {
75
148
  return reject(429, "upload storage quota exceeded");
@@ -84,20 +157,13 @@ export class HubUploads {
84
157
  if (!name.trim() || name.length > 255 || /[\x00-\x1f/\\]/.test(name)) {
85
158
  return reject(400, "invalid file name");
86
159
  }
87
- const mimeType = String(req.headers["content-type"] ?? "application/octet-stream")
88
- .split(";", 1)[0]
89
- .trim()
90
- .toLowerCase();
91
- if (mimeType.length > 128 || !/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/.test(mimeType)) {
92
- return reject(400, "invalid content type");
93
- }
94
160
  const upload = {
95
161
  id,
96
162
  owner,
97
163
  name,
98
164
  mimeType,
99
165
  size,
100
- path: join(this.root, `${randomUUID()}.upload`),
166
+ path: join(this.spool, `${randomUUID()}.upload`),
101
167
  createdAt: Date.now(),
102
168
  status: "receiving",
103
169
  };
@@ -108,8 +174,11 @@ export class HubUploads {
108
174
  deadline.unref?.();
109
175
  try {
110
176
  // Recheck the managed root: a workspace tool must not redirect the upload writer via symlink.
111
- if (!(await lstat(this.root)).isDirectory() || (await lstat(this.root)).isSymbolicLink()) {
112
- throw new Error("unsafe upload directory");
177
+ for (const directory of [this.root, this.spool]) {
178
+ const current = await lstat(directory);
179
+ if (!current.isDirectory() || current.isSymbolicLink()) {
180
+ throw new Error("unsafe upload directory");
181
+ }
113
182
  }
114
183
  handle = await open(upload.path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
115
184
  let received = 0;
@@ -188,7 +257,7 @@ export class HubUploads {
188
257
  bytes,
189
258
  origin: "mobile",
190
259
  };
191
- result.push(record.mimeType.startsWith("image/") && record.mimeType !== "image/svg+xml"
260
+ result.push(routesToImageStaging(record.mimeType)
192
261
  ? await stageImageBytes(input)
193
262
  : await stageFileBytes(input));
194
263
  }
@@ -224,6 +293,19 @@ export class HubUploads {
224
293
  throw error;
225
294
  }
226
295
  }
296
+ /**
297
+ * Drop an owner's unsent upload, for example when it is removed from the
298
+ * draft. An upload being claimed by a send stays put. Returns whether the
299
+ * upload was discarded.
300
+ */
301
+ async discard(id, owner) {
302
+ const record = this.uploads.get(id);
303
+ if (!record || record.owner !== owner || record.status !== "ready")
304
+ return false;
305
+ this.uploads.delete(id);
306
+ await rm(record.path, { force: true });
307
+ return true;
308
+ }
227
309
  async sweep() {
228
310
  for (const [id, record] of this.uploads) {
229
311
  if (record.status === "ready" && Date.now() - record.createdAt >= TTL_MS) {
@@ -239,5 +321,15 @@ export class HubUploads {
239
321
  for (const record of this.uploads.values())
240
322
  await rm(record.path, { force: true });
241
323
  this.uploads.clear();
324
+ if (this.releaseLease) {
325
+ await rm(this.spool, { recursive: true, force: true });
326
+ try {
327
+ this.releaseLease();
328
+ }
329
+ catch {
330
+ /* A compromised lease has nothing left to release. */
331
+ }
332
+ this.releaseLease = undefined;
333
+ }
242
334
  }
243
335
  }
@@ -1,2 +1,3 @@
1
1
  /** Shared Web business HTTP API hosted by Desktop's existing remote listener. */
2
2
  export * from "./desktop-web/http-api.js";
3
+ export * from "./model-trace-http.js";
@@ -1,2 +1,3 @@
1
1
  /** Shared Web business HTTP API hosted by Desktop's existing remote listener. */
2
2
  export * from "./desktop-web/http-api.js";
3
+ export * from "./model-trace-http.js";
@@ -4,4 +4,7 @@ export * from "./links/device-oauth.js";
4
4
  export * from "./links/service.js";
5
5
  export * from "./links/types.js";
6
6
  export * from "./links/http.js";
7
+ export type { LinkAuthorizationDiagnostic, LinkAuthorizationDiagnosticStage, LinkAuthorizationDiagnosticCode, LinkAuthorizationDiagnosticSink, } from "./links/diagnostics.js";
7
8
  export * from "./links/remote-configuration.js";
9
+ export { readRemoteProviderCatalog, readRemoteProviderCapabilities, RemoteLinkCatalogError, type RemoteLinkCatalogFailureReason, type RemoteProviderCapabilities, } from "./links/remote-catalog.js";
10
+ export { createDefaultLinkProviders, installDefaultLinkProviders, createLinkProviderModule, } from "./links/providers/index.js";
@@ -5,3 +5,5 @@ export * from "./links/service.js";
5
5
  export * from "./links/types.js";
6
6
  export * from "./links/http.js";
7
7
  export * from "./links/remote-configuration.js";
8
+ export { readRemoteProviderCatalog, readRemoteProviderCapabilities, RemoteLinkCatalogError, } from "./links/remote-catalog.js";
9
+ export { createDefaultLinkProviders, installDefaultLinkProviders, createLinkProviderModule, } from "./links/providers/index.js";
@@ -25,3 +25,4 @@ export * from "./panels/task-cookies.js";
25
25
  export * from "./panels/task-cookie-host.js";
26
26
  export * from "./panels/execution-gate.js";
27
27
  export * from "./panels/package-mutation.js";
28
+ export * from "./panels/model-traces.js";
@@ -24,3 +24,4 @@ export * from "./panels/task-cookies.js";
24
24
  export * from "./panels/task-cookie-host.js";
25
25
  export * from "./panels/execution-gate.js";
26
26
  export * from "./panels/package-mutation.js";
27
+ export * from "./panels/model-traces.js";
@@ -1,5 +1,6 @@
1
1
  /** Ready-made headless HTTP/WebSocket host and CLI composition helpers. */
2
2
  export * from "./serve/headless-server.js";
3
+ export * from "./model-trace-http.js";
3
4
  export { startProjectControlServer, type ProjectControlServerOptions, } from "./project-runtime/control-server.js";
4
5
  export { CLOUD_MANAGEMENT_AUTHORITY_VERSION } from "./project-runtime/cloud-account-auth.js";
5
6
  export { CLOUD_BROWSER_AUTHORITY_VERSION } from "./project-runtime/cloud-browser-auth.js";
@@ -1,5 +1,6 @@
1
1
  /** Ready-made headless HTTP/WebSocket host and CLI composition helpers. */
2
2
  export * from "./serve/headless-server.js";
3
+ export * from "./model-trace-http.js";
3
4
  export { startProjectControlServer, } from "./project-runtime/control-server.js";
4
5
  export { CLOUD_MANAGEMENT_AUTHORITY_VERSION } from "./project-runtime/cloud-account-auth.js";
5
6
  export { CLOUD_BROWSER_AUTHORITY_VERSION } from "./project-runtime/cloud-browser-auth.js";
@@ -9,4 +9,5 @@ export * from "./client-message-id.js";
9
9
  export * from "./image-byte-probe.js";
10
10
  export * from "./sessions-service.js";
11
11
  export * from "./session-titles-store.js";
12
+ export * from "./model-trace-history.js";
12
13
  export * from "./panels/storage-store.js";
@@ -9,4 +9,5 @@ export * from "./client-message-id.js";
9
9
  export * from "./image-byte-probe.js";
10
10
  export * from "./sessions-service.js";
11
11
  export * from "./session-titles-store.js";
12
+ export * from "./model-trace-history.js";
12
13
  export * from "./panels/storage-store.js";
@@ -1,3 +1,4 @@
1
+ import { installDefaultLinkProviders } from "./providers/index.js";
1
2
  import { LINK_PROVIDER_MANIFESTS } from "@cjhyy/code-shell-link";
2
3
  import { listLocalLinkProviders } from "@cjhyy/code-shell-core";
3
4
  function unique(values) {
@@ -37,5 +38,6 @@ export function composeLinkProviderCatalog(manifests, executors) {
37
38
  });
38
39
  }
39
40
  export function listDesktopLinkProviders() {
41
+ installDefaultLinkProviders();
40
42
  return composeLinkProviderCatalog(LINK_PROVIDER_MANIFESTS, listLocalLinkProviders());
41
43
  }
@@ -0,0 +1,60 @@
1
+ import type { LinkAuthorization, LinkErrorCode } from "./types.js";
2
+ export type LinkAuthorizationDiagnosticStage = "authorization" | "body_validation" | "input_validation" | "host_store_read" | "remote_configuration" | "remote_catalog" | "mode_selection" | "connection_review" | "job_creation" | "final_authorization";
3
+ /** Fixed internal reasons, independent of exception text and provider responses. */
4
+ export type LinkAuthorizationDiagnosticCode = "authorization_check_failed" | "request_body_invalid" | "input_validation_failed" | "host_store_unreadable" | "host_store_read_failed" | "remote_configuration_missing" | "remote_configuration_invalid" | "catalog_network" | "catalog_timeout" | "catalog_http_status" | "catalog_invalid_json" | "catalog_invalid_schema" | "catalog_unreviewed_capability" | "catalog_unsupported_auth" | "catalog_response_limit" | "catalog_unexpected_error" | "catalog_unavailable" | "provider_capability_missing" | "auth_mode_unavailable" | "connection_review_failed" | "job_capacity_exceeded" | "remote_authorization_invalid" | "authorization_job_failed" | "final_authorization_failed";
5
+ /** Deliberately excludes all URLs, request data, account IDs, secrets, and exception text. */
6
+ export interface LinkAuthorizationDiagnostic {
7
+ event: "link_authorization";
8
+ transport: "http" | "host";
9
+ requestId: string;
10
+ outcome: "stage" | "success" | "failure";
11
+ stage: LinkAuthorizationDiagnosticStage;
12
+ elapsedMs: number;
13
+ providerId?: string;
14
+ methodId?: string;
15
+ authModeId?: string;
16
+ internalCode?: LinkAuthorizationDiagnosticCode;
17
+ publicCode?: LinkErrorCode;
18
+ httpStatus?: number;
19
+ upstreamStatus?: number;
20
+ /** API success can return an authorization job that has already failed. */
21
+ authorizationState?: LinkAuthorization["state"];
22
+ }
23
+ export type LinkAuthorizationDiagnosticSink = (diagnostic: Readonly<LinkAuthorizationDiagnostic>) => void | Promise<void>;
24
+ interface Failure {
25
+ stage: LinkAuthorizationDiagnosticStage;
26
+ internalCode: LinkAuthorizationDiagnosticCode;
27
+ upstreamStatus?: number;
28
+ }
29
+ declare class AuthorizationTrace {
30
+ private readonly sink?;
31
+ private readonly transport;
32
+ readonly requestId: `${string}-${string}-${string}-${string}-${string}`;
33
+ private readonly startedAt;
34
+ private active;
35
+ private stage;
36
+ private failure?;
37
+ private identity;
38
+ constructor(sink?: LinkAuthorizationDiagnosticSink | undefined, transport?: "http" | "host");
39
+ run<T>(operation: () => Promise<T>): Promise<T>;
40
+ isActive(): boolean;
41
+ setStage(stage: LinkAuthorizationDiagnosticStage): void;
42
+ setIdentity(identity: typeof this.identity): void;
43
+ setFailure(failure: Failure): void;
44
+ complete(httpStatus: number, publicCode?: LinkErrorCode, authorizationState?: LinkAuthorization["state"]): void;
45
+ private httpStatus;
46
+ /** Detached authorization jobs inherit ALS; they must lose authority to edit this trace. */
47
+ deactivate(): void;
48
+ private emit;
49
+ }
50
+ export declare function createLinkAuthorizationTrace(sink?: LinkAuthorizationDiagnosticSink): AuthorizationTrace;
51
+ /** Native Host callers share the same diagnostics; HTTP already owns its longer request lifetime. */
52
+ export declare function withLinkAuthorizationTrace(sink: LinkAuthorizationDiagnosticSink | undefined, operation: () => Promise<LinkAuthorization>, failure: (error: unknown) => {
53
+ status: number;
54
+ code: LinkErrorCode;
55
+ }): Promise<LinkAuthorization>;
56
+ export declare function authorizationDiagnosticStage(stage: LinkAuthorizationDiagnosticStage): void;
57
+ /** Callers must select identity values from the compiled provider/method/mode catalog. */
58
+ export declare function authorizationDiagnosticIdentity(identity: Pick<LinkAuthorizationDiagnostic, "providerId" | "methodId" | "authModeId">): void;
59
+ export declare function authorizationDiagnosticFailure(stage: LinkAuthorizationDiagnosticStage, internalCode: LinkAuthorizationDiagnosticCode, upstreamStatus?: number): void;
60
+ export {};