@gajae-code/coding-agent 0.12.4 → 0.12.6

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 (139) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +4 -1
  3. package/dist/types/async/job-manager.d.ts +9 -1
  4. package/dist/types/eval/backend.d.ts +2 -2
  5. package/dist/types/exec/bash-executor.d.ts +10 -8
  6. package/dist/types/extensibility/extensions/runner.d.ts +9 -7
  7. package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
  8. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
  9. package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
  10. package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
  11. package/dist/types/gjc-runtime/team-store.d.ts +0 -4
  12. package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
  13. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  14. package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
  15. package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
  16. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
  17. package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +83 -0
  18. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
  19. package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
  20. package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
  21. package/dist/types/modes/components/plugin-settings.d.ts +2 -0
  22. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  23. package/dist/types/sdk/broker/broker.d.ts +18 -0
  24. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
  25. package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
  26. package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -2
  27. package/dist/types/sdk/bus/index.d.ts +4 -0
  28. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -3
  29. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
  30. package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
  31. package/dist/types/sdk/host/host.d.ts +23 -2
  32. package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
  33. package/dist/types/session/agent-session.d.ts +5 -4
  34. package/dist/types/session/artifacts.d.ts +8 -5
  35. package/dist/types/session/attempt-record-store.d.ts +19 -0
  36. package/dist/types/session/client-bridge.d.ts +14 -0
  37. package/dist/types/session/internal/managed-session-storage.d.ts +7 -23
  38. package/dist/types/session/messages.d.ts +4 -1
  39. package/dist/types/session/session-manager.d.ts +7 -5
  40. package/dist/types/session/session-storage.d.ts +8 -0
  41. package/dist/types/session/streaming-output.d.ts +32 -0
  42. package/dist/types/task/executor.d.ts +1 -1
  43. package/dist/types/task/index.d.ts +2 -1
  44. package/dist/types/task/receipt.d.ts +3 -0
  45. package/dist/types/task/types.d.ts +194 -0
  46. package/dist/types/task/worktree.d.ts +13 -6
  47. package/dist/types/tools/bash.d.ts +1 -1
  48. package/dist/types/tools/fetch.d.ts +3 -0
  49. package/dist/types/tools/output-meta.d.ts +37 -5
  50. package/dist/types/tools/subagent.d.ts +2 -0
  51. package/dist/types/tools/tool-result.d.ts +1 -0
  52. package/dist/types/utils/artifact-id.d.ts +2 -0
  53. package/dist/types/utils/git.d.ts +1 -0
  54. package/package.json +8 -8
  55. package/scripts/capture-sticky-viewport-showcase.ts +131 -5
  56. package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
  57. package/scripts/verify-sticky-viewport-showcase.ts +620 -93
  58. package/src/async/job-manager.ts +13 -5
  59. package/src/config/model-profiles.ts +8 -0
  60. package/src/config/settings-schema.ts +17 -3
  61. package/src/cursor.ts +20 -13
  62. package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
  63. package/src/eval/backend.ts +2 -2
  64. package/src/exec/bash-executor.ts +425 -151
  65. package/src/extensibility/extensions/runner.ts +80 -6
  66. package/src/extensibility/extensions/wrapper.ts +22 -15
  67. package/src/extensibility/runtime-skill-discovery.ts +4 -6
  68. package/src/gjc-runtime/launch-tmux.ts +309 -293
  69. package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
  70. package/src/gjc-runtime/psmux-detect.ts +52 -6
  71. package/src/gjc-runtime/team-launch.ts +50 -2
  72. package/src/gjc-runtime/team-runtime.ts +716 -251
  73. package/src/gjc-runtime/team-store.ts +25 -7
  74. package/src/gjc-runtime/team-workers.ts +161 -1
  75. package/src/gjc-runtime/tmux-common.ts +16 -4
  76. package/src/gjc-runtime/tmux-gc.ts +37 -5
  77. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
  78. package/src/gjc-runtime/tmux-provider-context.ts +451 -0
  79. package/src/gjc-runtime/tmux-sessions.ts +409 -156
  80. package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
  81. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
  82. package/src/gjc-runtime/ultragoal-review-source.ts +378 -0
  83. package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
  84. package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
  85. package/src/gjc-runtime/workflow-manifest.ts +12 -0
  86. package/src/internal-urls/agent-protocol.ts +98 -38
  87. package/src/internal-urls/artifact-protocol.ts +28 -17
  88. package/src/internal-urls/docs-index.generated.ts +6 -6
  89. package/src/internal-urls/local-protocol.ts +39 -18
  90. package/src/internal-urls/registry-helpers.ts +62 -0
  91. package/src/modes/acp/acp-agent.ts +51 -13
  92. package/src/modes/acp/acp-client-bridge.ts +154 -11
  93. package/src/modes/components/gjc-bundle-settings.ts +2 -0
  94. package/src/modes/components/plugin-settings.ts +5 -0
  95. package/src/modes/components/settings-selector.ts +4 -0
  96. package/src/modes/controllers/selector-controller.ts +1 -0
  97. package/src/prompts/tools/bash.md +2 -2
  98. package/src/prompts/tools/monitor.md +4 -4
  99. package/src/sdk/acp/adapter.ts +200 -9
  100. package/src/sdk/broker/broker.ts +93 -10
  101. package/src/sdk/broker/lifecycle-ledger.ts +227 -11
  102. package/src/sdk/broker/lifecycle.ts +692 -98
  103. package/src/sdk/bus/chat-daemon-control.ts +33 -5
  104. package/src/sdk/bus/index.ts +347 -14
  105. package/src/sdk/bus/telegram-daemon-contract.ts +7 -3
  106. package/src/sdk/bus/telegram-daemon.ts +283 -44
  107. package/src/sdk/bus/topic-registry.ts +133 -10
  108. package/src/sdk/host/host.ts +187 -9
  109. package/src/sdk/host/reverse-leases.ts +249 -42
  110. package/src/sdk/session.ts +8 -6
  111. package/src/session/agent-session.ts +930 -354
  112. package/src/session/artifacts.ts +211 -74
  113. package/src/session/attempt-record-store.ts +106 -0
  114. package/src/session/client-bridge.ts +15 -0
  115. package/src/session/internal/managed-session-scope.ts +332 -198
  116. package/src/session/internal/managed-session-storage.ts +162 -450
  117. package/src/session/messages.ts +32 -7
  118. package/src/session/session-manager.ts +31 -28
  119. package/src/session/session-storage.ts +190 -53
  120. package/src/session/streaming-output.ts +198 -40
  121. package/src/task/executor.ts +5 -2
  122. package/src/task/index.ts +277 -64
  123. package/src/task/provider-retry-status.ts +4 -1
  124. package/src/task/receipt.ts +26 -2
  125. package/src/task/render.ts +42 -1
  126. package/src/task/types.ts +52 -0
  127. package/src/task/worktree.ts +122 -43
  128. package/src/tools/bash.ts +818 -239
  129. package/src/tools/eval.ts +99 -4
  130. package/src/tools/fetch.ts +77 -15
  131. package/src/tools/find.ts +7 -3
  132. package/src/tools/monitor.ts +7 -1
  133. package/src/tools/output-meta.ts +706 -116
  134. package/src/tools/search.ts +9 -4
  135. package/src/tools/subagent-render.ts +5 -2
  136. package/src/tools/subagent.ts +8 -0
  137. package/src/tools/tool-result.ts +1 -0
  138. package/src/utils/artifact-id.ts +6 -0
  139. package/src/utils/git.ts +2 -0
