@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
package/src/task/receipt.ts
CHANGED
|
@@ -39,6 +39,7 @@ export interface TaskResultReceipt {
|
|
|
39
39
|
cost?: number;
|
|
40
40
|
usageCostBreakdownComplete?: true;
|
|
41
41
|
branchName?: string;
|
|
42
|
+
persistence?: SingleResult["persistence"];
|
|
42
43
|
retryFailure?: { attempt: number; errorSummary: string };
|
|
43
44
|
setupFailure?: { summary: string };
|
|
44
45
|
errorSummary?: string;
|
|
@@ -128,6 +129,18 @@ function buildSafeSynopsis(raw: SingleResult, outputRef: TaskResultReceipt["outp
|
|
|
128
129
|
if (raw.retryFailure) {
|
|
129
130
|
return `Task ${status}; retry stopped after attempt ${raw.retryFailure.attempt}.`;
|
|
130
131
|
}
|
|
132
|
+
if (raw.persistence?.outcome === "recovery_available") {
|
|
133
|
+
const recovery = raw.persistence.recoveryRef
|
|
134
|
+
? ` Recovery patch: ${raw.persistence.recoveryRef.uri} (${raw.persistence.recoveryRef.sizeBytes} bytes).`
|
|
135
|
+
: "";
|
|
136
|
+
return `Task ${status}; changes were not persisted to the owner worktree.${recovery}`;
|
|
137
|
+
}
|
|
138
|
+
if (raw.persistence?.outcome === "applied") {
|
|
139
|
+
return `Task ${status}; changes persisted to the owner worktree.`;
|
|
140
|
+
}
|
|
141
|
+
if (raw.persistence?.outcome === "no_changes") {
|
|
142
|
+
return `Task ${status}; no changes to persist.`;
|
|
143
|
+
}
|
|
131
144
|
if (raw.abortReason) {
|
|
132
145
|
return `Task ${status}; abort reason recorded.`;
|
|
133
146
|
}
|
|
@@ -143,7 +156,7 @@ function buildSafeSynopsis(raw: SingleResult, outputRef: TaskResultReceipt["outp
|
|
|
143
156
|
function getStatus(raw: SingleResult): TaskResultReceipt["status"] {
|
|
144
157
|
if (raw.paused) return "paused";
|
|
145
158
|
if (raw.aborted) return "aborted";
|
|
146
|
-
if (raw.exitCode === 0 && raw.error) return "merge_failed";
|
|
159
|
+
if (raw.exitCode === 0 && (raw.error || raw.persistence?.outcome === "recovery_available")) return "merge_failed";
|
|
147
160
|
if (raw.exitCode !== 0 || raw.error) return "failed";
|
|
148
161
|
return "completed";
|
|
149
162
|
}
|
|
@@ -275,11 +288,18 @@ export function buildTaskReceipt(raw: SingleResult): TaskResultReceipt {
|
|
|
275
288
|
usageCostBreakdownComplete:
|
|
276
289
|
raw.usageCostBreakdownComplete === true && hasCompleteUsageCostBreakdown(raw.usage) ? true : undefined,
|
|
277
290
|
branchName: raw.branchName,
|
|
291
|
+
persistence: raw.persistence,
|
|
278
292
|
fastMode: raw.fastMode,
|
|
279
293
|
retryFailure: raw.retryFailure
|
|
280
294
|
? { attempt: raw.retryFailure.attempt, errorSummary: "Retry failure recorded." }
|
|
281
295
|
: undefined,
|
|
282
|
-
errorSummary:
|
|
296
|
+
errorSummary:
|
|
297
|
+
raw.setupFailure?.summary ??
|
|
298
|
+
(raw.error
|
|
299
|
+
? "Error recorded."
|
|
300
|
+
: raw.persistence?.outcome === "recovery_available"
|
|
301
|
+
? "Changes were not persisted to the owner worktree."
|
|
302
|
+
: undefined),
|
|
283
303
|
setupFailure: raw.setupFailure ? { summary: raw.setupFailure.summary } : undefined,
|
|
284
304
|
abortSummary: raw.abortReason ? "Abort reason recorded." : undefined,
|
|
285
305
|
preview,
|
package/src/task/render.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import type { Component } from "@gajae-code/tui";
|
|
9
9
|
import { Text } from "@gajae-code/tui";
|
|
10
|
-
import { formatNumber } from "@gajae-code/utils";
|
|
10
|
+
import { formatNumber, sanitizeText } from "@gajae-code/utils";
|
|
11
11
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
12
12
|
import type { Theme } from "../modes/theme/theme";
|
|
13
13
|
import {
|
|
@@ -917,6 +917,17 @@ function renderAgentResult(result: TaskResultReceipt, isLast: boolean, expanded:
|
|
|
917
917
|
lines.push(`${continuePrefix}${theme.fg("warning", "low ROI: produced no material contribution")}`);
|
|
918
918
|
}
|
|
919
919
|
|
|
920
|
+
if (result.persistence?.recoveryRef) {
|
|
921
|
+
const label = result.persistence.ownerWorktreeApplied ? "Recovery patch" : "Unapplied recovery patch";
|
|
922
|
+
const recoveryUri = truncateToWidth(replaceTabs(sanitizeText(result.persistence.recoveryRef.uri)), 80);
|
|
923
|
+
lines.push(
|
|
924
|
+
`${continuePrefix}${theme.fg(
|
|
925
|
+
result.persistence.ownerWorktreeApplied ? "dim" : "warning",
|
|
926
|
+
`${label}: ${recoveryUri} (${formatBytes(result.persistence.recoveryRef.sizeBytes)})`,
|
|
927
|
+
)}`,
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
920
931
|
if (result.outputRef) {
|
|
921
932
|
lines.push(
|
|
922
933
|
`${continuePrefix}${theme.fg(
|
package/src/task/types.ts
CHANGED
|
@@ -425,6 +425,20 @@ export function createSetupFailureSummary(error: unknown): SetupFailureSummary {
|
|
|
425
425
|
};
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
+
export interface TaskRecoveryArtifactRef {
|
|
429
|
+
uri: string;
|
|
430
|
+
sizeBytes: number;
|
|
431
|
+
sha256: string;
|
|
432
|
+
/** Recovery artifacts remain readable for the parent session lifetime. */
|
|
433
|
+
durability: "session";
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export interface TaskPersistenceResult {
|
|
437
|
+
outcome: "applied" | "no_changes" | "recovery_available";
|
|
438
|
+
ownerWorktreeApplied: boolean;
|
|
439
|
+
recoveryRef?: TaskRecoveryArtifactRef;
|
|
440
|
+
}
|
|
441
|
+
|
|
428
442
|
/** Result from a single agent execution */
|
|
429
443
|
export interface SingleResult {
|
|
430
444
|
index: number;
|
|
@@ -470,6 +484,10 @@ export interface SingleResult {
|
|
|
470
484
|
nestedPatches?: NestedRepoPatch[];
|
|
471
485
|
/** Whether isolated execution produced a non-empty root or nested patch. */
|
|
472
486
|
producedChanges?: boolean;
|
|
487
|
+
/** Receipt-safe owner-worktree persistence result for isolated execution. */
|
|
488
|
+
persistence?: TaskPersistenceResult;
|
|
489
|
+
/** Identity-bound patch artifact captured before isolation cleanup. */
|
|
490
|
+
recoveryRef?: TaskRecoveryArtifactRef;
|
|
473
491
|
/** Data extracted by registered subprocess tool handlers (keyed by tool name) */
|
|
474
492
|
extractedToolData?: Record<string, unknown[]>;
|
|
475
493
|
/** Full wrapper-owned review evidence, kept separate from caller completion data. */
|
package/src/task/worktree.ts
CHANGED
|
@@ -83,13 +83,19 @@ async function captureUntrackedPatch(repoRoot: string, untracked: readonly strin
|
|
|
83
83
|
if (untracked.length === 0) return "";
|
|
84
84
|
const nullPath = getGitNoIndexNullPath();
|
|
85
85
|
const untrackedDiffs = await Promise.all(
|
|
86
|
-
untracked.map(entry =>
|
|
87
|
-
|
|
86
|
+
untracked.map(async entry => {
|
|
87
|
+
const absolutePath = path.join(repoRoot, entry);
|
|
88
|
+
const stat = await fs.lstat(absolutePath);
|
|
89
|
+
if (stat.isFile()) {
|
|
90
|
+
const handle = await fs.open(absolutePath, "r");
|
|
91
|
+
await handle.close();
|
|
92
|
+
}
|
|
93
|
+
return await git.diff(repoRoot, {
|
|
88
94
|
allowFailure: true,
|
|
89
95
|
binary: true,
|
|
90
96
|
noIndex: { left: nullPath, right: entry },
|
|
91
|
-
})
|
|
92
|
-
),
|
|
97
|
+
});
|
|
98
|
+
}),
|
|
93
99
|
);
|
|
94
100
|
return untrackedDiffs.filter(diff => diff.trim()).join("\n");
|
|
95
101
|
}
|
|
@@ -135,26 +141,40 @@ export async function captureBaseline(repoRoot: string): Promise<WorktreeBaselin
|
|
|
135
141
|
return { root, nested };
|
|
136
142
|
}
|
|
137
143
|
|
|
138
|
-
async function
|
|
144
|
+
async function captureRepoCurrentTree(repoDir: string): Promise<string> {
|
|
139
145
|
const currentHead = (await git.head.sha(repoDir)) ?? "";
|
|
140
146
|
const currentStaged = await git.diff(repoDir, { binary: true, cached: true });
|
|
141
147
|
const currentUnstaged = await git.diff(repoDir, { binary: true });
|
|
142
148
|
const currentUntracked = await git.ls.untracked(repoDir);
|
|
143
149
|
const currentUntrackedPatch = await captureUntrackedPatch(repoDir, currentUntracked);
|
|
150
|
+
return await writeSyntheticTree(repoDir, currentHead, [currentStaged, currentUnstaged, currentUntrackedPatch]);
|
|
151
|
+
}
|
|
144
152
|
|
|
153
|
+
async function captureRepoDeltaPatch(repoDir: string, rb: RepoBaseline): Promise<string> {
|
|
145
154
|
const baselineTree = await writeSyntheticTree(repoDir, rb.headCommit, [rb.staged, rb.unstaged, rb.untrackedPatch]);
|
|
146
|
-
const currentTree = await
|
|
147
|
-
currentStaged,
|
|
148
|
-
currentUnstaged,
|
|
149
|
-
currentUntrackedPatch,
|
|
150
|
-
]);
|
|
155
|
+
const currentTree = await captureRepoCurrentTree(repoDir);
|
|
151
156
|
|
|
152
157
|
return git.diff.tree(repoDir, baselineTree, currentTree, {
|
|
153
|
-
allowFailure: true,
|
|
154
158
|
binary: true,
|
|
155
159
|
});
|
|
156
160
|
}
|
|
157
161
|
|
|
162
|
+
/** Confirm that applying patches to the captured owner baseline exactly matches the current owner worktree. */
|
|
163
|
+
export async function verifyRootPatchesApplied(
|
|
164
|
+
repoRoot: string,
|
|
165
|
+
baseline: WorktreeBaseline,
|
|
166
|
+
patches: readonly string[],
|
|
167
|
+
): Promise<boolean> {
|
|
168
|
+
const baselineTree = await writeSyntheticTree(repoRoot, baseline.root.headCommit, [
|
|
169
|
+
baseline.root.staged,
|
|
170
|
+
baseline.root.unstaged,
|
|
171
|
+
baseline.root.untrackedPatch,
|
|
172
|
+
]);
|
|
173
|
+
const expectedTree = await writeSyntheticTree(repoRoot, baselineTree, patches);
|
|
174
|
+
const currentTree = await captureRepoCurrentTree(repoRoot);
|
|
175
|
+
return expectedTree === currentTree;
|
|
176
|
+
}
|
|
177
|
+
|
|
158
178
|
export interface NestedRepoPatch {
|
|
159
179
|
relativePath: string;
|
|
160
180
|
patch: string;
|
|
@@ -163,64 +183,123 @@ export interface NestedRepoPatch {
|
|
|
163
183
|
export interface DeltaPatchResult {
|
|
164
184
|
rootPatch: string;
|
|
165
185
|
nestedPatches: NestedRepoPatch[];
|
|
186
|
+
captureErrors?: string[];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface RecoveryPatchBundle {
|
|
190
|
+
version: 1;
|
|
191
|
+
rootPatch: string;
|
|
192
|
+
nestedPatches: NestedRepoPatch[];
|
|
193
|
+
captureErrors?: string[];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function captureErrorCode(error: unknown): string {
|
|
197
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
198
|
+
return typeof code === "string" && code.length > 0 ? code : "unknown_error";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function serializeRecoveryPatchBundle(delta: DeltaPatchResult): string {
|
|
202
|
+
if (delta.nestedPatches.length === 0 && !delta.captureErrors?.length) return delta.rootPatch;
|
|
203
|
+
return `${JSON.stringify({
|
|
204
|
+
version: 1,
|
|
205
|
+
rootPatch: delta.rootPatch,
|
|
206
|
+
nestedPatches: delta.nestedPatches,
|
|
207
|
+
...(delta.captureErrors?.length ? { captureErrors: delta.captureErrors } : {}),
|
|
208
|
+
} satisfies RecoveryPatchBundle)}\n`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function verifyNestedPatchesApplied(
|
|
212
|
+
repoRoot: string,
|
|
213
|
+
baseline: WorktreeBaseline,
|
|
214
|
+
patches: readonly NestedRepoPatch[],
|
|
215
|
+
): Promise<boolean> {
|
|
216
|
+
const grouped = new Map<string, string[]>();
|
|
217
|
+
for (const patch of patches) {
|
|
218
|
+
const current = grouped.get(patch.relativePath) ?? [];
|
|
219
|
+
current.push(patch.patch);
|
|
220
|
+
grouped.set(patch.relativePath, current);
|
|
221
|
+
}
|
|
222
|
+
for (const [relativePath, repoPatches] of grouped) {
|
|
223
|
+
const nestedBaseline = baseline.nested.find(entry => entry.relativePath === relativePath)?.baseline;
|
|
224
|
+
if (!nestedBaseline) return false;
|
|
225
|
+
const nestedDir = path.join(repoRoot, relativePath);
|
|
226
|
+
const baselineTree = await writeSyntheticTree(nestedDir, nestedBaseline.headCommit, [
|
|
227
|
+
nestedBaseline.staged,
|
|
228
|
+
nestedBaseline.unstaged,
|
|
229
|
+
nestedBaseline.untrackedPatch,
|
|
230
|
+
]);
|
|
231
|
+
const expectedTree = await writeSyntheticTree(nestedDir, baselineTree, repoPatches);
|
|
232
|
+
if ((await captureRepoCurrentTree(nestedDir)) !== expectedTree) return false;
|
|
233
|
+
}
|
|
234
|
+
return true;
|
|
166
235
|
}
|
|
167
236
|
|
|
168
237
|
export async function captureDeltaPatch(isolationDir: string, baseline: WorktreeBaseline): Promise<DeltaPatchResult> {
|
|
169
238
|
const rootPatch = await captureRepoDeltaPatch(isolationDir, baseline.root);
|
|
170
239
|
const nestedPatches: NestedRepoPatch[] = [];
|
|
240
|
+
const captureErrors: string[] = [];
|
|
171
241
|
|
|
172
242
|
for (const { relativePath, baseline: nb } of baseline.nested) {
|
|
173
243
|
const nestedDir = path.join(isolationDir, relativePath);
|
|
174
244
|
try {
|
|
175
245
|
await fs.access(path.join(nestedDir, ".git"));
|
|
176
|
-
|
|
177
|
-
|
|
246
|
+
const patch = await captureRepoDeltaPatch(nestedDir, nb);
|
|
247
|
+
if (patch.trim()) nestedPatches.push({ relativePath, patch });
|
|
248
|
+
} catch (error) {
|
|
249
|
+
captureErrors.push(`Nested repository capture failed (${relativePath}): ${captureErrorCode(error)}`);
|
|
178
250
|
}
|
|
179
|
-
const patch = await captureRepoDeltaPatch(nestedDir, nb);
|
|
180
|
-
if (patch.trim()) nestedPatches.push({ relativePath, patch });
|
|
181
251
|
}
|
|
182
252
|
|
|
183
|
-
return { rootPatch, nestedPatches };
|
|
253
|
+
return { rootPatch, nestedPatches, ...(captureErrors.length ? { captureErrors } : {}) };
|
|
184
254
|
}
|
|
185
255
|
|
|
186
|
-
/**
|
|
187
|
-
|
|
188
|
-
* @param commitMessage Optional async function to generate a commit message from the combined diff.
|
|
189
|
-
* If omitted or returns null, falls back to a generic message.
|
|
190
|
-
*/
|
|
191
|
-
export async function applyNestedPatches(
|
|
192
|
-
repoRoot: string,
|
|
193
|
-
patches: NestedRepoPatch[],
|
|
194
|
-
commitMessage?: (diff: string) => Promise<string | null>,
|
|
195
|
-
): Promise<void> {
|
|
196
|
-
// Group patches by target repo to apply all at once and commit
|
|
256
|
+
/** Preflight every nested delta, then roll back earlier repositories if a later apply fails. */
|
|
257
|
+
export async function applyNestedPatches(repoRoot: string, patches: NestedRepoPatch[]): Promise<void> {
|
|
197
258
|
const byRepo = new Map<string, NestedRepoPatch[]>();
|
|
198
|
-
for (const
|
|
199
|
-
if (!
|
|
200
|
-
const group = byRepo.get(
|
|
201
|
-
group.push(
|
|
202
|
-
byRepo.set(
|
|
259
|
+
for (const patch of patches) {
|
|
260
|
+
if (!patch.patch.trim()) continue;
|
|
261
|
+
const group = byRepo.get(patch.relativePath) ?? [];
|
|
262
|
+
group.push(patch);
|
|
263
|
+
byRepo.set(patch.relativePath, group);
|
|
203
264
|
}
|
|
204
265
|
|
|
205
|
-
|
|
266
|
+
const prepared: Array<{ relativePath: string; nestedDir: string; combinedPatch: string }> = [];
|
|
267
|
+
for (const [relativePath, repoPatches] of [...byRepo.entries()].sort(([left], [right]) =>
|
|
268
|
+
left.localeCompare(right),
|
|
269
|
+
)) {
|
|
206
270
|
const nestedDir = path.join(repoRoot, relativePath);
|
|
207
271
|
try {
|
|
208
272
|
await fs.access(path.join(nestedDir, ".git"));
|
|
209
273
|
} catch {
|
|
210
|
-
|
|
274
|
+
throw new Error(`Nested repository is unavailable: ${relativePath}`);
|
|
211
275
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
await git.patch.applyText(nestedDir, patch);
|
|
276
|
+
const combinedPatch = git.patch.join(repoPatches.map(entry => entry.patch));
|
|
277
|
+
if (!(await git.patch.canApplyText(nestedDir, combinedPatch))) {
|
|
278
|
+
throw new Error(`Nested repository patch does not apply cleanly: ${relativePath}`);
|
|
216
279
|
}
|
|
280
|
+
prepared.push({ relativePath, nestedDir, combinedPatch });
|
|
281
|
+
}
|
|
217
282
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
await git.
|
|
222
|
-
|
|
283
|
+
const applied: typeof prepared = [];
|
|
284
|
+
try {
|
|
285
|
+
for (const entry of prepared) {
|
|
286
|
+
await git.patch.applyText(entry.nestedDir, entry.combinedPatch);
|
|
287
|
+
applied.push(entry);
|
|
288
|
+
}
|
|
289
|
+
} catch (error) {
|
|
290
|
+
const rollbackFailures: string[] = [];
|
|
291
|
+
for (const entry of applied.reverse()) {
|
|
292
|
+
try {
|
|
293
|
+
await git.patch.applyText(entry.nestedDir, entry.combinedPatch, { reverse: true });
|
|
294
|
+
} catch {
|
|
295
|
+
rollbackFailures.push(entry.relativePath);
|
|
296
|
+
}
|
|
223
297
|
}
|
|
298
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
299
|
+
const rollback = rollbackFailures.length
|
|
300
|
+
? `; rollback failed for: ${rollbackFailures.join(", ")}`
|
|
301
|
+
: "; earlier nested patches were rolled back";
|
|
302
|
+
throw new Error(`Nested repository patch application failed: ${message}${rollback}`);
|
|
224
303
|
}
|
|
225
304
|
}
|
|
226
305
|
|
|
@@ -182,12 +182,15 @@ function renderSubagentSnapshotBody(
|
|
|
182
182
|
lines.push(` ${theme.fg("dim", `Agent: ${snapshot.agent} (${snapshot.agentSource})`)}`);
|
|
183
183
|
}
|
|
184
184
|
if (snapshot.effectiveModel) {
|
|
185
|
+
// Fast mode is a property of the tier this model runs under, so the ⚡ glyph
|
|
186
|
+
// belongs on the model line rather than the id.
|
|
187
|
+
const fastSuffix = snapshot.fastMode && theme.icon.fast ? ` ${theme.icon.fast}` : "";
|
|
185
188
|
if (snapshot.modelFellBack && snapshot.requestedModel) {
|
|
186
189
|
lines.push(
|
|
187
|
-
` ${theme.fg("warning", `Model: ${snapshot.effectiveModel} (requested ${snapshot.requestedModel}, fell back — no credentials)`)}`,
|
|
190
|
+
` ${theme.fg("warning", `Model: ${snapshot.effectiveModel} (requested ${snapshot.requestedModel}, fell back — no credentials)`)}${fastSuffix}`,
|
|
188
191
|
);
|
|
189
192
|
} else {
|
|
190
|
-
lines.push(` ${theme.fg("dim", `Model: ${snapshot.effectiveModel}`)}`);
|
|
193
|
+
lines.push(` ${theme.fg("dim", `Model: ${snapshot.effectiveModel}`)}${fastSuffix}`);
|
|
191
194
|
}
|
|
192
195
|
}
|
|
193
196
|
if (snapshot.description) lines.push(` ${theme.fg("dim", `Description: ${snapshot.description}`)}`);
|
package/src/tools/subagent.ts
CHANGED
|
@@ -84,6 +84,8 @@ export interface SubagentSnapshot {
|
|
|
84
84
|
requestedModel?: string;
|
|
85
85
|
/** True when the requested model lacked credentials and fell back to the parent model. */
|
|
86
86
|
modelFellBack?: boolean;
|
|
87
|
+
/** True when the effective subagent provider is in fast mode. */
|
|
88
|
+
fastMode?: boolean;
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
export type SubagentAwaitOutcome = "completed" | "timed_out" | "interrupted";
|
|
@@ -698,6 +700,7 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
|
|
|
698
700
|
if (record.effectiveModel) fields.effectiveModel = record.effectiveModel;
|
|
699
701
|
if (record.requestedModel) fields.requestedModel = record.requestedModel;
|
|
700
702
|
if (record.modelFellBack) fields.modelFellBack = true;
|
|
703
|
+
if (record.fastMode) fields.fastMode = true;
|
|
701
704
|
return fields;
|
|
702
705
|
}
|
|
703
706
|
|
|
@@ -906,6 +909,7 @@ function canonicalizeSnapshotForSignature(snapshot: SubagentSnapshot): unknown {
|
|
|
906
909
|
effectiveModel: snapshot.effectiveModel ?? null,
|
|
907
910
|
requestedModel: snapshot.requestedModel ?? null,
|
|
908
911
|
modelFellBack: snapshot.modelFellBack ?? false,
|
|
912
|
+
fastMode: snapshot.fastMode ?? false,
|
|
909
913
|
// durationMs intentionally excluded (time-derived; would defeat idle gating).
|
|
910
914
|
progress: snapshot.progress ? canonicalizeProgressForSignature(snapshot.progress) : null,
|
|
911
915
|
};
|
|
@@ -933,6 +937,10 @@ function canonicalizeProgressForSignature(progress: AgentProgress): unknown {
|
|
|
933
937
|
cost: progress.cost,
|
|
934
938
|
modelOverride: progress.modelOverride ?? null,
|
|
935
939
|
modelSubstitutionWarning: progress.modelSubstitutionWarning ?? null,
|
|
940
|
+
// The nested task panel renders this, so it must reach the signature or a
|
|
941
|
+
// fast-mode-only change would render differently while comparing byte-identical,
|
|
942
|
+
// suppressing the very update that introduces the glyph.
|
|
943
|
+
fastMode: progress.fastMode ?? false,
|
|
936
944
|
// durationMs intentionally excluded (time-derived).
|
|
937
945
|
extractedToolData: progress.extractedToolData
|
|
938
946
|
? canonicalizeExtractedToolDataForSignature(progress.extractedToolData)
|
package/src/utils/git.ts
CHANGED
|
@@ -85,6 +85,7 @@ export interface PushOptions {
|
|
|
85
85
|
export interface PatchOptions {
|
|
86
86
|
readonly cached?: boolean;
|
|
87
87
|
readonly check?: boolean;
|
|
88
|
+
readonly reverse?: boolean;
|
|
88
89
|
readonly env?: Record<string, string | undefined>;
|
|
89
90
|
readonly signal?: AbortSignal;
|
|
90
91
|
}
|
|
@@ -358,6 +359,7 @@ function buildApplyArgs(patchPath: string, options: PatchOptions): string[] {
|
|
|
358
359
|
const args = ["apply"];
|
|
359
360
|
if (options.check) args.push("--check");
|
|
360
361
|
if (options.cached) args.push("--cached");
|
|
362
|
+
if (options.reverse) args.push("--reverse");
|
|
361
363
|
args.push("--binary", patchPath);
|
|
362
364
|
return args;
|
|
363
365
|
}
|