@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.
- package/CHANGELOG.md +3 -1
- package/dist/types/async/job-manager.d.ts +36 -1
- package/dist/types/eval/backend.d.ts +2 -2
- package/dist/types/exec/bash-executor.d.ts +8 -10
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -36
- package/dist/types/internal-urls/registry-helpers.d.ts +0 -18
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +1 -3
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -4
- package/dist/types/sdk/host/host.d.ts +2 -23
- package/dist/types/sdk/host/reverse-leases.d.ts +4 -7
- package/dist/types/session/artifacts.d.ts +5 -8
- package/dist/types/session/client-bridge.d.ts +0 -14
- package/dist/types/session/internal/managed-session-storage.d.ts +2 -5
- package/dist/types/session/messages.d.ts +1 -4
- package/dist/types/session/session-manager.d.ts +4 -5
- package/dist/types/session/streaming-output.d.ts +0 -32
- package/dist/types/task/receipt.d.ts +1 -2
- package/dist/types/task/types.d.ts +43 -178
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/fetch.d.ts +0 -3
- package/dist/types/tools/output-meta.d.ts +5 -37
- package/dist/types/tools/subagent.d.ts +15 -0
- package/dist/types/tools/todo-write.d.ts +3 -0
- package/dist/types/tools/tool-result.d.ts +0 -1
- package/package.json +8 -8
- package/src/async/job-manager.ts +430 -46
- package/src/cursor.ts +13 -20
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -3
- package/src/eval/backend.ts +2 -2
- package/src/exec/bash-executor.ts +151 -425
- package/src/gjc-runtime/ultragoal-runtime.ts +3 -280
- package/src/gjc-runtime/workflow-manifest.generated.json +1 -40
- package/src/gjc-runtime/workflow-manifest.ts +0 -12
- package/src/internal-urls/agent-protocol.ts +38 -98
- package/src/internal-urls/artifact-protocol.ts +17 -28
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/internal-urls/registry-helpers.ts +0 -62
- package/src/modes/acp/acp-agent.ts +0 -24
- package/src/modes/acp/acp-client-bridge.ts +11 -154
- package/src/prompts/tools/bash.md +2 -2
- package/src/sdk/acp/adapter.ts +9 -200
- package/src/sdk/bus/chat-daemon-control.ts +3 -5
- package/src/sdk/bus/index.ts +8 -324
- package/src/sdk/bus/telegram-daemon-contract.ts +2 -4
- package/src/sdk/host/host.ts +9 -187
- package/src/sdk/host/reverse-leases.ts +42 -249
- package/src/session/agent-session.ts +15 -178
- package/src/session/artifacts.ts +74 -211
- package/src/session/client-bridge.ts +0 -15
- package/src/session/internal/managed-session-storage.ts +5 -104
- package/src/session/messages.ts +7 -32
- package/src/session/session-manager.ts +21 -12
- package/src/session/streaming-output.ts +40 -198
- package/src/task/index.ts +230 -64
- package/src/task/receipt.ts +2 -4
- package/src/task/render.ts +0 -30
- package/src/task/types.ts +10 -34
- package/src/tools/bash.ts +239 -818
- package/src/tools/eval.ts +4 -99
- package/src/tools/fetch.ts +15 -77
- package/src/tools/find.ts +3 -7
- package/src/tools/output-meta.ts +114 -696
- package/src/tools/search.ts +4 -9
- package/src/tools/subagent.ts +97 -75
- package/src/tools/todo-write.ts +47 -0
- package/src/tools/tool-result.ts +0 -1
- package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +0 -83
- package/dist/types/utils/artifact-id.d.ts +0 -2
- package/src/gjc-runtime/ultragoal-review-source.ts +0 -378
- 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
|
-
|
|
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
|
|
3812
|
-
|
|
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
|
|
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
|
|
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
|
|
3851
|
-
const
|
|
3852
|
-
|
|
3853
|
-
const
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
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),
|
package/src/session/artifacts.ts
CHANGED
|
@@ -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.#
|
|
156
|
-
|
|
157
|
-
this.#
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
287
|
-
|
|
208
|
+
const previous = this.#store.readExpected(filename);
|
|
209
|
+
if (previous) this.#store.removeExpected(filename, previous);
|
|
288
210
|
} catch {
|
|
289
|
-
//
|
|
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
|
|
359
|
-
const id =
|
|
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
|
-
/**
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
): Promise<
|
|
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
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
|
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
|
|
445
|
-
|
|
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[],
|