@@ -1745,6 +1745,40 @@
1745
1745
  "name": "goal-id",
1746
1746
  "type": "string"
1747
1747
  },
1748
+ {
1749
+ "appliesToVerbs": [
1750
+ "review-source"
1751
+ ],
1752
+ "name": "cohort-id",
1753
+ "type": "string"
1754
+ },
1755
+ {
1756
+ "appliesToVerbs": [
1757
+ "review-source"
1758
+ ],
1759
+ "name": "task-id",
1760
+ "type": "string"
1761
+ },
1762
+ {
1763
+ "appliesToVerbs": [
1764
+ "review-source"
1765
+ ],
1766
+ "enumValues": [
1767
+ "cleaner",
1768
+ "architect",
1769
+ "qa",
1770
+ "critic"
1771
+ ],
1772
+ "name": "lane",
1773
+ "type": "enum"
1774
+ },
1775
+ {
1776
+ "appliesToVerbs": [
1777
+ "review-source"
1778
+ ],
1779
+ "name": "rerun-command",
1780
+ "type": "string"
1781
+ },
1748
1782
  {
1749
1783
  "appliesToVerbs": [
1750
1784
  "steer"
@@ -1873,7 +1907,8 @@
1873
1907
  "steer",
1874
1908
  "classify-blocker",
1875
1909
  "record-critic-verdict",
1876
- "record-critic-gate-override"
1910
+ "record-critic-gate-override",
1911
+ "review-source"
1877
1912
  ],
1878
1913
  "name": "json",
1879
1914
  "type": "boolean"
@@ -1970,6 +2005,10 @@
1970
2005
  "name": "quality-gate",
1971
2006
  "surface": "command-positional"
1972
2007
  },
