@gajae-code/coding-agent 0.12.6 → 0.12.8

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 (150) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/dist/types/async/job-manager.d.ts +36 -1
  3. package/dist/types/commands/notify.d.ts +4 -0
  4. package/dist/types/config/file-lock.d.ts +3 -0
  5. package/dist/types/config/model-discovery-manager.d.ts +6 -1
  6. package/dist/types/config/model-registry.d.ts +2 -0
  7. package/dist/types/config/resolve-config-value.d.ts +2 -1
  8. package/dist/types/config/settings-schema.d.ts +17 -1
  9. package/dist/types/eval/backend.d.ts +2 -2
  10. package/dist/types/exec/bash-executor.d.ts +8 -10
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -36
  12. package/dist/types/internal-urls/registry-helpers.d.ts +0 -18
  13. package/dist/types/main.d.ts +1 -0
  14. package/dist/types/modes/components/hook-selector.d.ts +20 -0
  15. package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
  16. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  17. package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
  18. package/dist/types/sdk/broker/broker.d.ts +5 -1
  19. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
  20. package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -3
  21. package/dist/types/sdk/bus/config.d.ts +75 -113
  22. package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
  23. package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
  24. package/dist/types/sdk/bus/index.d.ts +1 -1
  25. package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
  26. package/dist/types/sdk/bus/notification-service.d.ts +31 -4
  27. package/dist/types/sdk/bus/session-control.d.ts +8 -3
  28. package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
  29. package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
  30. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  31. package/dist/types/sdk/client/discovery.d.ts +2 -1
  32. package/dist/types/sdk/host/host.d.ts +2 -23
  33. package/dist/types/sdk/host/query/handlers.d.ts +2 -0
  34. package/dist/types/sdk/host/reverse-leases.d.ts +4 -7
  35. package/dist/types/sdk/index.d.ts +1 -0
  36. package/dist/types/sdk/providers.d.ts +22 -0
  37. package/dist/types/sdk/session-directory.d.ts +1 -1
  38. package/dist/types/session/artifacts.d.ts +5 -8
  39. package/dist/types/session/client-bridge.d.ts +0 -14
  40. package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
  41. package/dist/types/session/internal/managed-session-storage.d.ts +11 -4
  42. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -0
  43. package/dist/types/session/internal/session-open-errors.d.ts +5 -0
  44. package/dist/types/session/messages.d.ts +1 -4
  45. package/dist/types/session/session-manager.d.ts +7 -6
  46. package/dist/types/session/session-storage.d.ts +2 -0
  47. package/dist/types/session/streaming-output.d.ts +0 -32
  48. package/dist/types/task/receipt.d.ts +1 -2
  49. package/dist/types/task/types.d.ts +43 -178
  50. package/dist/types/tools/bash.d.ts +1 -1
  51. package/dist/types/tools/fetch.d.ts +0 -3
  52. package/dist/types/tools/output-meta.d.ts +5 -37
  53. package/dist/types/tools/subagent.d.ts +15 -0
  54. package/dist/types/tools/todo-write.d.ts +3 -0
  55. package/dist/types/tools/tool-result.d.ts +0 -1
  56. package/package.json +10 -8
  57. package/scripts/build-sdk-package-smoke.ts +51 -0
  58. package/scripts/capture-notifications-settings-showcase.ts +1 -1
  59. package/src/async/job-manager.ts +430 -46
  60. package/src/cli/notify-cli.ts +204 -94
  61. package/src/commands/notify.ts +19 -8
  62. package/src/config/file-lock.ts +6 -0
  63. package/src/config/model-discovery-manager.ts +50 -8
  64. package/src/config/model-registry.ts +862 -75
  65. package/src/config/resolve-config-value.ts +10 -8
  66. package/src/config/settings-schema.ts +25 -15
  67. package/src/config/settings.ts +15 -15
  68. package/src/cursor.ts +13 -20
  69. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -3
  70. package/src/eval/backend.ts +2 -2
  71. package/src/exec/bash-executor.ts +151 -425
  72. package/src/gjc-runtime/session-state-sidecar.ts +12 -2
  73. package/src/gjc-runtime/ultragoal-runtime.ts +3 -280
  74. package/src/gjc-runtime/workflow-manifest.generated.json +1 -40
  75. package/src/gjc-runtime/workflow-manifest.ts +0 -12
  76. package/src/internal-urls/agent-protocol.ts +38 -98
  77. package/src/internal-urls/artifact-protocol.ts +17 -28
  78. package/src/internal-urls/docs-index.generated.ts +7 -7
  79. package/src/internal-urls/registry-helpers.ts +0 -62
  80. package/src/main.ts +43 -7
  81. package/src/modes/DESIGN.md +11 -11
  82. package/src/modes/acp/acp-agent.ts +0 -24
  83. package/src/modes/acp/acp-client-bridge.ts +11 -154
  84. package/src/modes/components/hook-selector.ts +155 -44
  85. package/src/modes/components/model-selector.ts +242 -49
  86. package/src/modes/components/notifications-settings-editor.ts +631 -85
  87. package/src/modes/controllers/extension-ui-controller.ts +114 -15
  88. package/src/modes/controllers/selector-controller.ts +464 -50
  89. package/src/prompts/tools/bash.md +2 -2
  90. package/src/sdk/acp/adapter.ts +9 -200
  91. package/src/sdk/broker/broker.ts +54 -10
  92. package/src/sdk/broker/lifecycle-ledger.ts +34 -1
  93. package/src/sdk/broker/lifecycle.ts +47 -50
  94. package/src/sdk/bus/chat-daemon-cli.ts +12 -5
  95. package/src/sdk/bus/chat-daemon-control.ts +35 -8
  96. package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
  97. package/src/sdk/bus/config.ts +534 -286
  98. package/src/sdk/bus/discord-live-provider.ts +57 -2
  99. package/src/sdk/bus/discord-provider.ts +21 -0
  100. package/src/sdk/bus/index.ts +105 -359
  101. package/src/sdk/bus/notification-orchestration.ts +371 -68
  102. package/src/sdk/bus/notification-service.ts +431 -130
  103. package/src/sdk/bus/session-control.ts +119 -44
  104. package/src/sdk/bus/slack-live-provider.ts +78 -2
  105. package/src/sdk/bus/slack-provider.ts +24 -0
  106. package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
  107. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  108. package/src/sdk/bus/telegram-daemon-control.ts +3 -3
  109. package/src/sdk/bus/telegram-daemon.ts +23 -15
  110. package/src/sdk/client/discovery.ts +10 -4
  111. package/src/sdk/host/host.ts +9 -187
  112. package/src/sdk/host/query/handlers.ts +20 -1
  113. package/src/sdk/host/reverse-leases.ts +42 -249
  114. package/src/sdk/index.ts +1 -0
  115. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  116. package/src/sdk/protocol/operation-registry.ts +5 -2
  117. package/src/sdk/providers.ts +51 -0
  118. package/src/sdk/session-directory.ts +1 -0
  119. package/src/sdk/session.ts +10 -7
  120. package/src/session/agent-session.ts +59 -202
  121. package/src/session/artifacts.ts +74 -211
  122. package/src/session/client-bridge.ts +0 -15
  123. package/src/session/internal/managed-session-scope.ts +981 -450
  124. package/src/session/internal/managed-session-storage.ts +362 -124
  125. package/src/session/internal/native-publish-outcome.ts +3 -1
  126. package/src/session/internal/session-open-errors.ts +11 -0
  127. package/src/session/messages.ts +7 -32
  128. package/src/session/session-manager.ts +48 -15
  129. package/src/session/session-storage.ts +72 -5
  130. package/src/session/streaming-output.ts +40 -198
  131. package/src/slash-commands/builtin-registry.ts +82 -4
  132. package/src/task/index.ts +237 -65
  133. package/src/task/receipt.ts +2 -4
  134. package/src/task/render.ts +0 -30
  135. package/src/task/types.ts +10 -34
  136. package/src/tools/ask.ts +1 -1
  137. package/src/tools/bash.ts +239 -818
  138. package/src/tools/eval.ts +4 -99
  139. package/src/tools/fetch.ts +15 -77
  140. package/src/tools/find.ts +3 -7
  141. package/src/tools/output-meta.ts +114 -696
  142. package/src/tools/search.ts +4 -9
  143. package/src/tools/subagent.ts +97 -75
  144. package/src/tools/telegram-send.ts +4 -2
  145. package/src/tools/todo-write.ts +47 -0
  146. package/src/tools/tool-result.ts +0 -1
  147. package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +0 -83
  148. package/dist/types/utils/artifact-id.d.ts +0 -2
  149. package/src/gjc-runtime/ultragoal-review-source.ts +0 -378
  150. package/src/utils/artifact-id.ts +0 -6
