@gajae-code/coding-agent 0.12.6 → 0.12.7

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 (70) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/dist/types/async/job-manager.d.ts +36 -1
  3. package/dist/types/eval/backend.d.ts +2 -2
  4. package/dist/types/exec/bash-executor.d.ts +8 -10
  5. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -36
  6. package/dist/types/internal-urls/registry-helpers.d.ts +0 -18
  7. package/dist/types/sdk/bus/chat-daemon-control.d.ts +1 -3
  8. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -4
  9. package/dist/types/sdk/host/host.d.ts +2 -23
  10. package/dist/types/sdk/host/reverse-leases.d.ts +4 -7
  11. package/dist/types/session/artifacts.d.ts +5 -8
  12. package/dist/types/session/client-bridge.d.ts +0 -14
  13. package/dist/types/session/internal/managed-session-storage.d.ts +2 -5
  14. package/dist/types/session/messages.d.ts +1 -4
  15. package/dist/types/session/session-manager.d.ts +4 -5
  16. package/dist/types/session/streaming-output.d.ts +0 -32
  17. package/dist/types/task/receipt.d.ts +1 -2
  18. package/dist/types/task/types.d.ts +43 -178
  19. package/dist/types/tools/bash.d.ts +1 -1
  20. package/dist/types/tools/fetch.d.ts +0 -3
  21. package/dist/types/tools/output-meta.d.ts +5 -37
  22. package/dist/types/tools/subagent.d.ts +15 -0
  23. package/dist/types/tools/todo-write.d.ts +3 -0
  24. package/dist/types/tools/tool-result.d.ts +0 -1
  25. package/package.json +8 -8
  26. package/src/async/job-manager.ts +430 -46
  27. package/src/cursor.ts +13 -20
  28. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -3
  29. package/src/eval/backend.ts +2 -2
  30. package/src/exec/bash-executor.ts +151 -425
  31. package/src/gjc-runtime/ultragoal-runtime.ts +3 -280
  32. package/src/gjc-runtime/workflow-manifest.generated.json +1 -40
  33. package/src/gjc-runtime/workflow-manifest.ts +0 -12
  34. package/src/internal-urls/agent-protocol.ts +38 -98
  35. package/src/internal-urls/artifact-protocol.ts +17 -28
  36. package/src/internal-urls/docs-index.generated.ts +1 -1
  37. package/src/internal-urls/registry-helpers.ts +0 -62
  38. package/src/modes/acp/acp-agent.ts +0 -24
  39. package/src/modes/acp/acp-client-bridge.ts +11 -154
  40. package/src/prompts/tools/bash.md +2 -2
  41. package/src/sdk/acp/adapter.ts +9 -200
  42. package/src/sdk/bus/chat-daemon-control.ts +3 -5
  43. package/src/sdk/bus/index.ts +8 -324
  44. package/src/sdk/bus/telegram-daemon-contract.ts +2 -4
  45. package/src/sdk/host/host.ts +9 -187
  46. package/src/sdk/host/reverse-leases.ts +42 -249
  47. package/src/session/agent-session.ts +15 -178
  48. package/src/session/artifacts.ts +74 -211
  49. package/src/session/client-bridge.ts +0 -15
  50. package/src/session/internal/managed-session-storage.ts +5 -104
  51. package/src/session/messages.ts +7 -32
  52. package/src/session/session-manager.ts +21 -12
  53. package/src/session/streaming-output.ts +40 -198
  54. package/src/task/index.ts +230 -64
  55. package/src/task/receipt.ts +2 -4
  56. package/src/task/render.ts +0 -30
  57. package/src/task/types.ts +10 -34
  58. package/src/tools/bash.ts +239 -818
  59. package/src/tools/eval.ts +4 -99
  60. package/src/tools/fetch.ts +15 -77
  61. package/src/tools/find.ts +3 -7
  62. package/src/tools/output-meta.ts +114 -696
  63. package/src/tools/search.ts +4 -9
  64. package/src/tools/subagent.ts +97 -75
  65. package/src/tools/todo-write.ts +47 -0
  66. package/src/tools/tool-result.ts +0 -1
  67. package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +0 -83
  68. package/dist/types/utils/artifact-id.d.ts +0 -2
  69. package/src/gjc-runtime/ultragoal-review-source.ts +0 -378
  70. package/src/utils/artifact-id.ts +0 -6