2008
+ {
2009
+ "name": "review-source",
2010
+ "surface": "command-positional"
2011
+ },
1973
2012
  {
1974
2013
  "name": "graph",
1975
2014
  "planned": true,
@@ -306,6 +306,7 @@ export const WORKFLOW_MANIFEST: Record<CanonicalGjcWorkflowSkill, SkillManifest>
306
306
  "record-critic-verdict",
307
307
  "record-critic-gate-override",
308
308
  "quality-gate",
309
+ "review-source",
309
310
  ]),
310
311
  ...plannedVerbs(PLANNED_ADMIN_VERBS),
311
312
  ],
@@ -362,6 +363,16 @@ export const WORKFLOW_MANIFEST: Record<CanonicalGjcWorkflowSkill, SkillManifest>
362
363
  { name: "classification-event-id", type: "string", appliesToVerbs: ["record-critic-verdict"] },
363
364
  { name: "quality-gate-json", type: "string", appliesToVerbs: ["checkpoint", "quality-gate"] },
364
365
  { name: "goal-id", type: "string", appliesToVerbs: ["quality-gate"] },
366
+ { name: "cohort-id", type: "string", appliesToVerbs: ["review-source"] },
367
+ { name: "task-id", type: "string", appliesToVerbs: ["review-source"] },
368
+ {
369
+ name: "lane",
370
+ type: "enum",
371
+ enumValues: ["cleaner", "architect", "qa", "critic"],
372
+ appliesToVerbs: ["review-source"],
373
+ },
374
+ { name: "rerun-command", type: "string", appliesToVerbs: ["review-source"] },
375
+
365
376
  { name: "goal-id", type: "string", appliesToVerbs: ["steer"] },
366
377
  { name: "goal-id", type: "string", appliesToVerbs: ["classify-blocker"] },
367
378
  {
@@ -413,6 +424,7 @@ export const WORKFLOW_MANIFEST: Record<CanonicalGjcWorkflowSkill, SkillManifest>
413
424
  "classify-blocker",
414
425
  "record-critic-verdict",
415
426
  "record-critic-gate-override",
427
+ "review-source",
416
428
  ],
417
429
  },
418
430
  { name: "directive-json", type: "string", appliesToVerbs: ["steer"], planned: true },
@@ -11,11 +11,16 @@
11
11
  * - agent://<id>?q=<query> - JSON extraction via query form
