@parall/agent-core 1.31.0 → 1.32.1

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 (72) hide show
  1. package/dist/bridge-workspace.d.ts +1 -1
  2. package/dist/bridge-workspace.d.ts.map +1 -1
  3. package/dist/bridge-workspace.js +13 -13
  4. package/dist/dispatch-adapter.d.ts +15 -8
  5. package/dist/dispatch-adapter.d.ts.map +1 -1
  6. package/dist/event-format.d.ts +1 -1
  7. package/dist/event-format.d.ts.map +1 -1
  8. package/dist/event-format.js +68 -25
  9. package/dist/gateway-base.d.ts +14 -13
  10. package/dist/gateway-base.d.ts.map +1 -1
  11. package/dist/gateway-base.js +650 -313
  12. package/dist/index.d.ts +15 -13
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +13 -12
  15. package/dist/internal/attachment-input.d.ts +3 -3
  16. package/dist/internal/attachment-input.d.ts.map +1 -1
  17. package/dist/internal/attachment-input.js +61 -58
  18. package/dist/logger.d.ts +1 -1
  19. package/dist/platform-config.d.ts +28 -2
  20. package/dist/platform-config.d.ts.map +1 -1
  21. package/dist/platform-config.js +42 -11
  22. package/dist/prompt-fragments.d.ts +2 -2
  23. package/dist/prompt-fragments.d.ts.map +1 -1
  24. package/dist/prompt-fragments.js +37 -14
  25. package/dist/provider-config.d.ts +9 -0
  26. package/dist/provider-config.d.ts.map +1 -1
  27. package/dist/provider-config.js +13 -2
  28. package/dist/routing.d.ts +5 -5
  29. package/dist/routing.js +6 -6
  30. package/dist/session-state.d.ts +16 -0
  31. package/dist/session-state.d.ts.map +1 -1
  32. package/dist/session-state.js +45 -0
  33. package/dist/skills/index.d.ts +5 -4
  34. package/dist/skills/index.d.ts.map +1 -1
  35. package/dist/skills/index.js +28 -21
  36. package/dist/skills/parall-clips.d.ts +2 -0
  37. package/dist/skills/parall-clips.d.ts.map +1 -0
  38. package/dist/skills/parall-clips.js +44 -0
  39. package/dist/skills/parall-platform.d.ts +1 -1
  40. package/dist/skills/parall-platform.d.ts.map +1 -1
  41. package/dist/skills/parall-platform.js +6 -2
  42. package/dist/skills/parall-tasks.d.ts +1 -1
  43. package/dist/skills/parall-tasks.d.ts.map +1 -1
  44. package/dist/skills/parall-tasks.js +1 -1
  45. package/dist/skills/parall-wiki.d.ts +1 -1
  46. package/dist/skills/parall-wiki.d.ts.map +1 -1
  47. package/dist/skills/parall-wiki.js +1 -1
  48. package/dist/telemetry.d.ts +27 -0
  49. package/dist/telemetry.d.ts.map +1 -0
  50. package/dist/telemetry.js +205 -0
  51. package/dist/types.d.ts +18 -2
  52. package/dist/types.d.ts.map +1 -1
  53. package/package.json +11 -2
  54. package/src/bridge-workspace.ts +13 -13
  55. package/src/dispatch-adapter.ts +31 -8
  56. package/src/event-format.ts +80 -30
  57. package/src/gateway-base.ts +988 -445
  58. package/src/index.ts +23 -13
  59. package/src/internal/attachment-input.ts +127 -100
  60. package/src/logger.ts +1 -1
  61. package/src/platform-config.ts +61 -16
  62. package/src/prompt-fragments.ts +37 -14
  63. package/src/provider-config.ts +14 -2
  64. package/src/routing.ts +11 -11
  65. package/src/session-state.ts +62 -0
  66. package/src/skills/index.ts +34 -23
  67. package/src/skills/parall-clips.ts +44 -0
  68. package/src/skills/parall-platform.ts +6 -2
  69. package/src/skills/parall-tasks.ts +1 -1
  70. package/src/skills/parall-wiki.ts +1 -1
  71. package/src/telemetry.ts +252 -0
  72. package/src/types.ts +18 -2