@@ -9,10 +9,8 @@ import { createHash, randomUUID } from "node:crypto";
9
9
  import * as fs from "node:fs/promises";
10
10
 
11
11
  import * as path from "node:path";
12
- import { isValidArtifactId } from "../utils/artifact-id";
13
12
  import {
14
13
  ensureManagedDirectory,
15
- type ManagedFileSnapshot,
16
14
  type ManagedSessionDescendantStore,
17
15
  publishManagedFileNoReplace,
18
16
  } from "./internal/managed-session-storage";
@@ -35,9 +33,6 @@ function isSafeFilename(filename: string): boolean {
35
33
  return /^[a-zA-Z0-9_.-]+$/.test(filename);
36
34
  }
37
35
 
38
- const MANAGED_OUTPUT_GENERATION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
39
- const SHA256_PATTERN = /^[0-9a-f]{64}$/;
40
-
41
36
  function parseManagedOutputGeneration(value: Uint8Array, outputFilenamePrefix: string): ManagedOutputGeneration | null {
42
37
  try {
43
38
  const parsed = JSON.parse(Buffer.from(value).toString("utf8")) as Partial<ManagedOutputGeneration>;
@@ -48,19 +43,10 @@ function parseManagedOutputGeneration(value: Uint8Array, outputFilenamePrefix: s
48
43
  typeof parsed.outputSha256 !== "string" ||
49
44
  typeof parsed.metadataSizeBytes !== "number" ||
50
45
  typeof parsed.metadataSha256 !== "string" ||
51
- !Number.isSafeInteger(parsed.outputSizeBytes) ||
52
- (parsed.outputSizeBytes ?? -1) < 0 ||
53
- !Number.isSafeInteger(parsed.metadataSizeBytes) ||
54
- (parsed.metadataSizeBytes ?? -1) < 0 ||
55
- !SHA256_PATTERN.test(parsed.outputSha256) ||
56
- !SHA256_PATTERN.test(parsed.metadataSha256) ||
57
46
  !isSafeFilename(parsed.outputFilename) ||
58
47
  !isSafeFilename(parsed.metadataFilename) ||
59
48
  !parsed.outputFilename.startsWith(`${outputFilenamePrefix}.`) ||
60
49
  !parsed.outputFilename.endsWith(".output") ||
61
- !MANAGED_OUTPUT_GENERATION_ID_PATTERN.test(
62
- parsed.outputFilename.slice(outputFilenamePrefix.length + 1, -".output".length),
63
- ) ||
64
50
  parsed.metadataFilename !== `${parsed.outputFilename}.meta.json`
65
51
  )
66
52
  return null;
@@ -71,36 +57,12 @@ function parseManagedOutputGeneration(value: Uint8Array, outputFilenamePrefix: s
71
57
  }
72
58
 
73
59
  function sameGeneration(left: ManagedOutputGeneration, right: ManagedOutputGeneration): boolean {
74
- return (
75
- left.outputFilename === right.outputFilename &&
76
- left.metadataFilename === right.metadataFilename &&
77
- left.outputSizeBytes === right.outputSizeBytes &&
78
- left.outputSha256 === right.outputSha256 &&
79
- left.metadataSizeBytes === right.metadataSizeBytes &&
80
- left.metadataSha256 === right.metadataSha256
81
- );
82
- }
83
-
84
- function referencesGeneration(value: Uint8Array, generation: ManagedOutputGeneration): boolean {
85
- try {
86
- const parsed = JSON.parse(Buffer.from(value).toString("utf8")) as Partial<ManagedOutputGeneration>;
87
- return (
88
- parsed.outputFilename === generation.outputFilename && parsed.metadataFilename === generation.metadataFilename
89
- );
90
- } catch {
91
- return false;
92
- }
60
+ return left.outputFilename === right.outputFilename && left.metadataFilename === right.metadataFilename;
93
61
  }
94
62
  export interface ArtifactSaveOptions {
95
63
  maxBytes?: number;
96
64
  }
97
65
 
98
- export interface ArtifactSaveReceipt {
99
- id: string;
100
- complete: boolean;
101
- omittedBytes?: number;
102
- }
103
-
104
66
  /**
105
67
  * Manages artifact storage for a session.
106
68
  *
@@ -117,7 +79,6 @@ export class ArtifactManager {
117
79
  readonly #store: ManagedSessionDescendantStore | undefined;
118
80
  #dirCreated = false;
119
81
  #initialized = false;
120
- #initializing: Promise<void> | undefined;
121
82
 
122
83
  /**
123
84
  * @param dir Directory that will hold artifact files. Created lazily on first save.
@@ -152,21 +113,15 @@ export class ArtifactManager {
152
113
  }
153
114
 
154
115
  async #ensureDir(): Promise<void> {
155
- if (this.#initialized) return;
156
- if (!this.#initializing) {
157
- this.#initializing = (async () => {
158
- if (!this.#dirCreated) {
159
- if (this.#store) this.#store.ensureDirectory();
160
- else ensureManagedDirectory(this.#dir);
161
- this.#dirCreated = true;
162
- }
163
- await this.#scanExistingIds();
164
- this.#initialized = true;
165
- })().finally(() => {
166
- this.#initializing = undefined;
167
- });
116
+ if (!this.#dirCreated) {
117
+ if (this.#store) this.#store.ensureDirectory();
118
+ else ensureManagedDirectory(this.#dir);
119
+ this.#dirCreated = true;
120
+ }
121
+ if (!this.#initialized) {
122
+ await this.#scanExistingIds();
123
+ this.#initialized = true;
168
124
  }
169
- await this.#initializing;
170
125
  }
171
126
 
172
127
  #filename(id: string, toolType: string): string {
@@ -204,116 +159,58 @@ export class ArtifactManager {
204
159
  }
205
160
  await this.#ensureDir();
206
161
  if (!this.#store) throw new Error("Managed output generation requires retained authority");
207
- const mutationFence = await this.#store.acquireMutationFence(`${selectorFilename}.publish`);
208
- try {
209
- const priorSelector = this.#store.readExpected(selectorFilename);
210
- const priorGeneration = priorSelector
211
- ? parseManagedOutputGeneration(priorSelector.bytes, outputFilenamePrefix)
212
- : null;
213
- const generationId = randomUUID();
214
- const outputFilename = `${outputFilenamePrefix}.${generationId}.output`;
215
- const metadataFilename = `${outputFilename}.meta.json`;
216
- const generation: ManagedOutputGeneration = {
217
- outputFilename,
218
- metadataFilename,
219
- outputSizeBytes: outputBytes.byteLength,
220
- outputSha256: sha256(outputBytes),
221
- metadataSizeBytes: metadataBytes.byteLength,
222
- metadataSha256: sha256(metadataBytes),
223
- };
224
-
225
- // Immutable generations are not visible until the selector is replaced.
226
- let stagedOutput: ManagedFileSnapshot | undefined;
227
- let stagedMetadata: ManagedFileSnapshot | undefined;
228
- const removeStaged = () => {
229
- for (const [filename, snapshot] of [
230
- [metadataFilename, stagedMetadata],
231
- [outputFilename, stagedOutput],
232
- ] as const) {
233
- if (!snapshot) continue;
234
- try {
235
- this.#store!.removeExpected(filename, snapshot);
236
- } catch {
237
- // An identity mismatch is not authority to remove a successor.
238
- }
239
- }
240
- };
241
- try {
242
- await this.#store.publishNoReplace(outputFilename, outputBytes);
243
- stagedOutput = this.#store.readExpected(outputFilename) ?? undefined;
244
- if (
245
- !stagedOutput ||
246
- stagedOutput.bytes.byteLength !== generation.outputSizeBytes ||
247
- sha256(stagedOutput.bytes) !== generation.outputSha256
248
- ) {
249
- throw new Error("managed_output_generation_verification_failed");
250
- }
251
- await this.#store.publishNoReplace(metadataFilename, metadataBytes);
252
- stagedMetadata = this.#store.readExpected(metadataFilename) ?? undefined;
253
- if (
254
- !stagedMetadata ||
255
- stagedMetadata.bytes.byteLength !== generation.metadataSizeBytes ||
256
- sha256(stagedMetadata.bytes) !== generation.metadataSha256
257
- ) {
258
- throw new Error("managed_output_generation_verification_failed");
259
- }
260
- } catch (error) {
261
- removeStaged();
262
- throw error;
263
- }
264
162
 
265
- const rollbackSelector = (observed: ManagedFileSnapshot | null): boolean => {
266
- if (!observed || !referencesGeneration(observed.bytes, generation)) return false;
267
- try {
268
- if (priorSelector) {
269
- this.#store!.replaceExpected(selectorFilename, priorSelector.bytes, observed);
270
- } else {
271
- this.#store!.removeExpected(selectorFilename, observed);
272
- }
273
- return true;
274
- } catch {
275
- return false;
276
- }
277
- };
278
- const selectorBytes = Buffer.from(JSON.stringify(generation), "utf8");
279
- try {
280
- if (priorSelector) this.#store.replaceExpected(selectorFilename, selectorBytes, priorSelector);
281
- else await this.#store.publishNoReplace(selectorFilename, selectorBytes);
282
- } catch (error) {
283
- let observed: ManagedFileSnapshot | null = null;
284
- let observedRead = false;
163
+ const priorSelector = this.#store.readExpected(selectorFilename);
164
+ const priorGeneration = priorSelector
165
+ ? parseManagedOutputGeneration(priorSelector.bytes, outputFilenamePrefix)
166
+ : null;
167
+ const generationId = randomUUID();
168
+ const outputFilename = `${outputFilenamePrefix}.${generationId}.output`;
169
+ const metadataFilename = `${outputFilename}.meta.json`;
170
+ const generation: ManagedOutputGeneration = {
171
+ outputFilename,
172
+ metadataFilename,
173
+ outputSizeBytes: outputBytes.byteLength,
174
+ outputSha256: sha256(outputBytes),
175
+ metadataSizeBytes: metadataBytes.byteLength,
176
+ metadataSha256: sha256(metadataBytes),
177
+ };
178
+
179
+ // Immutable generations are not visible until the selector is replaced.
180
+ await this.#store.publishNoReplace(outputFilename, outputBytes);
181
+ await this.#store.publishNoReplace(metadataFilename, metadataBytes);
182
+ const stagedOutput = this.#store.readExpected(outputFilename);
183
+ const stagedMetadata = this.#store.readExpected(metadataFilename);
184
+ if (
185
+ !stagedOutput ||
186
+ !stagedMetadata ||
187
+ stagedOutput.bytes.byteLength !== generation.outputSizeBytes ||
188
+ stagedMetadata.bytes.byteLength !== generation.metadataSizeBytes ||
189
+ sha256(stagedOutput.bytes) !== generation.outputSha256 ||
190
+ sha256(stagedMetadata.bytes) !== generation.metadataSha256
191
+ ) {
192
+ throw new Error("managed_output_generation_verification_failed");
193
+ }
194
+
195
+ await this.#store.replace(selectorFilename, Buffer.from(JSON.stringify(generation), "utf8"));
196
+ const publishedSelector = this.#store.readExpected(selectorFilename);
197
+ const publishedGeneration = publishedSelector
198
+ ? parseManagedOutputGeneration(publishedSelector.bytes, outputFilenamePrefix)
199
+ : null;
200
+ if (!publishedGeneration || !sameGeneration(publishedGeneration, generation)) {
201
+ throw new Error("managed_output_selector_verification_failed");
202
+ }
203
+
204
+ // Cleanup cannot affect the selected generation or publication outcome.
205
+ if (priorGeneration && !sameGeneration(priorGeneration, generation)) {
206
+ for (const filename of [priorGeneration.outputFilename, priorGeneration.metadataFilename]) {
285
207
  try {
286
- observed = this.#store.readExpected(selectorFilename);
287
- observedRead = true;
208
+ const previous = this.#store.readExpected(filename);
209
+ if (previous) this.#store.removeExpected(filename, previous);
288
210
  } catch {
289
- // Without an exact selector snapshot, staged files remain quarantined.
211
+ // Retain unreachable generations for a later safe cleanup.
290
212
  }
291
- if (observedRead && (!observed || !referencesGeneration(observed.bytes, generation))) removeStaged();
292
- else if (rollbackSelector(observed)) removeStaged();
293
- throw error;
294
213
  }
295
- const publishedSelector = this.#store.readExpected(selectorFilename);
296
- const publishedGeneration = publishedSelector
297
- ? parseManagedOutputGeneration(publishedSelector.bytes, outputFilenamePrefix)
298
- : null;
299
- if (!publishedGeneration || !sameGeneration(publishedGeneration, generation)) {
300
- if (rollbackSelector(publishedSelector)) removeStaged();
301
- throw new Error("managed_output_selector_verification_failed");
302
- }
303
-
304
- // Cleanup cannot affect the selected generation or publication outcome.
305
- if (priorGeneration && !sameGeneration(priorGeneration, generation)) {
306
- for (const filename of [priorGeneration.outputFilename, priorGeneration.metadataFilename]) {
307
- try {
308
- const previous = this.#store.readExpected(filename);
309
- if (previous) this.#store.removeExpected(filename, previous);
310
- } catch {
311
- // Retain unreachable generations for a later safe cleanup.
312
- }
313
- }
314
- }
315
- } finally {
316
- await mutationFence?.release();
317
214
  }
318
215
  }
319
216
  async publishNamedNoReplace(filename: string, bytes: Uint8Array): Promise<void> {
@@ -355,8 +252,8 @@ export class ArtifactManager {
355
252
  for (const file of files) {
356
253
  // Files are named: {id}.{toolType}.log
357
254
  const match = file.match(/^(\d+)\..*\.log$/);
358
- if (match && isValidArtifactId(match[1])) {
359
- const id = Number(match[1]);
255
+ if (match) {
256
+ const id = parseInt(match[1], 10);
360
257
  if (id > maxId) maxId = id;
361
258
  }
362
259
  }
@@ -368,9 +265,6 @@ export class ArtifactManager {
368
265
  * IDs are sequential within the session.
369
266
  */
370
267
  allocateId(): number {
371
- if (!Number.isSafeInteger(this.#nextId) || this.#nextId < 0) {
372
- throw new Error("Artifact id space exhausted");
373
- }
374
268
  return this.#nextId++;
375
269
  }
376
270
 
@@ -387,28 +281,25 @@ export class ArtifactManager {
387
281
  return { id, path: path.join(this.#dir, this.#filename(id, toolType)) };
388
282
  }
389
283
 
390
- /** Save content and return the artifact ID plus exact storage completeness evidence. */
391
- async saveWithReceipt(
392
- content: string,
393
- toolType: string,
394
- options: ArtifactSaveOptions = {},
395
- ): Promise<ArtifactSaveReceipt> {
284
+ /**
285
+ * Save content as an artifact and return the artifact ID.
286
+ * Content is written to a private temporary inode, synced, and linked into
287
+ * the artifact directory only after the complete terminal payload exists.
288
+ */
289
+ async save(content: string, toolType: string, options: ArtifactSaveOptions = {}): Promise<string> {
396
290
  await this.#ensureDir();
397
291
  const id = String(this.allocateId());
398
292
  const maxBytes = Math.max(0, options.maxBytes ?? DEFAULT_ARTIFACT_MAX_BYTES);
399
293
  const contentBytes = Buffer.byteLength(content, "utf-8");
400
- const truncated = contentBytes > maxBytes ? truncateHeadBytes(content, maxBytes) : undefined;
401
- const omittedBytes = truncated ? contentBytes - truncated.bytes : undefined;
402
- const published = truncated
403
- ? `${truncated.text}\n[artifact truncated after ${truncated.bytes} bytes; omitted at least ${omittedBytes} bytes]\n`
404
- : content;
294
+ const published =
295
+ contentBytes > maxBytes
296
+ ? (() => {
297
+ const truncated = truncateHeadBytes(content, maxBytes);
298
+ return `${truncated.text}\n[artifact truncated after ${truncated.bytes} bytes; omitted at least ${contentBytes - truncated.bytes} bytes]\n`;
299
+ })()
300
+ : content;
405
301
  await this.#publish(published, this.#filename(id, toolType));
406
- return { id, complete: omittedBytes === undefined, omittedBytes };
407
- }
408
-
409
- /** Save content as an artifact and return the artifact ID. */
410
- async save(content: string, toolType: string, options: ArtifactSaveOptions = {}): Promise<string> {
411
- return (await this.saveWithReceipt(content, toolType, options)).id;
302
+ return id;
412
303
  }
413
304
 
414
305
  /**
@@ -439,36 +330,8 @@ export class ArtifactManager {
439
330
  * @param id Artifact ID (numeric string)
440
331
  */
441
332
  async getPath(id: string): Promise<string | null> {
442
- if (!isValidArtifactId(id)) return null;
443
333
  const files = await this.listFiles();
444
- const matches = files.filter(filename => new RegExp(`^${id}\\.[a-zA-Z0-9_-]+\\.log$`, "u").test(filename));
445
- if (matches.length !== 1) return null;
446
- const filename = matches[0]!;
447
- if (this.#store) {
448
- try {
449
- return this.#store.readExpected(filename) ? path.join(this.#dir, filename) : null;
450
- } catch {
451
- return null;
452
- }
453
- }
454
- try {
455
- const rootBefore = await fs.lstat(this.#dir, { bigint: true });
456
- if (!rootBefore.isDirectory() || rootBefore.isSymbolicLink()) return null;
457
- const artifactPath = path.join(this.#dir, filename);
458
- const leaf = await fs.lstat(artifactPath, { bigint: true });
459
- if (!leaf.isFile() || leaf.isSymbolicLink() || leaf.nlink !== 1n) return null;
460
- const rootAfter = await fs.lstat(this.#dir, { bigint: true });
461
- if (
462
- !rootAfter.isDirectory() ||
463
- rootAfter.isSymbolicLink() ||
464
- rootAfter.dev !== rootBefore.dev ||
465
- rootAfter.ino !== rootBefore.ino
466
- ) {
467
- return null;
468
- }
469
- return artifactPath;
470
- } catch {
471
- return null;
472
- }
334
+ const match = files.find(f => f.startsWith(`${id}.`));
335
+ return match ? path.join(this.#dir, match) : null;
473
336
  }
474
337
  }
@@ -56,31 +56,18 @@ export interface ClientBridgeTerminalOutput {
56
56
 
57
57
  export interface ClientBridgeTerminalHandle {
58
58
  terminalId: string;
59
- /** Exact provider lease/session identity that owns this terminal. */
60
- providerLeaseId: string;
61
59
  waitForExit(): Promise<ClientBridgeTerminalExitStatus>;
62
60
  currentOutput(): Promise<ClientBridgeTerminalOutput>;
63
61
  kill(): Promise<void>;
64
62
  release(): Promise<void>;
65
- /** Full authenticated provider identity, when supplied by the transport. */
66
- providerLeaseIdentity?: ClientBridgeProviderLeaseIdentity;
67
- }
68
-
69
- export interface ClientBridgeProviderLeaseIdentity {
70
- leaseId: string;
71
- connectionId: string;
72
- connectionGeneration: number;
73
- fence: string;
74
63
  }
75
64
 
76
65
  export interface ClientBridgeCreateTerminalParams {
77
- toolCallId: string;
78
66
  command: string;
79
67
  args?: string[];
80
68
  env?: Array<{ name: string; value: string }>;
81
69
  cwd?: string;
82
70
  outputByteLimit?: number;
83
- signal?: AbortSignal;
84
71
  }
85
72
 
86
73
  export interface ClientBridge {
@@ -90,8 +77,6 @@ export interface ClientBridge {
90
77
  readTextFile?(params: { path: string; line?: number; limit?: number }): Promise<string>;
91
78
  writeTextFile?(params: { path: string; content: string }): Promise<void>;
92
79
  createTerminal?(params: ClientBridgeCreateTerminalParams): Promise<ClientBridgeTerminalHandle>;
93
- /** Fence the exact provider identity after cleanup ownership becomes uncertain. */
94
- quarantineProviderLease?(providerLease: string | ClientBridgeProviderLeaseIdentity, reason: string): Promise<void>;
95
80
  requestPermission?(
96
81
  toolCall: ClientBridgePermissionToolCall,
97
82
  options: ClientBridgePermissionOption[],