12
12
  */
13
13
  import { createHash } from "node:crypto";
14
- import * as fs from "node:fs/promises";
15
14
  import * as path from "node:path";
16
15
  import { isEnoent } from "@gajae-code/utils";
17
16
  import { applyQuery, pathToQuery } from "./json-query";
18
- import { authorizedArtifactsDirsFromContext } from "./registry-helpers";
17
+ import {
18
+ type AuthorizedArtifactsRoot,
19
+ authorizedArtifactsDirsFromContext,
20
+ closeAuthorizedArtifactsRoot,
21
+ openAuthorizedArtifactsRoot,
22
+ readAuthorizedArtifactFile,
23
+ } from "./registry-helpers";
19
24
  import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext } from "./types";
20
25
 
21
26
  interface AgentOutputMetadata {
@@ -36,6 +41,24 @@ interface ManagedOutputSelector {
36
41
  metadataSha256: string;
37
42
  }
38
43
 
44
+ const MAX_AGENT_SELECTOR_BYTES = 64 * 1024;
45
+ const MAX_AGENT_METADATA_BYTES = 1024 * 1024;
46
+ const MAX_AGENT_OUTPUT_BYTES = 16 * 1024 * 1024;
47
+
48
+ function readRetainedAgentFile(
49
+ outputId: string,
50
+ root: AuthorizedArtifactsRoot,
51
+ filename: string,
52
+ maxBytes: number,
53
+ ): Buffer | null {
54
+ const captured = readAuthorizedArtifactFile(root, filename, maxBytes);
55
+ if (!captured) return null;
56
+ if (captured.size !== captured.bytes.byteLength) {
57
+ throw new Error(`agent://${outputId} retained file exceeds read capacity`);
58
+ }
59
+ return captured.bytes;
60
+ }
61
+
39
62
  function isSafeGenerationFilename(filename: unknown): filename is string {
40
63
  return typeof filename === "string" && /^[a-zA-Z0-9_.-]+$/.test(filename);
41
64
  }
@@ -56,17 +79,14 @@ function isManagedOutputSelector(value: unknown, outputId: string): value is Man
56
79
  );
57
80
  }
58
81
 
