@cjhyy/code-shell-server 0.9.44 → 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,14 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { ModelTraceListQuery } from "@cjhyy/code-shell-core/internal";
3
+ import { type ModelTraceReader } from "./model-trace-history.js";
4
+ /** Authenticated read-only facade over one Host-owned store and one fixed workspace. */
5
+ export declare function createModelTraceHttp(options: {
6
+ storageDir: string;
7
+ workspacePath: string;
8
+ isAuthorized: (request: IncomingMessage) => boolean | Promise<boolean>;
9
+ /** Recomputed for each authenticated request and immediately before responding. */
10
+ resolveWorkspaceScopes?: (request: IncomingMessage) => ModelTraceListQuery["workspaceScopes"] | Promise<ModelTraceListQuery["workspaceScopes"]>;
11
+ store?: ModelTraceReader;
12
+ }): {
13
+ handle(request: IncomingMessage, response: ServerResponse): Promise<boolean>;
14
+ };
@@ -0,0 +1,105 @@
1
+ import { resolve } from "node:path";
2
+ import { createModelTraceHistory, ModelTraceRequestError, } from "./model-trace-history.js";
3
+ const ROOT = "/api/v1/model-traces";
4
+ function json(response, status, body) {
5
+ if (response.destroyed || response.writableEnded)
6
+ return;
7
+ response.writeHead(status, {
8
+ "content-type": "application/json; charset=utf-8",
9
+ "cache-control": "no-store",
10
+ "x-content-type-options": "nosniff",
11
+ });
12
+ response.end(JSON.stringify(body));
13
+ }
14
+ function queryFromUrl(url) {
15
+ const query = {};
16
+ for (const key of new Set(url.searchParams.keys())) {
17
+ if (key === "workspace")
18
+ continue; // Resolved and authorized by the outer Host route.
19
+ const values = url.searchParams.getAll(key);
20
+ if (values.length !== 1)
21
+ throw new ModelTraceRequestError(400, "Duplicate model trace query field");
22
+ const value = values[0];
23
+ if (key === "withoutSession") {
24
+ if (value !== "true" && value !== "false")
25
+ throw new ModelTraceRequestError(400, "Invalid model trace withoutSession");
26
+ query[key] = value === "true";
27
+ }
28
+ else if (["limit", "since", "until"].includes(key)) {
29
+ if (!value.trim())
30
+ throw new ModelTraceRequestError(400, "Invalid numeric model trace query");
31
+ query[key] = Number(value);
32
+ }
33
+ else
34
+ query[key] = value;
35
+ }
36
+ return query;
37
+ }
38
+ /** Authenticated read-only facade over one Host-owned store and one fixed workspace. */
39
+ export function createModelTraceHttp(options) {
40
+ const resolveScopes = async (request) => {
41
+ const scopes = await options.resolveWorkspaceScopes?.(request);
42
+ return scopes?.map((scope) => ({ ...scope, workspacePath: resolve(scope.workspacePath) }));
43
+ };
44
+ const fingerprint = (scopes) => JSON.stringify(scopes?.map((scope) => [scope.workspacePath, scope.sourcePrefix ?? null]).sort());
45
+ return {
46
+ async handle(request, response) {
47
+ const url = new URL(request.url ?? "/", "http://localhost");
48
+ if (url.pathname !== ROOT && !url.pathname.startsWith(`${ROOT}/`))
49
+ return false;
50
+ try {
51
+ if (!(await options.isAuthorized(request)))
52
+ throw new ModelTraceRequestError(401, "Model history authorization expired");
53
+ if (request.method !== "GET") {
54
+ response.setHeader("allow", "GET");
55
+ throw new ModelTraceRequestError(405, "Model history is read-only on remote clients");
56
+ }
57
+ const workspaceScopes = await resolveScopes(request);
58
+ const history = createModelTraceHistory({ ...options, workspaceScopes });
59
+ let result;
60
+ if (url.pathname === ROOT)
61
+ result = history.list(queryFromUrl(url));
62
+ else if (url.pathname === `${ROOT}/sessions`)
63
+ result = history.sessions(queryFromUrl(url));
64
+ else if (url.pathname === `${ROOT}/config`) {
65
+ if (Object.keys(queryFromUrl(url)).length)
66
+ throw new ModelTraceRequestError(400, "Configuration queries are not supported");
67
+ result = history.getConfig();
68
+ }
69
+ else {
70
+ const match = new RegExp(`^${ROOT}/([^/]+)$`).exec(url.pathname);
71
+ if (!match)
72
+ throw new ModelTraceRequestError(404, "Model record not found");
73
+ if (Object.keys(queryFromUrl(url)).length)
74
+ throw new ModelTraceRequestError(400, "Record queries are not supported");
75
+ let id;
76
+ try {
77
+ id = decodeURIComponent(match[1]);
78
+ }
79
+ catch {
80
+ throw new ModelTraceRequestError(400, "Invalid model trace id");
81
+ }
82
+ result = history.get(id);
83
+ if (!result)
84
+ throw new ModelTraceRequestError(404, "Model record not found");
85
+ }
86
+ // Authentication may have been revoked during asynchronous Host validation.
87
+ if (!(await options.isAuthorized(request)))
88
+ throw new ModelTraceRequestError(401, "Model history authorization expired");
89
+ if (fingerprint(await resolveScopes(request)) !== fingerprint(workspaceScopes))
90
+ throw new ModelTraceRequestError(401, "Model history workspace authorization changed");
91
+ if (!(await options.isAuthorized(request)))
92
+ throw new ModelTraceRequestError(401, "Model history authorization expired");
93
+ json(response, 200, result);
94
+ }
95
+ catch (error) {
96
+ json(response, error instanceof ModelTraceRequestError ? error.status : 500, {
97
+ error: error instanceof ModelTraceRequestError
98
+ ? error.message
99
+ : "Model history is unavailable",
100
+ });
101
+ }
102
+ return true;
103
+ },
104
+ };
105
+ }
@@ -341,6 +341,7 @@ export function createPanelAgentTaskHost(options = {}) {
341
341
  cwd: scope.cwd,
342
342
  bucket: owner.bucket,
343
343
  task: input.prompt,
344
+ modelTraceSource: `panel:${scope.appId}`,
344
345
  displayText: input.label,
345
346
  clientMessageId: `panel-task:${scope.appId}:${input.sessionId}`,
346
347
  behaviorMode: "isolatedTask",
@@ -12,14 +12,14 @@ export function createPanelDirectoryProjectScope(workspacePath, expectedProjectP
12
12
  const binding = createHubPanelBinding(workspacePath);
13
13
  binding.assertBinding();
14
14
  const projectPath = realpathSync(expectedProjectPath);
15
- const identity = lstatSync(projectPath);
15
+ const identity = lstatSync(projectPath, { bigint: true });
16
16
  if (!identity.isDirectory() ||
17
17
  identity.isSymbolicLink() ||
18
18
  realpathSync(binding.bindingCwd) !== projectPath)
19
19
  throw new Error("Panel directory project scope is unavailable");
20
20
  const assertCurrent = () => {
21
21
  binding.assertBinding();
22
- const current = lstatSync(projectPath);
22
+ const current = lstatSync(projectPath, { bigint: true });
23
23
  if (!current.isDirectory() ||
24
24
  current.isSymbolicLink() ||
25
25
  current.dev !== identity.dev ||
@@ -65,11 +65,14 @@ export function hubPanelDirectoryBookmarks(dataDir) {
65
65
  const MAX_RECORDS = 256;
66
66
  const MAX_BYTES = 256 * 1024;
67
67
  const ID = /^[a-f0-9-]{36}$/i;
68
- function valid(record) {
68
+ function parseBookmark(record, version) {
69
+ const invalid = () => {
70
+ throw new Error("Invalid Panel directory bookmarks");
71
+ };
69
72
  if (!record || typeof record !== "object" || Array.isArray(record))
70
- return false;
73
+ return invalid();
71
74
  const item = record;
72
- return (typeof item.id === "string" &&
75
+ if (!(typeof item.id === "string" &&
73
76
  ID.test(item.id) &&
74
77
  typeof item.appId === "string" &&
75
78
  /^[a-z][a-z0-9-]{0,63}$/.test(item.appId) &&
@@ -79,9 +82,36 @@ function valid(record) {
79
82
  isAbsolute(item.path) &&
80
83
  item.path.length <= 32_768 &&
81
84
  item.projectPath.length <= 32_768 &&
82
- Number.isSafeInteger(item.dev) &&
83
- Number.isSafeInteger(item.ino) &&
84
- Number.isSafeInteger(item.savedAt));
85
+ Number.isSafeInteger(item.savedAt)))
86
+ return invalid();
87
+ let dev, ino;
88
+ if (version === 1) {
89
+ const legacy = record;
90
+ if (typeof legacy.dev !== "number" ||
91
+ !Number.isInteger(legacy.dev) ||
92
+ legacy.dev < 0 ||
93
+ typeof legacy.ino !== "number" ||
94
+ !Number.isInteger(legacy.ino) ||
95
+ legacy.ino < 0)
96
+ return invalid();
97
+ // Never recover an authorization by rounding a file ID. Retain a tombstone
98
+ // so this UUID cannot fall back to a different grant in the legacy file.
99
+ if (!Number.isSafeInteger(legacy.dev) || !Number.isSafeInteger(legacy.ino)) {
100
+ dev = ino = null;
101
+ }
102
+ else {
103
+ dev = String(legacy.dev);
104
+ ino = String(legacy.ino);
105
+ }
106
+ }
107
+ else {
108
+ const exact = (value) => typeof value === "string" && /^(0|[1-9][0-9]{0,19})$/.test(value);
109
+ if (!(item.dev === null && item.ino === null) && !(exact(item.dev) && exact(item.ino)))
110
+ return invalid();
111
+ dev = item.dev;
112
+ ino = item.ino;
113
+ }
114
+ return { ...item, dev, ino };
85
115
  }
86
116
  function read(file) {
87
117
  let fd;
@@ -97,12 +127,11 @@ function read(file) {
97
127
  if (!document || typeof document !== "object" || Array.isArray(document))
98
128
  throw new Error("Invalid Panel directory bookmarks");
99
129
  const value = document;
100
- if (value.version !== 1 ||
130
+ if ((value.version !== 1 && value.version !== 2) ||
101
131
  !Array.isArray(value.bookmarks) ||
102
- value.bookmarks.length > MAX_RECORDS ||
103
- !value.bookmarks.every(valid))
132
+ value.bookmarks.length > MAX_RECORDS)
104
133
  throw new Error("Invalid Panel directory bookmarks");
105
- return value.bookmarks;
134
+ return value.bookmarks.map((record) => parseBookmark(record, value.version));
106
135
  }
107
136
  catch (error) {
108
137
  if (error.code === "ENOENT")
@@ -129,7 +158,7 @@ function write(file, bookmarks) {
129
158
  if (error.code !== "ENOENT")
130
159
  throw error;
131
160
  }
132
- const serialized = JSON.stringify({ version: 1, bookmarks });
161
+ const serialized = JSON.stringify({ version: 2, bookmarks });
133
162
  if (Buffer.byteLength(serialized) > MAX_BYTES)
134
163
  throw new Error("Panel directory bookmarks are full");
135
164
  const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
@@ -154,7 +183,7 @@ export class PanelAppDirectoryBookmarks {
154
183
  remember(appId, projectPath, path) {
155
184
  if (!/^[a-z][a-z0-9-]{0,63}$/.test(appId) || !isAbsolute(projectPath) || !isAbsolute(path))
156
185
  throw new Error("Invalid Panel directory bookmark scope");
157
- const info = lstatSync(path);
186
+ const info = lstatSync(path, { bigint: true });
158
187
  if (!info.isDirectory() || info.isSymbolicLink() || realpathSync(path) !== path)
159
188
  throw new Error("Selected directory changed; choose it again");
160
189
  const bookmark = {
@@ -162,8 +191,8 @@ export class PanelAppDirectoryBookmarks {
162
191
  appId,
163
192
  projectPath,
164
193
  path,
165
- dev: info.dev,
166
- ino: info.ino,
194
+ dev: info.dev.toString(),
195
+ ino: info.ino.toString(),
167
196
  savedAt: Date.now(),
168
197
  };
169
198
  mkdirSync(dirname(this.file), { recursive: true, mode: 0o700 });
@@ -173,18 +202,22 @@ export class PanelAppDirectoryBookmarks {
173
202
  const existing = previous.find((item) => item.appId === appId &&
174
203
  item.projectPath === projectPath &&
175
204
  item.path === path &&
176
- item.dev === info.dev &&
177
- item.ino === info.ino);
205
+ item.dev === bookmark.dev &&
206
+ item.ino === bookmark.ino);
178
207
  if (existing)
179
208
  bookmark.id = existing.id;
180
- write(this.file, [
181
- bookmark,
182
- ...previous.filter((item) => item.id !== bookmark.id &&
183
- !(item.appId === appId &&
184
- item.projectPath === projectPath &&
185
- item.path === path &&
186
- (item.dev !== info.dev || item.ino !== info.ino))),
187
- ].slice(0, MAX_RECORDS));
209
+ const retained = previous.filter((item) => item.id !== bookmark.id &&
210
+ !(item.dev !== null &&
211
+ item.appId === appId &&
212
+ item.projectPath === projectPath &&
213
+ item.path === path &&
214
+ (item.dev !== bookmark.dev || item.ino !== bookmark.ino)));
215
+ // Keep revocations even when the bounded store evicts older live grants:
216
+ // forgetting one could resurrect that UUID through a legacy-file fallback.
217
+ const revoked = retained.filter((item) => item.dev === null);
218
+ if (revoked.length >= MAX_RECORDS)
219
+ throw new Error("Panel directory bookmarks are full");
220
+ write(this.file, [bookmark, ...revoked, ...retained.filter((item) => item.dev !== null)].slice(0, MAX_RECORDS));
188
221
  }
189
222
  finally {
190
223
  release();
@@ -222,11 +255,11 @@ export class PanelAppDirectoryBookmarks {
222
255
  };
223
256
  assertProjectScope();
224
257
  try {
225
- const info = lstatSync(bookmark.path);
258
+ const info = lstatSync(bookmark.path, { bigint: true });
226
259
  if (!info.isDirectory() ||
227
260
  info.isSymbolicLink() ||
228
- info.dev !== bookmark.dev ||
229
- info.ino !== bookmark.ino ||
261
+ info.dev.toString() !== bookmark.dev ||
262
+ info.ino.toString() !== bookmark.ino ||
230
263
  realpathSync(bookmark.path) !== bookmark.path)
231
264
  throw new Error("Saved directory changed");
232
265
  }
@@ -7,6 +7,7 @@ import { type PanelAgentTaskHostOptions } from "./agent-task-host.js";
7
7
  import type { PanelProjectScopeBinding } from "./project-scope.js";
8
8
  export declare function createPanelHttp(options: PanelManagementHttpOptions & {
9
9
  dataDir: string;
10
+ traceStorageDir?: string;
10
11
  host: "hub" | "desktop";
11
12
  projectScope?: () => Promise<PanelProjectScopeBinding | undefined>;
12
13
  publicPathPrefix?: string;
@@ -9,7 +9,7 @@ function refused() {
9
9
  }
10
10
  function metadata(file) {
11
11
  try {
12
- return lstatSync(file);
12
+ return lstatSync(file, { bigint: true });
13
13
  }
14
14
  catch (error) {
15
15
  if (error.code === "ENOENT")
@@ -20,8 +20,8 @@ function metadata(file) {
20
20
  function readPointer(file) {
21
21
  const descriptor = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
22
22
  try {
23
- const info = fstatSync(descriptor);
24
- if (!info.isFile() || info.size > 8192)
23
+ const info = fstatSync(descriptor, { bigint: true });
24
+ if (!info.isFile() || info.size > 8192n)
25
25
  refused();
26
26
  const content = readFileSync(descriptor, "utf8");
27
27
  if (Buffer.byteLength(content) > 8192 || content.includes("\0"))
@@ -54,13 +54,13 @@ function sameIdentity(actual, expected) {
54
54
  }
55
55
  function directoryGuard(path) {
56
56
  const canonical = realpathSync(path);
57
- const info = lstatSync(canonical);
57
+ const info = lstatSync(canonical, { bigint: true });
58
58
  if (!info.isDirectory() || info.isSymbolicLink())
59
59
  refused();
60
60
  return () => {
61
61
  if (realpathSync(path) !== canonical)
62
62
  refused();
63
- const current = lstatSync(canonical);
63
+ const current = lstatSync(canonical, { bigint: true });
64
64
  if (!current.isDirectory() || current.isSymbolicLink() || !sameIdentity(current, info))
65
65
  refused();
66
66
  };
@@ -0,0 +1,21 @@
1
+ import { PanelModelTraceService, type PanelModelTraceOwner } from "./model-traces.js";
2
+ /** A dedicated reviewed-child descriptor; ordinary stdout/stderr never become prompts. */
3
+ export declare class PanelModelTraceChannel {
4
+ private readonly options;
5
+ private readonly decoder;
6
+ private readonly frames;
7
+ private buffer;
8
+ private readonly calls;
9
+ private tail;
10
+ private stopped;
11
+ private pending;
12
+ constructor(options: {
13
+ service: PanelModelTraceService;
14
+ owner: PanelModelTraceOwner;
15
+ isAuthorized(): Promise<boolean>;
16
+ });
17
+ write(chunk: Buffer | string): void;
18
+ finish(status: "cancelled" | "failed"): Promise<void>;
19
+ abort(status?: "cancelled" | "failed"): void;
20
+ private consume;
21
+ }
@@ -0,0 +1,110 @@
1
+ import { StringDecoder } from "node:string_decoder";
2
+ import { createNativeModelTraceFrameDecoder } from "@cjhyy/code-shell-core/internal";
3
+ import { panelModelTraceLimits, } from "./model-traces.js";
4
+ /** A dedicated reviewed-child descriptor; ordinary stdout/stderr never become prompts. */
5
+ export class PanelModelTraceChannel {
6
+ options;
7
+ decoder = new StringDecoder("utf8");
8
+ frames = createNativeModelTraceFrameDecoder();
9
+ buffer = "";
10
+ calls = new Map();
11
+ tail = Promise.resolve();
12
+ stopped = false;
13
+ pending = 0;
14
+ constructor(options) {
15
+ this.options = options;
16
+ }
17
+ write(chunk) {
18
+ if (this.stopped)
19
+ return;
20
+ this.buffer += typeof chunk === "string" ? chunk : this.decoder.write(chunk);
21
+ let newline;
22
+ while ((newline = this.buffer.indexOf("\n")) >= 0) {
23
+ const physical = this.buffer.slice(0, newline);
24
+ this.buffer = this.buffer.slice(newline + 1);
25
+ if (!physical.trim())
26
+ continue;
27
+ let line;
28
+ try {
29
+ if (Buffer.byteLength(physical) > panelModelTraceLimits.maxParamsBytes)
30
+ throw new Error("Oversized model observation");
31
+ line = this.frames.read(physical);
32
+ }
33
+ catch {
34
+ this.abort("failed");
35
+ return;
36
+ }
37
+ if (line === undefined || !line.trim())
38
+ continue;
39
+ if (++this.pending > 128) {
40
+ this.abort("failed");
41
+ return;
42
+ }
43
+ this.tail = this.tail.then(async () => {
44
+ try {
45
+ if (this.stopped || !(await this.options.isAuthorized())) {
46
+ this.abort();
47
+ return;
48
+ }
49
+ if (this.stopped)
50
+ return;
51
+ this.consume(line);
52
+ }
53
+ catch {
54
+ this.abort("failed");
55
+ }
56
+ finally {
57
+ this.pending--;
58
+ }
59
+ });
60
+ }
61
+ if (Buffer.byteLength(this.buffer) > panelModelTraceLimits.maxParamsBytes)
62
+ this.abort("failed");
63
+ }
64
+ async finish(status) {
65
+ this.buffer += this.decoder.end();
66
+ if (this.buffer.trim())
67
+ this.write("\n");
68
+ await this.tail;
69
+ this.abort(status);
70
+ }
71
+ abort(status = "cancelled") {
72
+ this.stopped = true;
73
+ this.buffer = "";
74
+ this.calls.clear();
75
+ this.frames.clear();
76
+ this.options.service.cancelOwner(this.options.owner.ownerKey, status);
77
+ }
78
+ consume(line) {
79
+ if (!line.trim())
80
+ return;
81
+ const value = JSON.parse(line);
82
+ if (!value ||
83
+ typeof value !== "object" ||
84
+ Array.isArray(value) ||
85
+ value.version !== 1 ||
86
+ value.type !== "model_trace" ||
87
+ !["start", "finish"].includes(value.phase) ||
88
+ typeof value.callId !== "string" ||
89
+ !/^[a-zA-Z0-9_-]{1,128}$/.test(value.callId))
90
+ throw new Error("Invalid model observation channel message");
91
+ const { version: _version, type: _type, phase, callId, ...params } = value;
92
+ if (Object.hasOwn(params, "id"))
93
+ throw new Error("Child cannot select a Host observation id");
94
+ if (phase === "start") {
95
+ if (this.calls.has(callId))
96
+ throw new Error("Duplicate model observation call");
97
+ const result = this.options.service.call(this.options.owner, "modelTraces.start", params);
98
+ if (!("id" in result))
99
+ throw new Error("Model observation was not started");
100
+ this.calls.set(callId, result.id);
101
+ }
102
+ else {
103
+ const id = this.calls.get(callId);
104
+ if (!id)
105
+ throw new Error("Unknown model observation call");
106
+ this.options.service.call(this.options.owner, "modelTraces.finish", { ...params, id });
107
+ this.calls.delete(callId);
108
+ }
109
+ }
110
+ }
@@ -0,0 +1,39 @@
1
+ export declare const panelModelTraceMethods: string[];
2
+ export declare const panelModelTraceLimits: Readonly<{
3
+ maxParamsBytes: number;
4
+ maxActivePerOwner: 64;
5
+ maxActive: 2048;
6
+ maxCallsPerMinute: 240;
7
+ maxLifetimeMs: number;
8
+ }>;
9
+ /** Supplied by a verified Host binding; no field is taken from an observation. */
10
+ export interface PanelModelTraceOwner {
11
+ ownerKey: string;
12
+ appId: string;
13
+ revision: string;
14
+ workspacePath: string;
15
+ sessionId?: string;
16
+ traceId?: string;
17
+ taskId?: string;
18
+ }
19
+ /** Write-only Panel observations share the Host's policy, sanitization and history store. */
20
+ export declare class PanelModelTraceService {
21
+ private readonly options;
22
+ private readonly active;
23
+ private readonly calls;
24
+ constructor(options?: {
25
+ storageDir?: string;
26
+ now?: () => number;
27
+ });
28
+ call(owner: PanelModelTraceOwner, method: string, params: unknown): {
29
+ id: string;
30
+ recorded: boolean;
31
+ } | {
32
+ recorded: boolean;
33
+ };
34
+ cancelOwner(ownerKey: string, status?: "cancelled" | "failed"): void;
35
+ closeAll(): void;
36
+ private close;
37
+ private now;
38
+ private prune;
39
+ }