package/src/index.ts CHANGED
@@ -1,13 +1,23 @@
1
- export * from "./provider-config.js";
2
- export * from "./types.js";
3
- export * from "./session-state.js";
4
- export * from "./routing.js";
5
- export * from "./event-format.js";
6
- export * from "./prompt-fragments.js";
7
- export * from "./bridge-workspace.js";
8
- export * from "./dispatch-adapter.js";
9
- export { createLogger, childLogger } from "./logger.js";
10
- export * from "./gateway-base.js";
11
- export * from "./platform-config.js";
12
- export { writeSkillFiles, buildSkillReferences, SKILLS } from "./skills/index.js";
13
- export type { SkillMeta } from "./skills/index.js";
1
+ export * from './provider-config.js';
2
+ export * from './types.js';
3
+ export * from './session-state.js';
4
+ export * from './routing.js';
5
+ export * from './event-format.js';
6
+ export * from './prompt-fragments.js';
7
+ export * from './bridge-workspace.js';
8
+ export * from './dispatch-adapter.js';
9
+ export { createLogger, childLogger } from './logger.js';
10
+ export * from './gateway-base.js';
11
+ export * from './platform-config.js';
12
+ export { writeSkillFiles, buildSkillReferences, SKILLS } from './skills/index.js';
13
+ export type { SkillMeta } from './skills/index.js';
14
+ export {
15
+ initAgentTelemetry,
16
+ createOtelLogger,
17
+ runWithSessionKey,
18
+ startDispatchSpan,
19
+ endDispatchSpan,
20
+ recordDispatchMetric,
21
+ recordMissingReply,
22
+ } from './telemetry.js';
23
+ export type { TelemetryHandle } from './telemetry.js';
@@ -1,12 +1,12 @@
1
- import { execSync } from "node:child_process";
2
- import { constants, type Dirent, type Stats } from "node:fs";
3
- import * as fsSync from "node:fs";
4
- import * as fs from "node:fs/promises";
5
- import * as path from "node:path";
6
- import type { DispatchContext, GatewayLogger } from "../dispatch-adapter.js";
7
- import { formatBytes, renderLocalAttachmentSection } from "../prompt-fragments.js";
8
- import type { LocalAttachmentResult, PreparedLocalImage } from "../prompt-fragments.js";
9
- import type { ParallEvent } from "../types.js";
1
+ import { execSync } from 'node:child_process';
2
+ import { constants, type Dirent, type Stats } from 'node:fs';
3
+ import * as fsSync from 'node:fs';
4
+ import * as fs from 'node:fs/promises';
5
+ import * as path from 'node:path';
6
+ import type { DispatchContext, GatewayLogger } from '../dispatch-adapter.js';
7
+ import { formatBytes, renderLocalAttachmentSection } from '../prompt-fragments.js';
8
+ import type { LocalAttachmentResult, PreparedLocalImage } from '../prompt-fragments.js';
9
+ import type { ParallEvent } from '../types.js';
10
10
 
11
11
  // Re-export so external consumers (codex-agent, claude-agent) keep importing
12
12
  // via the internal/attachment-input subpath. The canonical definition lives
@@ -50,11 +50,11 @@ export const DEFAULT_ATTACHMENT_TTL_MS = 24 * 60 * 60 * 1000;
50
50
  export const DEFAULT_MAINTENANCE_COOLDOWN_MS = 60 * 1000;
51
51
 
52
52
  const SUPPORTED_IMAGE_MIME_TYPES = new Set([
53
- "image/png",
54
- "image/jpeg",
55
- "image/jpg",
56
- "image/webp",
57
- "image/gif",
53
+ 'image/png',
54
+ 'image/jpeg',
55
+ 'image/jpg',
56
+ 'image/webp',
57
+ 'image/gif',
58
58
  ]);
59
59
 
60
60
  // Process-global state — correct under the current "one gateway per agent"