59
- async function readManagedOutputSelector(
82
+ function readManagedOutputSelector(
60
83
  outputId: string,
61
- selectorPath: string,
62
- ): Promise<ManagedOutputSelector | null> {
63
- let raw: string;
64
- try {
65
- raw = await Bun.file(selectorPath).text();
66
- } catch (error) {
67
- if (isEnoent(error)) return null;
68
- throw error;
69
- }
84
+ root: AuthorizedArtifactsRoot,
85
+ selectorFilename: string,
86
+ ): ManagedOutputSelector | null {
87
+ const bytes = readRetainedAgentFile(outputId, root, selectorFilename, MAX_AGENT_SELECTOR_BYTES);
88
+ if (!bytes) return null;
89
+ const raw = bytes.toString("utf8");
70
90
  let parsed: unknown;
71
91
  try {
72
92
  parsed = JSON.parse(raw);
@@ -76,6 +96,18 @@ async function readManagedOutputSelector(
76
96
  if (!isManagedOutputSelector(parsed, outputId)) throw new Error(`agent://${outputId} malformed output selector`);
77
97
  return parsed;
78
98
  }
99
+ function sameManagedOutputSelector(left: ManagedOutputSelector, right: ManagedOutputSelector): boolean {
100
+ return (
101
+ left.outputFilename === right.outputFilename &&
102
+ left.metadataFilename === right.metadataFilename &&
103
+ left.outputSizeBytes === right.outputSizeBytes &&
104
+ left.outputSha256 === right.outputSha256 &&
105
+ left.metadataSizeBytes === right.metadataSizeBytes &&
106
+ left.metadataSha256 === right.metadataSha256
107
+ );
108
+ }
109
+
110
+ const MANAGED_SELECTOR_READ_ATTEMPTS = 8;
79
111
 
80
112
  function isAgentOutputMetadata(value: unknown, outputId: string): value is AgentOutputMetadata {
81
113
  if (!value || typeof value !== "object") return false;
@@ -92,18 +124,11 @@ function isAgentOutputMetadata(value: unknown, outputId: string): value is Agent
92
124
 
93
125
  async function verifyAgentOutputMetadata(
94
126
  outputId: string,
95
- foundPath: string,
96
- metadataPath: string,
127
+ metadataBytes: Buffer,
97
128
  bytes: Buffer,
98
129
  selector?: ManagedOutputSelector,
99
130
  ): Promise<void> {
100
- let metaRaw: string;
101
- try {
102
- metaRaw = await Bun.file(metadataPath).text();
103
- } catch (err) {
104
- if (isEnoent(err)) throw new Error(`agent://${outputId} missing metadata`);
105
- throw err;
106
- }
131
+ const metaRaw = metadataBytes.toString("utf8");
107
132
  let parsed: unknown;
108
133
  try {
109
134
  parsed = JSON.parse(metaRaw);
@@ -113,9 +138,7 @@ async function verifyAgentOutputMetadata(
113
138
  if (!isAgentOutputMetadata(parsed, outputId)) {
114
139
  throw new Error(`agent://${outputId} malformed metadata`);
115
140
  }
116
- const metadataBytes = Buffer.from(metaRaw, "utf8");
117
- const stat = await fs.stat(foundPath);
118
- if (stat.size !== parsed.sizeBytes || bytes.byteLength !== parsed.sizeBytes) {
141
+ if (bytes.byteLength !== parsed.sizeBytes) {
119
142
  throw new Error(`agent://${outputId} size mismatch`);
120
143
  }
121
144
  const sha256 = createHash("sha256").update(bytes).digest("hex");
@@ -172,27 +195,64 @@ export class AgentProtocolHandler implements ProtocolHandler {
172
195
  let foundPath: string | undefined;
173
196
  let foundMetadataPath: string | undefined;
174
197
  let foundSelector: ManagedOutputSelector | undefined;
198
+ let foundBytes: Buffer | undefined;
199
+ let foundMetadataBytes: Buffer | undefined;
175
200
  let anyDirExists = false;
176
201
 
177
202
  for (const dir of dirs) {
203
+ let root: AuthorizedArtifactsRoot;
178
204
  try {
179
- await fs.stat(dir);
205
+ root = openAuthorizedArtifactsRoot(dir);
180
206
  anyDirExists = true;
181
207
  } catch (err) {
182
208
  if (isEnoent(err)) continue;
183
209
  throw err;
184
210
  }
185
- const selector = await readManagedOutputSelector(outputId, path.join(dir, `${outputId}.md.selector.json`));
186
- const candidate = selector ? path.join(dir, selector.outputFilename) : path.join(dir, `${outputId}.md`);
187
- const metadataPath = selector ? path.join(dir, selector.metadataFilename) : `${candidate}.meta.json`;
188
211
  try {
189
- await fs.stat(candidate);
190
- if (foundPath) throw new Error(`agent://${outputId} ambiguous id in authorized artifacts`);
191
- foundPath = candidate;
192
- foundMetadataPath = metadataPath;
193
- foundSelector = selector ?? undefined;
194
- } catch (err) {
195
- if (!isEnoent(err)) throw err;
212
+ const selectorFilename = `${outputId}.md.selector.json`;
213
+ let selector = readManagedOutputSelector(outputId, root, selectorFilename);
214
+ for (let attempt = 0; attempt < MANAGED_SELECTOR_READ_ATTEMPTS; attempt++) {
215
+ const candidateFilename = selector ? selector.outputFilename : `${outputId}.md`;
216
+ const metadataFilename = selector ? selector.metadataFilename : `${candidateFilename}.meta.json`;
217
+ const rawBuffer = readRetainedAgentFile(outputId, root, candidateFilename, MAX_AGENT_OUTPUT_BYTES);
218
+ const metadataBuffer = readRetainedAgentFile(outputId, root, metadataFilename, MAX_AGENT_METADATA_BYTES);
219
+ if (rawBuffer && metadataBuffer) {
220
+ if (selector) {
221
+ const refreshed = readManagedOutputSelector(outputId, root, selectorFilename);
222
+ if (!refreshed || !sameManagedOutputSelector(selector, refreshed)) {
223
+ if (attempt + 1 >= MANAGED_SELECTOR_READ_ATTEMPTS) {
224
+ throw new Error(`agent://${outputId} generation changed during read`);
225
+ }
226
+ if (!refreshed) throw new Error(`agent://${outputId} generation changed during read`);
227
+ selector = refreshed;
228
+ continue;
229
+ }
230
+ }
231
+ if (foundPath) throw new Error(`agent://${outputId} ambiguous id in authorized artifacts`);
232
+ foundPath = path.join(dir, candidateFilename);
233
+ foundMetadataPath = path.join(dir, metadataFilename);
234
+ foundSelector = selector ?? undefined;
235
+ foundBytes = rawBuffer;
236
+ foundMetadataBytes = metadataBuffer;
237
+ break;
238
+ }
239
+ if (!selector) {
240
+ if (!rawBuffer) break;
241
+ throw new Error(`agent://${outputId} missing metadata`);
242
+ }
243
+ if (attempt + 1 >= MANAGED_SELECTOR_READ_ATTEMPTS) {
244
+ throw new Error(`agent://${outputId} generation changed during read`);
245
+ }
246
+ const refreshed = readManagedOutputSelector(outputId, root, selectorFilename);
247
+ if (!refreshed) throw new Error(`agent://${outputId} generation changed during read`);
248
+ if (sameManagedOutputSelector(selector, refreshed)) {
249
+ if (!rawBuffer) throw new Error(`agent://${outputId} selected generation unavailable`);
250
+ throw new Error(`agent://${outputId} missing metadata`);
251
+ }
252
+ selector = refreshed;
253
+ }
254
+ } finally {
255
+ closeAuthorizedArtifactsRoot(root);
196
256
  }
197
257
  }
198
258
 
@@ -200,12 +260,12 @@ export class AgentProtocolHandler implements ProtocolHandler {
200
260
  throw new Error("No artifacts directory found");
201
261
  }
202
262
 
203
- if (!foundPath) {
263
+ if (!foundPath || !foundMetadataPath || !foundBytes || !foundMetadataBytes) {
204
264
  throw new Error(`agent://${outputId} not found`);
205
265
  }
206
266
 
207
- const rawBytes = Buffer.from(await Bun.file(foundPath).arrayBuffer());
208
- await verifyAgentOutputMetadata(outputId, foundPath, foundMetadataPath!, rawBytes, foundSelector);
267
+ const rawBytes = foundBytes;
268
+ await verifyAgentOutputMetadata(outputId, foundMetadataBytes, rawBytes, foundSelector);
209
269
  const rawContent = rawBytes.toString("utf8");
210
270
  const notes: string[] = [];
211
271
  let content = rawContent;
@@ -12,7 +12,14 @@
12
12
  import * as fs from "node:fs/promises";
13
13
  import * as path from "node:path";
14
14
  import { isEnoent } from "@gajae-code/utils";
15
- import { authorizedArtifactsDirsFromContext } from "./registry-helpers";
15
+ import { isValidArtifactId } from "../utils/artifact-id";
16
+ import {
17
+ type AuthorizedArtifactsRoot,
18
+ authorizedArtifactsDirsFromContext,
19
+ closeAuthorizedArtifactsRoot,
20
+ openAuthorizedArtifactsRoot,
21
+ readAuthorizedArtifactFile,
22
+ } from "./registry-helpers";
16
23
  import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext } from "./types";
17
24
 
18
25
  export class ArtifactProtocolHandler implements ProtocolHandler {
@@ -24,7 +31,7 @@ export class ArtifactProtocolHandler implements ProtocolHandler {
24
31
  if (!id) {
25
32
  throw new Error("artifact:// URL requires a numeric ID: artifact://0");
26
33
  }
27
- if (!/^\d+$/.test(id)) {
34
+ if (!isValidArtifactId(id)) {
28
35
  throw new Error(`artifact:// ID must be numeric, got: ${id}`);
29
36
  }
30
37
 
@@ -35,23 +42,33 @@ export class ArtifactProtocolHandler implements ProtocolHandler {
35
42
  }
36
43
 
37
44
  let foundPath: string | undefined;
45
+ let foundBytes: Buffer | undefined;
46
+ let foundSize = 0;
38
47
  let anyDirExists = false;
48
+ const MAX_ARTIFACT_READ_BYTES = 16 * 1024 * 1024;
39
49
 
40
50
  for (const dir of dirs) {
41
- let files: string[];
51
+ let root: AuthorizedArtifactsRoot;
42
52
  try {
43
- files = await fs.readdir(dir);
53
+ root = openAuthorizedArtifactsRoot(dir);
44
54
  anyDirExists = true;
45
55
  } catch (err) {
46
56
  if (isEnoent(err)) continue;
47
57
  throw err;
48
58
  }
49
- for (const f of files) {
50
- if (f.endsWith(".meta.json")) continue;
51
- if (f.startsWith(`${id}.`)) {
59
+ try {
60
+ const files = await fs.readdir(dir);
61
+ for (const f of files) {
62
+ if (f.endsWith(".meta.json") || !f.startsWith(`${id}.`)) continue;
63
+ const captured = readAuthorizedArtifactFile(root, f, MAX_ARTIFACT_READ_BYTES);
64
+ if (!captured) continue;
52
65
  if (foundPath) throw new Error(`artifact://${id} ambiguous id in authorized artifacts`);
53
66
  foundPath = path.join(dir, f);
67
+ foundBytes = captured.bytes;
68
+ foundSize = captured.size;
54
69
  }
70
+ } finally {
71
+ closeAuthorizedArtifactsRoot(root);
55
72
  }
56
73
  }
57
74
 
@@ -59,20 +76,14 @@ export class ArtifactProtocolHandler implements ProtocolHandler {
59
76
  throw new Error("No artifacts directory found");
60
77
  }
61
78
 
62
- if (!foundPath) {
79
+ if (!foundPath || !foundBytes) {
63
80
  throw new Error(`artifact://${id} not found`);
64
81
  }
65
82
 
66
- // F20: cap the materialized artifact so reading a huge spilled artifact cannot
67
- // buffer GBs into memory (the range selector is applied downstream, so without a
68
- // cap a `artifact://id:range` over a multi-GB artifact still reads it whole).
69
- const MAX_ARTIFACT_READ_BYTES = 16 * 1024 * 1024;
70
- const file = Bun.file(foundPath);
71
- const fullSize = file.size;
72
83
  const content =
73
- fullSize > MAX_ARTIFACT_READ_BYTES
74
- ? `${await file.slice(0, MAX_ARTIFACT_READ_BYTES).text()}\n\n[Artifact truncated: first ${MAX_ARTIFACT_READ_BYTES} of ${fullSize} bytes shown; use a narrower range or a specialized tool for the full content.]`
75
- : await file.text();
84
+ foundSize > foundBytes.byteLength
85
+ ? `${foundBytes.toString("utf8")}\n\n[Artifact truncated: first ${foundBytes.byteLength} of ${foundSize} bytes shown; use a narrower range or a specialized tool for the full content.]`
86
+ : foundBytes.toString("utf8");
76
87
  return {
77
88
  url: url.href,
78
89
  content,