@@ -349,12 +349,7 @@ import {
349
349
  import { assertEditableFile } from "../tools/auto-generated-guard";
350
350
  import { releaseTabsForOwner } from "../tools/browser/tab-supervisor";
351
351
  import type { CheckpointState } from "../tools/checkpoint";
352
- import {
353
- createArtifactFailurePreview,
354
- type OutputMeta,
355
- outputMeta,
356
- wrapToolWithMetaNotice,
357
- } from "../tools/output-meta";
352
+ import { outputMeta, wrapToolWithMetaNotice } from "../tools/output-meta";
358
353
  import { normalizeLocalScheme, resolveReadPath, resolveToCwd } from "../tools/path-utils";
359
354
  import { registerResourceGcSession } from "../tools/resource-gc";
360
355
  import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo-write";
@@ -1062,102 +1057,6 @@ function boundAgentBashArtifactSaveDiagnostic(error: unknown): string {
1062
1057
  return truncateHeadBytes(normalized, AGENT_BASH_ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES).text;
1063
1058
  }
1064
1059
 
1065
- const PRE_ADMISSION_ARTIFACT_SAVE_WAIT_MS = 500;
1066
- const PRE_ADMISSION_ARTIFACT_MAX_PENDING_SAVES = 2;
1067
-
1068
- type PreAdmissionArtifactSaveOutcome =
1069
- | { kind: "saved"; saved: Awaited<ReturnType<SessionManager["saveArtifactReceipt"]>> }
1070
- | { kind: "failed"; error: unknown };
1071
-
1072
- interface PreAdmissionArtifactSaveAdmission {
1073
- readonly pending: Set<Promise<PreAdmissionArtifactSaveOutcome>>;
1074
- }
1075
-
1076
- /**
1077
- * Pre-admission spills are owned by their concrete SessionManager. A stalled
1078
- * save remains admitted until its promise settles, so timeout does not turn
1079
- * each subsequent tool result into another retained full payload.
1080
- */
1081
- const preAdmissionArtifactSaveAdmissions = new WeakMap<SessionManager, PreAdmissionArtifactSaveAdmission>();
1082
-
1083
- function preAdmissionArtifactSaveAdmissionFor(sessionManager: SessionManager): PreAdmissionArtifactSaveAdmission {
1084
- let admission = preAdmissionArtifactSaveAdmissions.get(sessionManager);
1085
- if (!admission) {
1086
- admission = { pending: new Set() };
1087
- preAdmissionArtifactSaveAdmissions.set(sessionManager, admission);
1088
- }
1089
- return admission;
1090
- }
1091
-
1092
- function beginPreAdmissionArtifactSave(
1093
- sessionManager: SessionManager,
1094
- content: string,
1095
- toolType: string,
1096
- ): { kind: "capacity" } | { kind: "started"; outcome: Promise<PreAdmissionArtifactSaveOutcome> } {
1097
- const admission = preAdmissionArtifactSaveAdmissionFor(sessionManager);
1098
- if (admission.pending.size >= PRE_ADMISSION_ARTIFACT_MAX_PENDING_SAVES) return { kind: "capacity" };
1099
-
1100
- let outcome: Promise<PreAdmissionArtifactSaveOutcome>;
1101
- try {
1102
- outcome = sessionManager.saveArtifactReceipt(content, toolType).then(
1103
- saved => ({ kind: "saved" as const, saved }),
1104
- error => ({ kind: "failed" as const, error }),
1105
- );
1106
- } catch (error) {
1107
- return { kind: "started", outcome: Promise.resolve({ kind: "failed", error }) };
1108
- }
1109
- admission.pending.add(outcome);
1110
- void outcome.then(
1111
- () => admission.pending.delete(outcome),
1112
- () => admission.pending.delete(outcome),
1113
- );
1114
- return { kind: "started", outcome };
1115
- }
1116
-
1117
- function replaceToolResultWithArtifactFailurePreview(
1118
- message: ToolResultMessage,
1119
- fullText: string,
1120
- maxInlineBytes: number | undefined,
1121
- diagnostic: string,
1122
- ): void {
1123
- const preview = createArtifactFailurePreview(fullText, maxInlineBytes, diagnostic);
1124
- const existingDetails =
1125
- message.details && typeof message.details === "object" ? (message.details as Record<string, unknown>) : {};
1126
- const existingMeta =
1127
- existingDetails.meta && typeof existingDetails.meta === "object" ? (existingDetails.meta as OutputMeta) : {};
1128
- const existingTruncation = existingMeta.truncation;
1129
- const totalLines = fullText.length > 0 ? fullText.split("\n").length : 0;
1130
- const outputLines = preview.text.length > 0 ? preview.text.split("\n").length : 0;
1131
- const outputBytes = Buffer.byteLength(preview.text, "utf-8");
1132
- const truncation: NonNullable<OutputMeta["truncation"]> = {
1133
- ...(existingTruncation ?? {}),
1134
- direction: "tail",
1135
- truncatedBy: "bytes",
1136
- noticeOwner: undefined,
1137
- totalLines,
1138
- totalBytes: Buffer.byteLength(fullText, "utf-8"),
1139
- outputLines,
1140
- outputBytes,
1141
- maxBytes: maxInlineBytes,
1142
- shownRange: undefined,
1143
- headRange: undefined,
1144
- tailRange: undefined,
1145
- nextOffset: undefined,
1146
- artifactId: undefined,
1147
- artifactVerified: false,
1148
- sourceCaptureIncomplete: true,
1149
- artifactFailureDiagnostic: diagnostic,
1150
- };
1151
- message.details = {
1152
- ...existingDetails,
1153
- meta: { ...existingMeta, truncation },
1154
- };
1155
- message.content = [
1156
- ...message.content.filter((block): block is ImageContent => block.type === "image"),
1157
- { type: "text", text: preview.text },
1158
- ];
1159
- }
1160
-
1161
1060
  function summarizeAgentBashArtifactSave(
1162
1061
  artifactId: string,
1163
1062
  originalText: string,
@@ -3782,101 +3681,39 @@ export class AgentSession {
3782
3681
  const textParts = message.content.flatMap(block => (block.type === "text" ? [block.text] : []));
3783
3682
  if (textParts.length === 0) return;
3784
3683
  const fullText = textParts.join("\n");
3785
- const configuredInlineKiB = this.settings.get("tools.maxInlineResultBytes");
3786
- const maxInlineBytes =
3787
- configuredInlineKiB > 0 ? Math.max(1024, Math.floor(configuredInlineKiB * 1024)) : undefined;
3788
- const totalBytes = Buffer.byteLength(fullText, "utf-8");
3789
- const exceedsInlineCap = maxInlineBytes !== undefined && totalBytes > maxInlineBytes;
3790
3684
  const contextWindow = this.model?.contextWindow;
3791
3685
  const thresholdTokens = Math.min(
3792
3686
  8_000,
3793
3687
  contextWindow && contextWindow > 0 ? Math.floor(contextWindow * 0.05) : 8_000,
3794
3688
  );
3795
- // The inline byte cap is an independent admission bound and must be checked
3796
- // before token heuristics can opt this result out of spilling.
3797
- if (!exceedsInlineCap && (thresholdTokens <= 0 || estimateTextTokensHeuristic(fullText) <= thresholdTokens))
3798
- return;
3689
+ if (thresholdTokens <= 0 || estimateTextTokensHeuristic(fullText) <= thresholdTokens) return;
3799
3690
 
3800
- const saveAdmission = beginPreAdmissionArtifactSave(this.sessionManager, fullText, "tool-result");
3801
- if (saveAdmission.kind === "capacity") {
3802
- replaceToolResultWithArtifactFailurePreview(
3803
- message,
3804
- fullText,
3805
- maxInlineBytes,
3806
- "artifact save capacity exhausted; the previous save is still settling",
3807
- );
3808
- return;
3809
- }
3810
3691
  try {
3811
- const saveOutcome = await Promise.race([
3812
- saveAdmission.outcome,
3813
- Bun.sleep(PRE_ADMISSION_ARTIFACT_SAVE_WAIT_MS).then(() => ({ kind: "timeout" as const })),
3814
- ]);
3815
- if (saveOutcome.kind !== "saved" || !saveOutcome.saved) {
3816
- const diagnostic =
3817
- saveOutcome.kind === "timeout"
3818
- ? `did not settle within ${PRE_ADMISSION_ARTIFACT_SAVE_WAIT_MS}ms`
3819
- : saveOutcome.kind === "failed"
3820
- ? boundAgentBashArtifactSaveDiagnostic(saveOutcome.error)
3821
- : "artifact storage is unavailable";
3822
- replaceToolResultWithArtifactFailurePreview(message, fullText, maxInlineBytes, diagnostic);
3823
- return;
3824
- }
3825
- const saved = saveOutcome.saved;
3826
- const existingDetails = message.details;
3827
- const detailRecord =
3828
- existingDetails && typeof existingDetails === "object" ? (existingDetails as Record<string, unknown>) : {};
3829
- const existingMeta =
3830
- detailRecord.meta && typeof detailRecord.meta === "object" ? (detailRecord.meta as OutputMeta) : {};
3831
- const existingTruncation = existingMeta.truncation;
3832
- const priorLoss = existingTruncation !== undefined;
3692
+ const artifactId = await this.sessionManager.saveArtifact(fullText, "tool-result");
3693
+ if (!artifactId) return;
3833
3694
  const digest = crypto.createHash("sha256").update(fullText).digest("hex");
3834
- const sourceCaptureIncomplete = priorLoss || !saved.complete || undefined;
3835
- const preview = createPreAdmissionArtifactSpillPreview(fullText, saved.id, digest, {
3836
- maxInlineBytes,
3837
- fullOutput: saved.complete && !priorLoss,
3838
- });
3695
+ const preview = createPreAdmissionArtifactSpillPreview(fullText, artifactId, digest);
3839
3696
  const spillMeta = outputMeta()
3840
3697
  .truncationFromText(preview, {
3841
3698
  direction: "middle",
3842
3699
  totalLines: fullText.split("\n").length,
3843
- totalBytes,
3844
- artifactId: saved.id,
3845
- artifactVerified: true,
3846
- sourceCaptureIncomplete,
3847
- ...(maxInlineBytes !== undefined ? { maxBytes: maxInlineBytes } : {}),
3700
+ totalBytes: Buffer.byteLength(fullText, "utf-8"),
3701
+ artifactId,
3848
3702
  })
3849
3703
  .get();
3850
- const spillTruncation = spillMeta?.truncation;
3851
- const artifactTruncatedBytes =
3852
- Math.max(existingTruncation?.artifactTruncatedBytes ?? 0, saved.omittedBytes ?? 0) || undefined;
3853
- const mergedTruncation = spillTruncation
3854
- ? {
3855
- ...existingTruncation,
3856
- ...spillTruncation,
3857
- artifactId: saved.id,
3858
- artifactVerified: true,
3859
- artifactTruncatedBytes,
3860
- sourceTruncatedBytes: existingTruncation?.sourceTruncatedBytes,
3861
- sourceCaptureIncomplete,
3862
- columnDroppedBytes: existingTruncation?.columnDroppedBytes,
3863
- shownRange: sourceCaptureIncomplete ? undefined : spillTruncation.shownRange,
3864
- headRange: sourceCaptureIncomplete ? undefined : spillTruncation.headRange,
3865
- tailRange: sourceCaptureIncomplete ? undefined : spillTruncation.tailRange,
3866
- nextOffset: sourceCaptureIncomplete ? undefined : spillTruncation.nextOffset,
3867
- }
3868
- : existingTruncation;
3869
- message.details = {
3870
- ...detailRecord,
3871
- meta: { ...existingMeta, ...spillMeta, truncation: mergedTruncation },
3872
- };
3704
+ const existingDetails = message.details;
3705
+ const detailRecord =
3706
+ existingDetails && typeof existingDetails === "object" ? (existingDetails as Record<string, unknown>) : {};
3707
+ const existingMeta =
3708
+ detailRecord.meta && typeof detailRecord.meta === "object"
3709
+ ? (detailRecord.meta as Record<string, unknown>)
3710
+ : {};
3711
+ message.details = { ...detailRecord, meta: { ...existingMeta, ...spillMeta } };
3873
3712
  message.content = [
3874
3713
  ...message.content.filter((block): block is ImageContent => block.type === "image"),
3875
3714
  { type: "text", text: preview },
3876
3715
  ];
3877
3716
  } catch (error) {
3878
- const diagnostic = boundAgentBashArtifactSaveDiagnostic(error);
3879
- replaceToolResultWithArtifactFailurePreview(message, fullText, maxInlineBytes, diagnostic);
3880
3717
  logger.warn("Failed to spill oversized tool result before context admission", {
3881
3718
  toolName: message.toolName,
3882
3719
  error: error instanceof Error ? error.message : String(error),
@@ -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[],