@@ -72,11 +72,16 @@ export async function prepareLocalImageAttachments(
72
72
  opts: LocalAttachmentOptions,
73
73
  ): Promise<LocalAttachmentResult> {
74
74
  const attachments = event.attachments ?? [];
75
- const imageAttachments = attachments.filter((att) => SUPPORTED_IMAGE_MIME_TYPES.has(att.mimeType.toLowerCase()));
75
+ const imageAttachments = attachments.filter((att) =>
76
+ SUPPORTED_IMAGE_MIME_TYPES.has(att.mimeType.toLowerCase()),
77
+ );
76
78
  if (imageAttachments.length === 0) return { images: [], notes: [] };
77
79
 
78
80
  const maxTotalImageBytes = opts.maxTotalImageBytes ?? DEFAULT_MAX_TOTAL_IMAGE_BYTES;
79
- const totalDeclaredBytes = imageAttachments.reduce((sum, att) => sum + Math.max(0, att.fileSize), 0);
81
+ const totalDeclaredBytes = imageAttachments.reduce(
82
+ (sum, att) => sum + Math.max(0, att.fileSize),
83
+ 0,
84
+ );
80
85
  if (totalDeclaredBytes > maxTotalImageBytes) {
81
86
  return {
82
87
  images: [],
@@ -107,15 +112,12 @@ export async function prepareLocalImageAttachments(
107
112
  // (one pass per cooldown window) and run it off the dispatch path. The
108
113
  // cache cap is best-effort, not a hard ceiling — cooldown drift is fine.
109
114
  const maintenanceCooldownMs = opts.maintenanceCooldownMs ?? DEFAULT_MAINTENANCE_COOLDOWN_MS;
110
- const maintenancePromise = scheduleAttachmentMaintenance(
111
- rootDir,
112
- {
113
- ttlMs: opts.attachmentTtlMs ?? DEFAULT_ATTACHMENT_TTL_MS,
114
- maxBytes: opts.maxCacheBytes ?? DEFAULT_ATTACHMENT_CACHE_MAX_BYTES,
115
- cooldownMs: maintenanceCooldownMs,
116
- log: opts.log,
117
- },
118
- );
115
+ const maintenancePromise = scheduleAttachmentMaintenance(rootDir, {
116
+ ttlMs: opts.attachmentTtlMs ?? DEFAULT_ATTACHMENT_TTL_MS,
117
+ maxBytes: opts.maxCacheBytes ?? DEFAULT_ATTACHMENT_CACHE_MAX_BYTES,
118
+ cooldownMs: maintenanceCooldownMs,
119
+ log: opts.log,
120
+ });
119
121
  if (maintenanceCooldownMs === 0 && maintenancePromise) {
120
122
  // Sync-mode for tests that need deterministic prune ordering relative to
121
123
  // active-dispatch tracking. Production paths leave this on the background.
@@ -150,7 +152,9 @@ export async function prepareLocalImageAttachments(
150
152
  try {
151
153
  await fetchFresh();
152
154
  } catch (err) {
153
- opts.log?.warn?.(`agent-core: failed to prepare local image attachment ${att.id}: ${String(err)}`);
155
+ opts.log?.warn?.(
156
+ `agent-core: failed to prepare local image attachment ${att.id}: ${String(err)}`,
157
+ );
154
158
  notes.push(`[Image attachment unavailable locally: prll://${sanitizeMeta(att.id)}]`);
155
159
  continue;
156
160
  }
@@ -167,7 +171,9 @@ export async function prepareLocalImageAttachments(
167
171
  try {
168
172
  await fetchFresh();
169
173
  } catch (err) {
170
- opts.log?.warn?.(`agent-core: failed to re-download corrupted cache for ${att.id}: ${String(err)}`);
174
+ opts.log?.warn?.(
175
+ `agent-core: failed to re-download corrupted cache for ${att.id}: ${String(err)}`,
176
+ );
171
177
  notes.push(`[Image attachment unavailable locally: prll://${sanitizeMeta(att.id)}]`);
172
178
  continue;
173
179
  }
@@ -176,7 +182,9 @@ export async function prepareLocalImageAttachments(
176
182
 
177
183
  if (!mimeOk) {
178
184
  await removeLocalFileIfInside(localPath, rootDir);
179
- notes.push(`[Image attachment unavailable locally: prll://${sanitizeMeta(att.id)} (downloaded content does not match ${sanitizeMeta(att.mimeType)})]`);
185
+ notes.push(
186
+ `[Image attachment unavailable locally: prll://${sanitizeMeta(att.id)} (downloaded content does not match ${sanitizeMeta(att.mimeType)})]`,
187
+ );
180
188
  continue;
181
189
  }
182
190
 
@@ -195,10 +203,7 @@ export async function prepareLocalImageAttachments(
195
203
  }
196
204
  }
197
205
 
198
- export function appendLocalAttachmentRefs(
199
- body: string,
200
- result: LocalAttachmentResult,
201
- ): string {
206
+ export function appendLocalAttachmentRefs(body: string, result: LocalAttachmentResult): string {
202
207
  const section = renderLocalAttachmentSection(result);
203
208
  return section ? `${body}\n\n${section}` : body;
204
209
  }
@@ -230,7 +235,7 @@ export function pinLocalAttachmentPaths(images: PreparedLocalImage[]): () => voi
230
235
  }
231
236
 
232
237
  function attachmentRootDir(workspaceDir: string): string {
233
- return path.join(path.resolve(workspaceDir), ".parall", "attachments");
238
+ return path.join(path.resolve(workspaceDir), '.parall', 'attachments');
234
239
  }
235
240
 
236
241
  /**
@@ -241,17 +246,17 @@ function attachmentRootDir(workspaceDir: string): string {
241
246
  */
242
247
  export function ensureLocalAttachmentGitExclude(workingDirectory: string): void {
243
248
  try {
244
- const rel = execSync("git rev-parse --git-path info/exclude", {
249
+ const rel = execSync('git rev-parse --git-path info/exclude', {
245
250
  cwd: workingDirectory,
246
- encoding: "utf8",
247
- stdio: ["ignore", "pipe", "ignore"],
251
+ encoding: 'utf8',
252
+ stdio: ['ignore', 'pipe', 'ignore'],
248
253
  }).trim();
249
254
  const excludePath = path.isAbsolute(rel) ? rel : path.join(workingDirectory, rel);
250
255
  fsSync.mkdirSync(path.dirname(excludePath), { recursive: true });
251
- const existing = fsSync.existsSync(excludePath) ? fsSync.readFileSync(excludePath, "utf8") : "";
252
- if (existing.split(/\r?\n/).some((line) => line.trim() === ".parall/")) return;
253
- const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
254
- fsSync.appendFileSync(excludePath, `${prefix}.parall/\n`, "utf8");
256
+ const existing = fsSync.existsSync(excludePath) ? fsSync.readFileSync(excludePath, 'utf8') : '';
257
+ if (existing.split(/\r?\n/).some((line) => line.trim() === '.parall/')) return;
258
+ const prefix = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
259
+ fsSync.appendFileSync(excludePath, `${prefix}.parall/\n`, 'utf8');
255
260
  } catch {
256
261
  // Best-effort only. A missing exclude entry should not block dispatch.
257
262
  }
@@ -308,7 +313,7 @@ function scheduleAttachmentMaintenance(
308
313
 
309
314
  async function ensureAttachmentRootDir(workspaceDir: string): Promise<string> {
310
315
  const workspaceRoot = path.resolve(workspaceDir);
311
- const parallDir = path.join(workspaceRoot, ".parall");
316
+ const parallDir = path.join(workspaceRoot, '.parall');
312
317
  const rootDir = attachmentRootDir(workspaceRoot);
313
318
 
314
319
  await fs.mkdir(workspaceRoot, { recursive: true });
@@ -334,17 +339,21 @@ async function ensurePathIsNotSymlink(filePath: string): Promise<void> {
334
339
  throw new Error(`refusing to use symlinked attachment path ${filePath}`);
335
340
  }
336
341
  } catch (err) {
337
- if ((err as NodeJS.ErrnoException)?.code === "ENOENT") return;
342
+ if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') return;
338
343
  throw err;
339
344
  }
340
345
  }
341
346
 
342
347
  function isPathInside(childPath: string, parentPath: string): boolean {
343
348
  const rel = path.relative(parentPath, childPath);
344
- return rel === "" || (!!rel && !rel.startsWith("..") && !path.isAbsolute(rel));
349
+ return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
345
350
  }
346
351
 
347
- async function existingUsableFile(filePath: string, expectedSize: number, rootDir: string): Promise<boolean> {
352
+ async function existingUsableFile(
353
+ filePath: string,
354
+ expectedSize: number,
355
+ rootDir: string,
356
+ ): Promise<boolean> {
348
357
  try {
349
358
  const stat = await localFileStatInsideRoot(filePath, rootDir);
350
359
  return stat.isFile() && stat.size > 0 && (expectedSize <= 0 || stat.size === expectedSize);
@@ -437,7 +446,11 @@ async function openLocalTempFileInsideRoot(filePath: string, rootDir: string) {
437
446
  }
438
447
  }
439
448
 
440
- async function assertLocalFileIdentity(filePath: string, rootDir: string, expected: Stats): Promise<void> {
449
+ async function assertLocalFileIdentity(
450
+ filePath: string,
451
+ rootDir: string,
452
+ expected: Stats,
453
+ ): Promise<void> {
441
454
  const stat = await localFileStatInsideRoot(filePath, rootDir);
442
455
  if (!sameFileIdentity(stat, expected)) {
443
456
  throw new Error(`attachment file changed during validation: ${filePath}`);
@@ -475,24 +488,26 @@ async function cleanupOldAttachmentFiles(
475
488
  }
476
489
 
477
490
  const cutoff = Date.now() - ttlMs;
478
- await Promise.all(entries.map(async (entry) => {
479
- if (!entry.isDirectory()) return;
480
- const fullPath = path.join(rootDir, entry.name);
481
- try {
482
- if (preserveDirs?.has(path.resolve(fullPath))) return;
483
- // lstat (not stat) for consistency with the symlink-hardened download
484
- // path: if a racing local writer swapped the dir entry for a symlink
485
- // between readdir and now, we don't want fs.rm to follow it and delete
486
- // outside the attachment root.
487
- const stat = await fs.lstat(fullPath);
488
- if (!stat.isDirectory()) return;
489
- if (stat.mtimeMs < cutoff) {
490
- await fs.rm(fullPath, { recursive: true, force: true });
491
+ await Promise.all(
492
+ entries.map(async (entry) => {
493
+ if (!entry.isDirectory()) return;
494
+ const fullPath = path.join(rootDir, entry.name);
495
+ try {
496
+ if (preserveDirs?.has(path.resolve(fullPath))) return;
497
+ // lstat (not stat) for consistency with the symlink-hardened download
498
+ // path: if a racing local writer swapped the dir entry for a symlink
499
+ // between readdir and now, we don't want fs.rm to follow it and delete
500
+ // outside the attachment root.
501
+ const stat = await fs.lstat(fullPath);
502
+ if (!stat.isDirectory()) return;
503
+ if (stat.mtimeMs < cutoff) {
504
+ await fs.rm(fullPath, { recursive: true, force: true });
505
+ }
506
+ } catch (err) {
507
+ log?.warn?.(`agent-core: failed to clean attachment temp dir ${fullPath}: ${String(err)}`);
491
508
  }
492
- } catch (err) {
493
- log?.warn?.(`agent-core: failed to clean attachment temp dir ${fullPath}: ${String(err)}`);
494
- }
495
- }));
509
+ }),
510
+ );
496
511
  }
497
512
 
498
513
  async function pruneAttachmentCache(
@@ -596,7 +611,7 @@ async function downloadAttachmentToFile(
596
611
  } catch {
597
612
  throw new Error(`invalid attachment URL`);
598
613
  }
599
- if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
614
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
600
615
  throw new Error(`refusing attachment URL with scheme ${parsed.protocol}`);
601
616
  }
602
617
 
@@ -612,7 +627,7 @@ async function downloadAttachmentToFile(
612
627
  throw new Error(`${res.status} ${res.statusText}`);
613
628
  }
614
629
  // Content-Length is only an early-exit hint; the stream byte counter below is authoritative.
615
- const contentLength = parseContentLength(res.headers.get("content-length"));
630
+ const contentLength = parseContentLength(res.headers.get('content-length'));
616
631
  if (contentLength !== undefined && contentLength > maxBytes) {
617
632
  throw new Error(`download would exceed ${formatBytes(maxBytes)} remaining turn limit`);
618
633
  }
@@ -649,7 +664,7 @@ async function writeResponseToFileWithLimit(
649
664
  rootDir: string,
650
665
  ): Promise<number> {
651
666
  if (maxBytes <= 0) {
652
- throw new Error("download would exceed turn limit");
667
+ throw new Error('download would exceed turn limit');
653
668
  }
654
669
 
655
670
  const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
@@ -716,26 +731,30 @@ function localFileName(attachmentId: string, fileName: string, mimeType: string)
716
731
 
717
732
  function extensionForMime(mimeType: string): string {
718
733
  switch (mimeType.toLowerCase()) {
719
- case "image/jpeg":
720
- case "image/jpg":
721
- return ".jpg";
722
- case "image/webp":
723
- return ".webp";
724
- case "image/gif":
725
- return ".gif";
726
- case "image/png":
734
+ case 'image/jpeg':
735
+ case 'image/jpg':
736
+ return '.jpg';
737
+ case 'image/webp':
738
+ return '.webp';
739
+ case 'image/gif':
740
+ return '.gif';
741
+ case 'image/png':
727
742
  default:
728
- return ".png";
743
+ return '.png';
729
744
  }
730
745
  }
731
746
 
732
747
  function sanitizePathSegment(value: string): string {
733
- const safe = value.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
734
- if (!safe || /^\.+$/.test(safe)) return "attachment";
748
+ const safe = value.replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^_+|_+$/g, '');
749
+ if (!safe || /^\.+$/.test(safe)) return 'attachment';
735
750
  return safe;
736
751
  }
737
752
 
738
- async function downloadedFileMatchesImageMime(filePath: string, mimeType: string, rootDir: string): Promise<boolean> {
753
+ async function downloadedFileMatchesImageMime(
754
+ filePath: string,
755
+ mimeType: string,
756
+ rootDir: string,
757
+ ): Promise<boolean> {
739
758
  const file = await openLocalFileInsideRoot(filePath, rootDir);
740
759
  try {
741
760
  const buffer = Buffer.alloc(16);
@@ -748,38 +767,47 @@ async function downloadedFileMatchesImageMime(filePath: string, mimeType: string
748
767
 
749
768
  function imageHeaderMatches(bytes: Buffer, mimeType: string): boolean {
750
769
  switch (mimeType.toLowerCase()) {
751
- case "image/png":
752
- return bytes.length >= 8
753
- && bytes[0] === 0x89
754
- && bytes[1] === 0x50
755
- && bytes[2] === 0x4e
756
- && bytes[3] === 0x47
757
- && bytes[4] === 0x0d
758
- && bytes[5] === 0x0a
759
- && bytes[6] === 0x1a
760
- && bytes[7] === 0x0a;
761
- case "image/jpeg":
762
- case "image/jpg":
770
+ case 'image/png':
771
+ return (
772
+ bytes.length >= 8 &&
773
+ bytes[0] === 0x89 &&
774
+ bytes[1] === 0x50 &&
775
+ bytes[2] === 0x4e &&
776
+ bytes[3] === 0x47 &&
777
+ bytes[4] === 0x0d &&
778
+ bytes[5] === 0x0a &&
779
+ bytes[6] === 0x1a &&
780
+ bytes[7] === 0x0a
781
+ );
782
+ case 'image/jpeg':
783
+ case 'image/jpg':
763
784
  return bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff;
764
- case "image/gif":
765
- return bytes.length >= 6
766
- && (bytes.subarray(0, 6).toString("ascii") === "GIF87a"
767
- || bytes.subarray(0, 6).toString("ascii") === "GIF89a");
768
- case "image/webp":
769
- return bytes.length >= 12
770
- && bytes.subarray(0, 4).toString("ascii") === "RIFF"
771
- && bytes.subarray(8, 12).toString("ascii") === "WEBP";
785
+ case 'image/gif':
786
+ return (
787
+ bytes.length >= 6 &&
788
+ (bytes.subarray(0, 6).toString('ascii') === 'GIF87a' ||
789
+ bytes.subarray(0, 6).toString('ascii') === 'GIF89a')
790
+ );
791
+ case 'image/webp':
792
+ return (
793
+ bytes.length >= 12 &&
794
+ bytes.subarray(0, 4).toString('ascii') === 'RIFF' &&
795
+ bytes.subarray(8, 12).toString('ascii') === 'WEBP'
796
+ );
772
797
  default:
773
798
  return false;
774
799
  }
775
800
  }
776
801
 
777
802
  function isAbortError(err: unknown): boolean {
778
- return err instanceof DOMException && err.name === "AbortError";
803
+ return err instanceof DOMException && err.name === 'AbortError';
779
804
  }
780
805
 
781
806
  function sanitizeMeta(value: string): string {
782
- return value.replace(/[\r\n]+/g, " ").replace(/[[\]|]/g, " ").trim();
807
+ return value
808
+ .replace(/[\r\n]+/g, ' ')
809
+ .replace(/[[\]|]/g, ' ')
810
+ .trim();
783
811
  }
784
812
 
785
813
  function parseContentLength(value: string | null): number | undefined {
@@ -788,4 +816,3 @@ function parseContentLength(value: string | null): number | undefined {
788
816
  if (!Number.isFinite(n) || n < 0) return undefined;
789
817
  return n;
790
818
  }
791
-
package/src/logger.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { GatewayLogger } from "./dispatch-adapter.js";
1
+ import type { GatewayLogger } from './dispatch-adapter.js';
2
2
 
3
3
  export function createLogger(prefix: string): GatewayLogger {
4
4
  return {
@@ -1,7 +1,7 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import type { ParallClient, PlatformConfigResponse } from "@parall/sdk";
4
- import type { GatewayLogger } from "./dispatch-adapter.js";
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import type { ParallClient, PlatformConfigResponse } from '@parall/sdk';
4
+ import type { GatewayLogger } from './dispatch-adapter.js';
5
5
 
6
6
  export interface PlatformDefaults {
7
7
  model: string | null;
@@ -19,8 +19,49 @@ export interface PlatformManagementProfile {
19
19
  model_management?: string | null;
20
20
  }
21
21
 
22
- export function isPlatformManagedProfile(profile: PlatformManagementProfile | null | undefined): boolean {
23
- return profile?.machine_id != null || profile?.model_management === "platform";
22
+ export function isPlatformManagedProfile(
23
+ profile: PlatformManagementProfile | null | undefined,
24
+ ): boolean {
25
+ return profile?.machine_id != null || profile?.model_management === 'platform';
26
+ }
27
+
28
+ /**
29
+ * Whether the platform-delivered model should be treated as an operator
30
+ * OVERRIDE (beats env) rather than a FLOOR (loses to env). True only when the
31
+ * profile is platform-managed AND not explicitly self-managed: legacy hosted
32
+ * profiles (model_management=null, resolved to platform server-side) are
33
+ * overrides; local self agents — whose delivered model is a catalog floor —
34
+ * are not. Pass the result as `resolveRuntimeModel`'s first argument. Keeping
35
+ * this derivation in one place avoids drift across the bridges' (re)config
36
+ * sites where the subtle null-hosted-vs-explicit-self distinction matters.
37
+ */
38
+ export function isPlatformModelOverride(
39
+ profile: PlatformManagementProfile | null | undefined,
40
+ ): boolean {
41
+ return isPlatformManagedProfile(profile) && profile?.model_management !== 'self';
42
+ }
43
+
44
+ /**
45
+ * Resolve the model a runtime should launch with. Precedence:
46
+ * 1. platform operator override (only when the agent is platform-managed)
47
+ * 2. env/runtime config (PRLL_CLAUDE_MODEL / PRLL_CODEX_MODEL)
48
+ * 3. server-delivered model floor (applies even when model_management=self)
49
+ *
50
+ * The floor (3) is what makes self-hosted + Parall-proxy agents work: the
51
+ * server now delivers `agents.defaults.model` whenever the route is the
52
+ * catalog-gated Parall proxy, so the CLI never falls back to its built-in
53
+ * default (which can outrun the catalog and get rejected). Returns null when
54
+ * no model is resolved, so the bridge omits `--model` and lets the CLI / own
55
+ * login pick — the correct behavior for runtime_auth / custom routes, where
56
+ * the server delivers no floor.
57
+ */
58
+ export function resolveRuntimeModel(
59
+ isOperatorOverride: boolean,
60
+ platformModel: string | null,
61
+ configModel: string | null | undefined,
62
+ ): string | undefined {
63
+ const operatorOverride = isOperatorOverride ? platformModel : null;
64
+ return operatorOverride ?? configModel ?? platformModel ?? undefined;
24
65
  }
25
66
 
26
67
  interface PlatformModelDef {
@@ -34,7 +75,7 @@ interface CachedPlatformConfig {
34
75
  fetchedAt: string;
35
76
  }
36
77
 
37
- const CACHE_FILENAME = "parall-platform-config.json";
78
+ const CACHE_FILENAME = 'parall-platform-config.json';
38
79
  const SUPPORTED_SCHEMA_VERSION = 1;
39
80
 
40
81
  function cachePath(stateDir: string): string {
@@ -43,7 +84,7 @@ function cachePath(stateDir: string): string {
43
84
 
44
85
  function loadCache(stateDir: string): CachedPlatformConfig | null {
45
86
  try {
46
- const raw = fs.readFileSync(cachePath(stateDir), "utf-8");
87
+ const raw = fs.readFileSync(cachePath(stateDir), 'utf-8');
47
88
  return JSON.parse(raw) as CachedPlatformConfig;
48
89
  } catch {
49
90
  return null;
@@ -59,7 +100,7 @@ function saveCache(stateDir: string, response: PlatformConfigResponse): void {
59
100
  const filePath = cachePath(stateDir);
60
101
  const tmpPath = `${filePath}.tmp`;
61
102
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
62
- fs.writeFileSync(tmpPath, JSON.stringify(cached, null, 2), "utf-8");
103
+ fs.writeFileSync(tmpPath, JSON.stringify(cached, null, 2), 'utf-8');
63
104
  fs.renameSync(tmpPath, filePath);
64
105
  }
65
106
 
@@ -69,7 +110,7 @@ function runtimeModelName(
69
110
  config: Record<string, unknown>,
70
111
  ): string | null {
71
112
  const runtime = runtimeType?.trim();
72
- if (!runtime || runtime === "openclaw") return canonicalModel;
113
+ if (!runtime || runtime === 'openclaw') return canonicalModel;
73
114
 
74
115
  const models = (config.models ?? {}) as Record<string, unknown>;
75
116
  const providers = (models.providers ?? {}) as Record<string, unknown>;
@@ -78,7 +119,7 @@ function runtimeModelName(
78
119
  const match = catalog.find((model) => model.id === canonicalModel);
79
120
  const runtimeNames = (match?.runtime_names ?? {}) as Record<string, unknown>;
80
121
  const runtimeName = runtimeNames[runtime];
81
- return typeof runtimeName === "string" && runtimeName ? runtimeName : null;
122
+ return typeof runtimeName === 'string' && runtimeName ? runtimeName : null;
82
123
  }
83
124
 
84
125
  function extractDefaults(config: Record<string, unknown>, runtimeType?: string): PlatformDefaults {
@@ -86,13 +127,13 @@ function extractDefaults(config: Record<string, unknown>, runtimeType?: string):
86
127
  const defaults = (agents.defaults ?? {}) as Record<string, unknown>;
87
128
 
88
129
  let model: string | null = null;
89
- if (typeof defaults.model === "string" && defaults.model) {
90
- const canonicalModel = defaults.model.replace(/^parall\//, "");
130
+ if (typeof defaults.model === 'string' && defaults.model) {
131
+ const canonicalModel = defaults.model.replace(/^parall\//, '');
91
132
  model = runtimeModelName(canonicalModel, runtimeType, config);
92
133
  }
93
134
 
94
135
  let thinkingEffort: string | null = null;
95
- if (typeof defaults.thinking_effort === "string" && defaults.thinking_effort) {
136
+ if (typeof defaults.thinking_effort === 'string' && defaults.thinking_effort) {
96
137
  thinkingEffort = defaults.thinking_effort;
97
138
  }
98
139
 
@@ -124,7 +165,9 @@ export function createPlatformConfigManager(opts: {
124
165
  fresh = await client.getPlatformConfig(cachedVersion);
125
166
  } catch (err) {
126
167
  if (cached) {
127
- log?.warn(`platform config fetch failed, using cached version ${cachedVersion}: ${String(err)}`);
168
+ log?.warn(
169
+ `platform config fetch failed, using cached version ${cachedVersion}: ${String(err)}`,
170
+ );
128
171
  return currentDefaults;
129
172
  }
130
173
  log?.warn(`platform config fetch failed and no cache available: ${String(err)}`);
@@ -136,7 +179,9 @@ export function createPlatformConfigManager(opts: {
136
179
  }
137
180
 
138
181
  if (fresh.schema_version !== undefined && fresh.schema_version > SUPPORTED_SCHEMA_VERSION) {
139
- log?.warn(`platform config schema_version ${fresh.schema_version} > supported (${SUPPORTED_SCHEMA_VERSION}), keeping current`);
182
+ log?.warn(
183
+ `platform config schema_version ${fresh.schema_version} > supported (${SUPPORTED_SCHEMA_VERSION}), keeping current`,
184
+ );
140
185
  return currentDefaults;
141
186
  }
142
187