@gajae-code/coding-agent 0.12.4 → 0.12.5
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 +4 -0
- package/dist/types/async/job-manager.d.ts +9 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -2
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +5 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +3 -22
- package/dist/types/session/session-manager.d.ts +1 -1
- package/dist/types/task/executor.d.ts +1 -1
- package/dist/types/task/index.d.ts +2 -1
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/types.d.ts +16 -0
- package/dist/types/task/worktree.d.ts +13 -6
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/utils/git.d.ts +1 -0
- package/package.json +8 -8
- package/src/async/job-manager.ts +13 -5
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/sdk/broker/lifecycle.ts +13 -18
- package/src/sdk/bus/chat-daemon-control.ts +6 -4
- package/src/sdk/bus/telegram-daemon-contract.ts +5 -3
- package/src/sdk/session.ts +1 -0
- package/src/session/internal/managed-session-scope.ts +107 -158
- package/src/session/internal/managed-session-storage.ts +57 -445
- package/src/session/session-manager.ts +12 -7
- package/src/session/session-storage.ts +44 -29
- package/src/task/executor.ts +5 -2
- package/src/task/index.ts +232 -64
- package/src/task/receipt.ts +22 -2
- package/src/task/render.ts +12 -1
- package/src/task/types.ts +18 -0
- package/src/task/worktree.ts +122 -43
- package/src/tools/subagent-render.ts +5 -2
- package/src/tools/subagent.ts +8 -0
- package/src/utils/git.ts +2 -0
|
@@ -9708,6 +9708,7 @@ export class SessionManager {
|
|
|
9708
9708
|
destination: SessionDestination,
|
|
9709
9709
|
store: ManagedSessionDescendantStore,
|
|
9710
9710
|
storage: SessionStorage = new FileSessionStorage(),
|
|
9711
|
+
cwdOverride?: string,
|
|
9711
9712
|
): Promise<SessionManager> {
|
|
9712
9713
|
if (destination.kind !== "managed" || !trustedSessionDestinations.has(destination))
|
|
9713
9714
|
throw new Error("Nested managed session authority is unavailable");
|
|
@@ -9721,16 +9722,20 @@ export class SessionManager {
|
|
|
9721
9722
|
const captured = store.readExpected(path.basename(resolved));
|
|
9722
9723
|
const entries = captured ? parseSessionEntries(captured.bytes.toString("utf8")) : [];
|
|
9723
9724
|
const header = entries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
9724
|
-
const
|
|
9725
|
-
|
|
9726
|
-
|
|
9727
|
-
true,
|
|
9728
|
-
storage,
|
|
9729
|
-
destination,
|
|
9725
|
+
const sessionCwd = cwdOverride ? path.resolve(cwdOverride) : (header?.cwd ?? getProjectDir());
|
|
9726
|
+
const cwdChanged = Boolean(
|
|
9727
|
+
header && cwdOverride && resolveEquivalentPath(header.cwd) !== resolveEquivalentPath(sessionCwd),
|
|
9730
9728
|
);
|
|
9729
|
+
if (header && cwdChanged) header.cwd = sessionCwd;
|
|
9730
|
+
const manager = new SessionManager(sessionCwd, destination.directory, true, storage, destination);
|
|
9731
9731
|
if (entries.length > 0) {
|
|
9732
|
-
const migrationApplied = migrateToCurrentVersion(entries);
|
|
9732
|
+
const migrationApplied = migrateToCurrentVersion(entries) || cwdChanged;
|
|
9733
9733
|
await manager.#hydrateExistingSession(resolved, entries, migrationApplied, "memory-fallback");
|
|
9734
|
+
if (cwdChanged) {
|
|
9735
|
+
await manager.#rewriteFile();
|
|
9736
|
+
manager.#flushed = true;
|
|
9737
|
+
manager.#ensuredOnDisk = true;
|
|
9738
|
+
}
|
|
9734
9739
|
writeTerminalBreadcrumb(manager.cwd, resolved);
|
|
9735
9740
|
await manager.#sanitizeLoadedOpenAIResponsesReplayMetadataAndPersist();
|
|
9736
9741
|
} else {
|
|
@@ -454,7 +454,7 @@ function secureOwnerOnlyFileDescriptor(
|
|
|
454
454
|
): void {
|
|
455
455
|
if (securityContext && !isValidManagedSecurityContext(securityContext))
|
|
456
456
|
throw new Error("Invalid managed session security context");
|
|
457
|
-
if (process.platform !== "linux"
|
|
457
|
+
if (process.platform !== "linux" || !securityContext) {
|
|
458
458
|
if (operation === "apply") {
|
|
459
459
|
const applied = validateNativeSecurityResult(
|
|
460
460
|
native.applyOwnerOnlyPathSecurity(pathname, "file"),
|
|
@@ -471,7 +471,7 @@ function secureOwnerOnlyFileDescriptor(
|
|
|
471
471
|
if (!verified.ok) throw new Error(`Owner-only security rejected ${pathname}: ${verified.code}`);
|
|
472
472
|
return;
|
|
473
473
|
}
|
|
474
|
-
if (
|
|
474
|
+
if (!pathIsWithin(securityContext.sessionDir, pathname))
|
|
475
475
|
throw new Error(`Managed writer escaped its session directory: ${pathname}`);
|
|
476
476
|
const result = validateNativeSecurityResult(
|
|
477
477
|
operation === "apply"
|
|
@@ -908,28 +908,6 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
908
908
|
}
|
|
909
909
|
|
|
910
910
|
const parentIdentity = this.#directoryIdentity(path.dirname(transcriptPath));
|
|
911
|
-
const durablyRecordArtifactPhase = (): void => {
|
|
912
|
-
if (process.platform === "win32") return;
|
|
913
|
-
let descriptor: number | undefined;
|
|
914
|
-
try {
|
|
915
|
-
descriptor = fs.openSync(
|
|
916
|
-
path.dirname(transcriptPath),
|
|
917
|
-
fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW,
|
|
918
|
-
);
|
|
919
|
-
const durableParent = fs.fstatSync(descriptor, { bigint: true });
|
|
920
|
-
if (
|
|
921
|
-
!durableParent.isDirectory() ||
|
|
922
|
-
durableParent.dev !== parentIdentity.dev ||
|
|
923
|
-
durableParent.ino !== parentIdentity.ino
|
|
924
|
-
)
|
|
925
|
-
throw new Error("parent_changed");
|
|
926
|
-
fs.fsyncSync(descriptor);
|
|
927
|
-
} catch (error) {
|
|
928
|
-
throw new SessionDeleteVerificationError("artifacts", "durability_failed", { cause: toError(error) });
|
|
929
|
-
} finally {
|
|
930
|
-
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
931
|
-
}
|
|
932
|
-
};
|
|
933
911
|
if (detachedArtifactsPath) {
|
|
934
912
|
if (
|
|
935
913
|
!expectedArtifactsIdentity ||
|
|
@@ -960,7 +938,6 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
960
938
|
"artifacts",
|
|
961
939
|
"Native artifact removal returned an unauthorized root",
|
|
962
940
|
);
|
|
963
|
-
durablyRecordArtifactPhase();
|
|
964
941
|
return {
|
|
965
942
|
kind: "cleanup_pending",
|
|
966
943
|
phase: "artifacts",
|
|
@@ -1001,7 +978,6 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1001
978
|
"artifacts",
|
|
1002
979
|
"Authorized artifact removal root remains after restart",
|
|
1003
980
|
);
|
|
1004
|
-
durablyRecordArtifactPhase();
|
|
1005
981
|
return { kind: "artifacts_removed", phase: "artifacts", transcriptIdentity };
|
|
1006
982
|
}
|
|
1007
983
|
|
|
@@ -1046,7 +1022,27 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1046
1022
|
`Exact artifact detach rejected: ${detach.ok ? "missing_path" : detach.code}`,
|
|
1047
1023
|
);
|
|
1048
1024
|
}
|
|
1049
|
-
if (!detach.ok)
|
|
1025
|
+
if (!detach.ok && process.platform !== "win32") {
|
|
1026
|
+
let descriptor: number | undefined;
|
|
1027
|
+
try {
|
|
1028
|
+
descriptor = fs.openSync(
|
|
1029
|
+
path.dirname(transcriptPath),
|
|
1030
|
+
fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW,
|
|
1031
|
+
);
|
|
1032
|
+
const durableParent = fs.fstatSync(descriptor, { bigint: true });
|
|
1033
|
+
if (
|
|
1034
|
+
!durableParent.isDirectory() ||
|
|
1035
|
+
durableParent.dev !== parentIdentity.dev ||
|
|
1036
|
+
durableParent.ino !== parentIdentity.ino
|
|
1037
|
+
)
|
|
1038
|
+
throw new Error("parent_changed");
|
|
1039
|
+
fs.fsyncSync(descriptor);
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
throw new SessionDeleteVerificationError("artifacts", "durability_failed", { cause: toError(error) });
|
|
1042
|
+
} finally {
|
|
1043
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1050
1046
|
if (!detach.ok) {
|
|
1051
1047
|
return {
|
|
1052
1048
|
kind: "cleanup_pending",
|
|
@@ -1070,7 +1066,6 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1070
1066
|
"artifacts",
|
|
1071
1067
|
"Native artifact removal returned an unauthorized root",
|
|
1072
1068
|
);
|
|
1073
|
-
durablyRecordArtifactPhase();
|
|
1074
1069
|
return {
|
|
1075
1070
|
kind: "cleanup_pending",
|
|
1076
1071
|
phase: "artifacts",
|
|
@@ -1095,7 +1090,27 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1095
1090
|
}
|
|
1096
1091
|
}
|
|
1097
1092
|
if (!artifactsRemoved) {
|
|
1098
|
-
|
|
1093
|
+
if (process.platform !== "win32") {
|
|
1094
|
+
let descriptor: number | undefined;
|
|
1095
|
+
try {
|
|
1096
|
+
descriptor = fs.openSync(
|
|
1097
|
+
path.dirname(transcriptPath),
|
|
1098
|
+
fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW,
|
|
1099
|
+
);
|
|
1100
|
+
const durableParent = fs.fstatSync(descriptor, { bigint: true });
|
|
1101
|
+
if (
|
|
1102
|
+
!durableParent.isDirectory() ||
|
|
1103
|
+
durableParent.dev !== parentIdentity.dev ||
|
|
1104
|
+
durableParent.ino !== parentIdentity.ino
|
|
1105
|
+
)
|
|
1106
|
+
throw new Error("parent_changed");
|
|
1107
|
+
fs.fsyncSync(descriptor);
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
throw new SessionDeleteVerificationError("artifacts", "durability_failed", { cause: toError(error) });
|
|
1110
|
+
} finally {
|
|
1111
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1099
1114
|
return { kind: "artifacts_removed", phase: "artifacts", transcriptIdentity };
|
|
1100
1115
|
}
|
|
1101
1116
|
if (hasDetachedTranscript) {
|
package/src/task/executor.ts
CHANGED
|
@@ -280,7 +280,7 @@ export class ManagedTaskPersistence {
|
|
|
280
280
|
throw new Error("Managed task persistence authority is unavailable");
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
async openSession(): Promise<SessionManager> {
|
|
283
|
+
async openSession(cwd: string): Promise<SessionManager> {
|
|
284
284
|
const store = this.#artifacts.getManagedStore();
|
|
285
285
|
if (!store) throw new Error("Managed task persistence authority is unavailable");
|
|
286
286
|
this.#artifacts.assertManagedBinding();
|
|
@@ -289,6 +289,8 @@ export class ManagedTaskPersistence {
|
|
|
289
289
|
sessionFile,
|
|
290
290
|
SessionManager.nestedManagedDestination(store, this.#artifacts.dir),
|
|
291
291
|
store,
|
|
292
|
+
undefined,
|
|
293
|
+
cwd,
|
|
292
294
|
);
|
|
293
295
|
this.#artifacts.assertManagedBinding();
|
|
294
296
|
return session;
|
|
@@ -1533,6 +1535,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1533
1535
|
requestedModel: modelSubstitutionWarning?.requested ?? resolvedModelString,
|
|
1534
1536
|
effectiveModel: resolvedModelString,
|
|
1535
1537
|
modelFellBack: authFallbackUsed === true,
|
|
1538
|
+
fastMode: progress.fastMode,
|
|
1536
1539
|
});
|
|
1537
1540
|
}
|
|
1538
1541
|
if (model?.contextWindow && model.contextWindow > 0) {
|
|
@@ -1547,7 +1550,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1547
1550
|
effectiveThinkingLevelForWarning = effectiveThinkingLevel;
|
|
1548
1551
|
|
|
1549
1552
|
const sessionManager = options.managedPersistence
|
|
1550
|
-
? await awaitAbortable(options.managedPersistence.openSession())
|
|
1553
|
+
? await awaitAbortable(options.managedPersistence.openSession(worktree ?? cwd))
|
|
1551
1554
|
: sessionFile
|
|
1552
1555
|
? await awaitAbortable(
|
|
1553
1556
|
SessionManager.open(sessionFile, SessionManager.explicitDestination(path.dirname(sessionFile))),
|
package/src/task/index.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - Progress tracking via JSON events
|
|
13
13
|
* - Session artifacts for debugging
|
|
14
14
|
*/
|
|
15
|
-
import { randomUUID } from "node:crypto";
|
|
15
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
16
16
|
import * as fs from "node:fs/promises";
|
|
17
17
|
import * as os from "node:os";
|
|
18
18
|
import path from "node:path";
|
|
@@ -64,7 +64,7 @@ import { FORK_CONTEXT_TOKEN_BUDGET_BY_MODE } from "./fork-context-budget";
|
|
|
64
64
|
import { getTaskIdValidationError, validateAllocatedTaskId } from "./id";
|
|
65
65
|
import { AgentOutputManager } from "./output-manager";
|
|
66
66
|
import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
|
|
67
|
-
import { assertNoRawTaskFields, buildTaskReceipt, buildTaskRoiSummary } from "./receipt";
|
|
67
|
+
import { assertNoRawTaskFields, buildTaskReceipt, buildTaskRoiSummary, type TaskResultReceipt } from "./receipt";
|
|
68
68
|
|
|
69
69
|
import { renderResult, renderCall as renderTaskCall } from "./render";
|
|
70
70
|
import { reconcileSpawnRoi } from "./roi-reconciliation";
|
|
@@ -82,6 +82,9 @@ import {
|
|
|
82
82
|
type IsolationHandle,
|
|
83
83
|
mergeTaskBranches,
|
|
84
84
|
parseIsolationMode,
|
|
85
|
+
serializeRecoveryPatchBundle,
|
|
86
|
+
verifyNestedPatchesApplied,
|
|
87
|
+
verifyRootPatchesApplied,
|
|
85
88
|
type WorktreeBaseline,
|
|
86
89
|
} from "./worktree";
|
|
87
90
|
|
|
@@ -104,9 +107,22 @@ function renderTaskAssignment(assignment: string, simpleMode: TaskSimpleMode): s
|
|
|
104
107
|
|
|
105
108
|
export function subagentRunOutcomeFromSingleResult(
|
|
106
109
|
finalText: string,
|
|
107
|
-
singleResult:
|
|
110
|
+
singleResult:
|
|
111
|
+
| (Pick<SingleResult, "aborted" | "exitCode" | "paused" | "setupFailure"> &
|
|
112
|
+
Partial<Pick<TaskResultReceipt, "status">>)
|
|
113
|
+
| undefined,
|
|
108
114
|
): string | SubagentRunOutcome {
|
|
109
115
|
if (singleResult?.paused) return { kind: "paused" };
|
|
116
|
+
if (!singleResult) return { kind: "failed", text: finalText };
|
|
117
|
+
if (singleResult?.status && singleResult.status !== "completed") {
|
|
118
|
+
return {
|
|
119
|
+
kind: "failed",
|
|
120
|
+
text: finalText,
|
|
121
|
+
...(singleResult.setupFailure && !singleResult.aborted
|
|
122
|
+
? { setupFailureSummary: singleResult.setupFailure.summary }
|
|
123
|
+
: {}),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
110
126
|
if (singleResult && ((singleResult.aborted ?? false) || singleResult.exitCode !== 0)) {
|
|
111
127
|
return {
|
|
112
128
|
kind: "failed",
|
|
@@ -1042,7 +1058,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1042
1058
|
progress.setupFailure = singleResult?.setupFailure;
|
|
1043
1059
|
}
|
|
1044
1060
|
completedJobs += 1;
|
|
1045
|
-
if (singleResult
|
|
1061
|
+
if (singleResult?.status !== "completed") {
|
|
1046
1062
|
failedJobs += 1;
|
|
1047
1063
|
}
|
|
1048
1064
|
const remaining = taskItems.length - completedJobs;
|
|
@@ -1157,6 +1173,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1157
1173
|
}
|
|
1158
1174
|
}
|
|
1159
1175
|
|
|
1176
|
+
if (failedSchedules.length > 0) {
|
|
1177
|
+
completedJobs += failedSchedules.length;
|
|
1178
|
+
failedJobs += failedSchedules.length;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1160
1181
|
if (startedJobs.length === 0) {
|
|
1161
1182
|
const failureText = `Failed to start background task jobs: ${failedSchedules.join("; ")}`;
|
|
1162
1183
|
return {
|
|
@@ -1165,9 +1186,14 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1165
1186
|
};
|
|
1166
1187
|
}
|
|
1167
1188
|
|
|
1189
|
+
const asyncState = completedJobs === taskItems.length ? (failedJobs > 0 ? "failed" : "completed") : "running";
|
|
1168
1190
|
emitAsyncUpdate(
|
|
1169
|
-
|
|
1170
|
-
|
|
1191
|
+
asyncState,
|
|
1192
|
+
asyncState === "running"
|
|
1193
|
+
? `Launching ${startedJobs.length} background ${startedJobs.length === 1 ? "task" : "tasks"}...`
|
|
1194
|
+
: asyncState === "completed"
|
|
1195
|
+
? `Background task batch complete: ${completedJobs}/${taskItems.length} finished.`
|
|
1196
|
+
: `Background task batch complete with failures: ${failedJobs} failed.`,
|
|
1171
1197
|
);
|
|
1172
1198
|
|
|
1173
1199
|
const scheduleFailureSummary =
|
|
@@ -1196,7 +1222,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1196
1222
|
content: [
|
|
1197
1223
|
{
|
|
1198
1224
|
type: "text",
|
|
1199
|
-
text:
|
|
1225
|
+
text:
|
|
1226
|
+
asyncState === "running"
|
|
1227
|
+
? `Started ${startedJobs.length} background task job${startedJobs.length === 1 ? "" : "s"} using ${params.agent}.${scheduleFailureSummary} Results will be delivered when complete.\n${startedListing}\n${coordinationHint}`
|
|
1228
|
+
: `Background task batch ${asyncState}: ${completedJobs}/${taskItems.length} finished.${scheduleFailureSummary}\n${startedListing}`,
|
|
1200
1229
|
},
|
|
1201
1230
|
],
|
|
1202
1231
|
details: {
|
|
@@ -1204,7 +1233,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1204
1233
|
results: [],
|
|
1205
1234
|
totalDurationMs: 0,
|
|
1206
1235
|
progress: getProgressSnapshot(),
|
|
1207
|
-
async: { state:
|
|
1236
|
+
async: { state: asyncState, jobId: startedJobs[0].jobId, type: "task" },
|
|
1208
1237
|
},
|
|
1209
1238
|
};
|
|
1210
1239
|
}
|
|
@@ -1801,13 +1830,18 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1801
1830
|
parentTelemetry: this.session.getTelemetry?.(),
|
|
1802
1831
|
forkContextSeed,
|
|
1803
1832
|
});
|
|
1804
|
-
|
|
1833
|
+
let capturedResult = {
|
|
1805
1834
|
...result,
|
|
1806
1835
|
...(forkContext ? { forkContext } : {}),
|
|
1807
1836
|
forkContextAdvisory,
|
|
1808
1837
|
repositoryBinding: publicRepositoryBinding(taskRepositoryBinding),
|
|
1809
1838
|
};
|
|
1810
|
-
|
|
1839
|
+
const taskSucceeded =
|
|
1840
|
+
capturedResult.exitCode === 0 &&
|
|
1841
|
+
!capturedResult.error &&
|
|
1842
|
+
!capturedResult.aborted &&
|
|
1843
|
+
!capturedResult.paused;
|
|
1844
|
+
if (mergeMode === "branch" && taskSucceeded) {
|
|
1811
1845
|
try {
|
|
1812
1846
|
const commitMsg =
|
|
1813
1847
|
commitStyle === "ai" && this.session.modelRegistry
|
|
@@ -1828,8 +1862,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1828
1862
|
commitMsg,
|
|
1829
1863
|
);
|
|
1830
1864
|
const producedChanges = Boolean(commitResult?.branchName || commitResult?.nestedPatches.length);
|
|
1831
|
-
|
|
1832
|
-
...
|
|
1865
|
+
capturedResult = {
|
|
1866
|
+
...capturedResult,
|
|
1833
1867
|
branchName: commitResult?.branchName,
|
|
1834
1868
|
nestedPatches: commitResult?.nestedPatches,
|
|
1835
1869
|
producedChanges,
|
|
@@ -1839,34 +1873,51 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1839
1873
|
const branchName = `gjc/task/${task.id}`;
|
|
1840
1874
|
await git.branch.tryDelete(repoRoot, branchName);
|
|
1841
1875
|
const msg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
|
|
1842
|
-
|
|
1876
|
+
capturedResult = { ...capturedResult, error: `Merge failed: ${msg}` };
|
|
1843
1877
|
}
|
|
1844
1878
|
}
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1879
|
+
try {
|
|
1880
|
+
const delta = await captureDeltaPatch(isolationDir, taskBaseline);
|
|
1881
|
+
await initializeLocalRoot(localProtocolOptions);
|
|
1882
|
+
const artifactId = validateAllocatedTaskId(task.id);
|
|
1883
|
+
const patchUri = `local://subagents/${artifactId}-${Snowflake.next()}.patch`;
|
|
1884
|
+
const patchPath = resolveLocalUrlToPath(patchUri, localProtocolOptions);
|
|
1885
|
+
const rootPatchBytes = Buffer.from(delta.rootPatch, "utf8");
|
|
1886
|
+
const recoveryBytes = Buffer.from(serializeRecoveryPatchBundle(delta), "utf8");
|
|
1887
|
+
await fs.mkdir(path.dirname(patchPath), { recursive: true, mode: 0o700 });
|
|
1888
|
+
await Bun.write(patchPath, recoveryBytes);
|
|
1889
|
+
isolatedPatchBytes.set(patchPath, rootPatchBytes);
|
|
1890
|
+
const producedChanges = Boolean(
|
|
1891
|
+
delta.rootPatch.trim() || delta.nestedPatches.length || delta.captureErrors?.length,
|
|
1892
|
+
);
|
|
1893
|
+
const recoveryRef =
|
|
1894
|
+
recoveryBytes.byteLength > 0
|
|
1895
|
+
? {
|
|
1896
|
+
uri: patchUri,
|
|
1897
|
+
sizeBytes: recoveryBytes.byteLength,
|
|
1898
|
+
sha256: createHash("sha256").update(recoveryBytes).digest("hex"),
|
|
1899
|
+
durability: "session" as const,
|
|
1900
|
+
}
|
|
1901
|
+
: undefined;
|
|
1902
|
+
const captureError = delta.captureErrors?.length
|
|
1903
|
+
? `Patch capture incomplete: ${delta.captureErrors.join("; ")}`
|
|
1904
|
+
: undefined;
|
|
1905
|
+
const resultError = captureError
|
|
1906
|
+
? `${capturedResult.error ? `${capturedResult.error}; ` : ""}${captureError}`
|
|
1907
|
+
: capturedResult.error;
|
|
1908
|
+
return {
|
|
1909
|
+
...capturedResult,
|
|
1910
|
+
...(resultError ? { error: resultError } : {}),
|
|
1911
|
+
patchPath,
|
|
1912
|
+
nestedPatches: delta.nestedPatches,
|
|
1913
|
+
producedChanges,
|
|
1914
|
+
recoveryRef,
|
|
1915
|
+
};
|
|
1916
|
+
} catch (patchErr) {
|
|
1917
|
+
const msg = patchErr instanceof Error ? patchErr.message : String(patchErr);
|
|
1918
|
+
const existingError = capturedResult.error ? `${capturedResult.error}; ` : "";
|
|
1919
|
+
return { ...capturedResult, error: `${existingError}Patch capture failed: ${msg}` };
|
|
1868
1920
|
}
|
|
1869
|
-
return resultWithForkContext;
|
|
1870
1921
|
} catch (err) {
|
|
1871
1922
|
const message = err instanceof Error ? err.message : String(err);
|
|
1872
1923
|
const assignment = task.assignment.trim();
|
|
@@ -1966,13 +2017,23 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1966
2017
|
let mergeSummary = "";
|
|
1967
2018
|
let changesApplied: boolean | null = null;
|
|
1968
2019
|
let hadAnyChanges = false;
|
|
2020
|
+
let rootPatchTexts: string[] = [];
|
|
1969
2021
|
let mergedBranchesForNestedPatches: Set<string> | null = null;
|
|
2022
|
+
let mergePhaseFailed = false;
|
|
2023
|
+
const setRecoveryAvailable = (result: SingleResult): void => {
|
|
2024
|
+
if (!result.recoveryRef) return;
|
|
2025
|
+
result.persistence = {
|
|
2026
|
+
outcome: "recovery_available",
|
|
2027
|
+
ownerWorktreeApplied: false,
|
|
2028
|
+
recoveryRef: result.recoveryRef,
|
|
2029
|
+
};
|
|
2030
|
+
};
|
|
1970
2031
|
if (isIsolated && repoRoot) {
|
|
1971
2032
|
try {
|
|
1972
2033
|
if (mergeMode === "branch") {
|
|
1973
2034
|
// Branch mode: merge task branches sequentially
|
|
1974
2035
|
const branchEntries = results
|
|
1975
|
-
.filter(r => r.branchName && r.exitCode === 0 && !r.aborted)
|
|
2036
|
+
.filter(r => r.branchName && r.exitCode === 0 && !r.error && !r.aborted && !r.paused)
|
|
1976
2037
|
.map(r => ({ branchName: r.branchName!, taskId: r.id, description: r.description }));
|
|
1977
2038
|
|
|
1978
2039
|
if (branchEntries.length === 0) {
|
|
@@ -1981,7 +2042,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1981
2042
|
mergeSummary = "\n\nNo changes to apply.";
|
|
1982
2043
|
} else {
|
|
1983
2044
|
const mergeResult = await mergeTaskBranches(repoRoot, branchEntries);
|
|
1984
|
-
mergedBranchesForNestedPatches = new Set(
|
|
2045
|
+
mergedBranchesForNestedPatches = new Set(
|
|
2046
|
+
mergeResult.merged.filter(branchName => !mergeResult.failed.includes(branchName)),
|
|
2047
|
+
);
|
|
1985
2048
|
changesApplied = mergeResult.failed.length === 0;
|
|
1986
2049
|
hadAnyChanges = changesApplied && mergeResult.merged.length > 0;
|
|
1987
2050
|
|
|
@@ -1998,15 +2061,48 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1998
2061
|
}
|
|
1999
2062
|
}
|
|
2000
2063
|
|
|
2064
|
+
for (const result of results) {
|
|
2065
|
+
const hasChanges = Boolean(
|
|
2066
|
+
result.branchName || result.nestedPatches?.length || result.recoveryRef,
|
|
2067
|
+
);
|
|
2068
|
+
if (!hasChanges) {
|
|
2069
|
+
if (result.exitCode === 0 && !result.error && !result.aborted && !result.paused) {
|
|
2070
|
+
result.persistence = { outcome: "no_changes", ownerWorktreeApplied: true };
|
|
2071
|
+
}
|
|
2072
|
+
continue;
|
|
2073
|
+
}
|
|
2074
|
+
if (
|
|
2075
|
+
result.branchName &&
|
|
2076
|
+
mergedBranchesForNestedPatches?.has(result.branchName) &&
|
|
2077
|
+
result.exitCode === 0 &&
|
|
2078
|
+
!result.error &&
|
|
2079
|
+
!result.aborted &&
|
|
2080
|
+
!result.paused
|
|
2081
|
+
) {
|
|
2082
|
+
result.persistence = {
|
|
2083
|
+
outcome: "applied",
|
|
2084
|
+
ownerWorktreeApplied: true,
|
|
2085
|
+
recoveryRef: result.recoveryRef,
|
|
2086
|
+
};
|
|
2087
|
+
} else {
|
|
2088
|
+
setRecoveryAvailable(result);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2001
2092
|
// Clean up merged branches (keep failed ones for manual resolution)
|
|
2002
2093
|
const allBranches = branchEntries.map(b => b.branchName);
|
|
2003
2094
|
if (changesApplied) {
|
|
2004
2095
|
await cleanupTaskBranches(repoRoot, allBranches);
|
|
2005
2096
|
}
|
|
2006
2097
|
} else {
|
|
2007
|
-
// Patch mode: combine and apply patches
|
|
2008
|
-
const
|
|
2009
|
-
|
|
2098
|
+
// Patch mode: combine and apply only successful task patches. Failed/interrupted patches remain recovery-only.
|
|
2099
|
+
const successfulPatchResults = results.filter(
|
|
2100
|
+
result => result.exitCode === 0 && !result.error && !result.aborted && !result.paused,
|
|
2101
|
+
);
|
|
2102
|
+
const patchesInOrder = successfulPatchResults
|
|
2103
|
+
.map(result => result.patchPath)
|
|
2104
|
+
.filter(Boolean) as string[];
|
|
2105
|
+
const missingPatch = successfulPatchResults.some(result => !result.patchPath);
|
|
2010
2106
|
if (missingPatch) {
|
|
2011
2107
|
changesApplied = false;
|
|
2012
2108
|
hadAnyChanges = false;
|
|
@@ -2023,10 +2119,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
2023
2119
|
changesApplied = true;
|
|
2024
2120
|
hadAnyChanges = false;
|
|
2025
2121
|
} else {
|
|
2026
|
-
|
|
2122
|
+
rootPatchTexts = nonEmptyPatches.map(patchPath =>
|
|
2027
2123
|
isolatedPatchBytes.get(patchPath)!.toString("utf8"),
|
|
2028
2124
|
);
|
|
2029
|
-
const combinedPatch =
|
|
2125
|
+
const combinedPatch = rootPatchTexts
|
|
2030
2126
|
.map(text => (text.endsWith("\n") ? text : `${text}\n`))
|
|
2031
2127
|
.join("");
|
|
2032
2128
|
if (!combinedPatch.trim()) {
|
|
@@ -2047,6 +2143,48 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
2047
2143
|
}
|
|
2048
2144
|
}
|
|
2049
2145
|
|
|
2146
|
+
if (changesApplied && rootPatchTexts.length > 0 && baseline) {
|
|
2147
|
+
changesApplied = await verifyRootPatchesApplied(repoRoot, baseline, rootPatchTexts);
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
for (const result of results) {
|
|
2151
|
+
const patchBytes = result.patchPath ? isolatedPatchBytes.get(result.patchPath) : undefined;
|
|
2152
|
+
const hasChanges = Boolean(
|
|
2153
|
+
patchBytes?.toString("utf8").trim() ||
|
|
2154
|
+
(result.nestedPatches && result.nestedPatches.length > 0) ||
|
|
2155
|
+
result.recoveryRef,
|
|
2156
|
+
);
|
|
2157
|
+
if (!hasChanges) {
|
|
2158
|
+
if (result.exitCode === 0 && !result.error && !result.aborted && !result.paused) {
|
|
2159
|
+
result.persistence = {
|
|
2160
|
+
outcome: "no_changes",
|
|
2161
|
+
ownerWorktreeApplied: true,
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
continue;
|
|
2165
|
+
}
|
|
2166
|
+
if (
|
|
2167
|
+
changesApplied &&
|
|
2168
|
+
patchBytes &&
|
|
2169
|
+
result.exitCode === 0 &&
|
|
2170
|
+
!result.error &&
|
|
2171
|
+
!result.aborted &&
|
|
2172
|
+
!result.paused
|
|
2173
|
+
) {
|
|
2174
|
+
result.persistence = {
|
|
2175
|
+
outcome: "applied",
|
|
2176
|
+
ownerWorktreeApplied: true,
|
|
2177
|
+
recoveryRef: result.recoveryRef,
|
|
2178
|
+
};
|
|
2179
|
+
} else {
|
|
2180
|
+
result.persistence = {
|
|
2181
|
+
outcome: "recovery_available",
|
|
2182
|
+
ownerWorktreeApplied: false,
|
|
2183
|
+
recoveryRef: result.recoveryRef,
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2050
2188
|
if (changesApplied) {
|
|
2051
2189
|
mergeSummary = hadAnyChanges ? "\n\nApplied patches: yes" : "\n\nNo changes to apply.";
|
|
2052
2190
|
} else {
|
|
@@ -2063,21 +2201,37 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
2063
2201
|
const msg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
|
|
2064
2202
|
changesApplied = false;
|
|
2065
2203
|
hadAnyChanges = false;
|
|
2204
|
+
mergePhaseFailed = true;
|
|
2205
|
+
for (const result of results) {
|
|
2206
|
+
if (result.producedChanges || result.recoveryRef || result.nestedPatches?.length) {
|
|
2207
|
+
setRecoveryAvailable(result);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2066
2210
|
mergeSummary = `\n\n<system-notification>Merge phase failed: ${msg}\nTask outputs are preserved but changes were not applied.</system-notification>`;
|
|
2067
2211
|
}
|
|
2068
2212
|
}
|
|
2069
2213
|
|
|
2070
2214
|
// Apply nested repo patches (separate from parent git)
|
|
2071
|
-
if (isIsolated && repoRoot && (mergeMode === "branch" || changesApplied !== false)) {
|
|
2215
|
+
if (isIsolated && repoRoot && !mergePhaseFailed && (mergeMode === "branch" || changesApplied !== false)) {
|
|
2072
2216
|
const allNestedPatches = results
|
|
2073
2217
|
.filter(r => {
|
|
2074
|
-
if (
|
|
2218
|
+
if (
|
|
2219
|
+
!r.nestedPatches ||
|
|
2220
|
+
r.nestedPatches.length === 0 ||
|
|
2221
|
+
r.exitCode !== 0 ||
|
|
2222
|
+
r.error ||
|
|
2223
|
+
r.aborted ||
|
|
2224
|
+
r.paused
|
|
2225
|
+
) {
|
|
2075
2226
|
return false;
|
|
2076
2227
|
}
|
|
2077
2228
|
if (mergeMode !== "branch") {
|
|
2078
2229
|
return true;
|
|
2079
2230
|
}
|
|
2080
|
-
if (!r.branchName
|
|
2231
|
+
if (!r.branchName) {
|
|
2232
|
+
return true;
|
|
2233
|
+
}
|
|
2234
|
+
if (!mergedBranchesForNestedPatches) {
|
|
2081
2235
|
return false;
|
|
2082
2236
|
}
|
|
2083
2237
|
return mergedBranchesForNestedPatches.has(r.branchName);
|
|
@@ -2085,32 +2239,46 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
2085
2239
|
.flatMap(r => r.nestedPatches!);
|
|
2086
2240
|
if (allNestedPatches.length > 0) {
|
|
2087
2241
|
try {
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2242
|
+
await applyNestedPatches(repoRoot, allNestedPatches);
|
|
2243
|
+
if (!baseline || !(await verifyNestedPatchesApplied(repoRoot, baseline, allNestedPatches))) {
|
|
2244
|
+
throw new Error("Nested repository persistence proof failed");
|
|
2245
|
+
}
|
|
2246
|
+
for (const result of results) {
|
|
2247
|
+
const rootApplied =
|
|
2248
|
+
mergeMode !== "branch"
|
|
2249
|
+
? changesApplied !== false
|
|
2250
|
+
: !result.branchName || Boolean(mergedBranchesForNestedPatches?.has(result.branchName));
|
|
2251
|
+
if (
|
|
2252
|
+
result.nestedPatches?.length &&
|
|
2253
|
+
result.exitCode === 0 &&
|
|
2254
|
+
!result.error &&
|
|
2255
|
+
!result.aborted &&
|
|
2256
|
+
!result.paused &&
|
|
2257
|
+
rootApplied
|
|
2258
|
+
) {
|
|
2259
|
+
result.persistence = {
|
|
2260
|
+
outcome: "applied",
|
|
2261
|
+
ownerWorktreeApplied: true,
|
|
2262
|
+
recoveryRef: result.recoveryRef,
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2100
2266
|
} catch {
|
|
2101
|
-
|
|
2267
|
+
changesApplied = false;
|
|
2268
|
+
for (const result of results) {
|
|
2269
|
+
if (result.nestedPatches?.length) setRecoveryAvailable(result);
|
|
2270
|
+
}
|
|
2102
2271
|
mergeSummary +=
|
|
2103
|
-
"\n\n<system-notification>Some nested repository patches failed to apply.</system-notification>";
|
|
2272
|
+
"\n\n<system-notification>Some nested repository patches failed to apply; affected tasks remain recovery-only.</system-notification>";
|
|
2104
2273
|
}
|
|
2105
2274
|
}
|
|
2106
2275
|
}
|
|
2107
2276
|
|
|
2108
2277
|
// Build final output - match plugin format
|
|
2109
|
-
const cancelledCount = results.filter(r => r.aborted).length;
|
|
2110
|
-
const successCount = results.filter(r => r.exitCode === 0 && !r.error && !r.aborted).length;
|
|
2111
2278
|
const totalDuration = Date.now() - startTime;
|
|
2112
|
-
|
|
2113
2279
|
const receipts = results.map(buildTaskReceipt);
|
|
2280
|
+
const cancelledCount = receipts.filter(r => r.status === "aborted").length;
|
|
2281
|
+
const successCount = receipts.filter(r => r.status === "completed").length;
|
|
2114
2282
|
const roiSummary = buildTaskRoiSummary(receipts);
|
|
2115
2283
|
const roiReconciliation = executionOverrides?.suppressRoiReconciliation
|
|
2116
2284
|
? undefined
|