Skip to content

Commit be78b8f

Browse files
authored
🤖 refactor: the subagent artifact index writes take the workspace FIFO from SessionLocks (#7425)
## Summary The report, failure, transcript and git patch artifact index writes take the workspace FIFO from `SessionLocks`. It is PR 14 of phase 4. Lock keys and order do not change: the Node layer gives today's `workspaceFileLocks`. ## Order - PR 13 (#7423), which the plan puts below it, has merged, so the base is main. PR 18 (the branch-summary marker, landing note 15) stacks on it in the native stack. - Phase 5b PR 3 waits for it. ## Implementation - `subagentFailureArtifacts.ts`, `subagentReportArtifacts.ts`, `subagentTranscriptArtifacts.ts`, `subagentGitPatchArtifacts.ts`: each index write takes an optional `locks` (Node default). In `subagentReportArtifacts.ts`, the helper `workspaceFileLocksOf` holds the fallback, so the CC of `upsertSubagentReportArtifact` does not grow. - `workspaceService.ts`: the child-artifact rollup passes `this.historyService.locks` for the parent's index writes. Files beyond the plan's list (landing note 14). The plan said that `taskService.ts`, `gitPatchArtifactService.ts` and `taskGitPatchEngine.ts` do not change. These changes close the gap of a production caller that has a service but keeps the Node default: - `taskService.ts`: the failure and report upserts pass `this.historyService.locks`, and so does `new GitPatchArtifactService(config, locks)`. - `gitPatchArtifactService.ts`: the constructor requires `locks`, so the type checker proves that no caller is left. Its 4 upserts pass them. `gitPatchArtifactService.test.ts` passes `nodeSessionLocks` at its 4 call sites. One test now records the locks that the service takes. - `taskGitPatchEngine.ts`: `TaskGitPatchApplyConfig` picks `sessionLocks`, and the applied watermark write passes it. - `common/utils/tools/tools.ts`: the `sessionLocks` comment names the watermark write. The `task_apply_git_patch` tool gets the locks through its tool config, which `turnRequestBuilder` already sets. - `turnRequestBuilder.ts`, `workflows/WorkflowService.ts`, `cli/workflow.ts`: each workflow patch config sets `sessionLocks` from its history service. `WorkflowServiceContext` gains `historyService: Pick<HistoryService, "locks">`. The ORPC context already has it. `WorkflowService.context.test.ts` adds it to its test context. The 4 artifact functions keep an optional `locks`: a required one breaks about 25 test files. ## Validation On the stack head `c0831c924e` (PR 14 on PR 13's final head `41aab3e5b4`), every gate passed: - `make static-check` and `make lint-eslint` pass. `make crap-check`: PASS, 0 violations, 7 warnings on touched hotspots (total CC of the changed files 10107 -> 10112: the 4 artifact functions each gain one `locks ?? nodeSessionLocks` fallback, and `workspaceFileLocksOf` holds the report one). - `bun test` of the 4 `subagent*Artifacts.test.ts`, `historyService.test.ts`, `taskService.agentReports.test.ts`, `taskService.handoffAndCleanup.test.ts`, `gitPatchArtifactService.test.ts`, `taskGitPatchEngine.test.ts`, `workflows/WorkflowService.context.test.ts`, `workflows/WorkflowTaskServiceAdapter.test.ts` and `tools/task_apply_git_patch.test.ts`: 589 pass, 0 fail. - The tripwire (`scripts/perf/loopTestBaseline.ts --runs 3 --base 41aab3e`): 0 trips. - `make test`: 0 fail in every run. The head `0ae88d7ef7` is the same 5 commits rebased on main `d1f9d61489` (after #7423). Its diff against main has the 14 files above. On this head, `make static-check` passes and the plan tests give 589 pass, 0 fail. Test: `gitPatchArtifactService.test.ts` gives the service a `SessionLocks` whose workspace FIFO records each key, and checks that the parent's index writes take it. Revert proof: with the 4 upserts back on the Node default, that test fails. ## Mutation check `make mutate-check` (base `6887934d00`, so PR 13's lines are in scope too): 167 killed, 128 survived, 4 equivalent, 1 without a test, 682 not run. No survivor is on a line that this PR changes. I list them in #7326 for PR 17. - The 4 equivalents are the `locks ?? nodeSessionLocks` fallbacks, each with a `mutation-equivalent` marker: `locks` is a `SessionLocks` object or undefined, so `??` and `||` agree. - The 1 without a test is `cli/workflow.ts`, and the local baseline reports it as failed. That is a local limit: mutate's worker copies hold only tracked files, and `cli/workflow.test.ts` runs the built `dist/cli/index.js`, which they lack. The test passes in the checkout (10 pass, 0 fail). The PR adds one `sessionLocks` field there, with no operator to mutate. CI's Quality / Mutation is the check of record. - Survivors: `GitPatchArtifactService.generate` 29, `maybeStartGenerationUnlocked` 20, `taskGitPatchEngine.applyProjectPatch` 31 and `applyPatch` 21, `upsertSubagentReportArtifact` 7, `upsertSubagentFailureArtifact` 3, and 5 more in the other artifact functions. PR 13's survivors (`rollbackRefinement` 9, `withTargetMutationLocks` 2, `acquireRollbackFileLock` 1) repeat and are already listed. ## Risks - None for behavior: in the app, `historyService.locks` is `nodeSessionLocks`, which gives the same `workspaceFileLocks` FIFO. --- _Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high` • Cost: `$163.55`_ <!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high costs=163.55 -->
1 parent b4ea1e4 commit be78b8f

14 files changed

Lines changed: 109 additions & 17 deletions

‎src/cli/workflow.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,7 @@ function createWorkflowService(input: {
476476
runtimeTempDir: runtime.normalizePath(".xum/tmp", input.ctx.workspacePath),
477477
workspaceSessionDir,
478478
trusted: input.ctx.projectTrusted,
479+
sessionLocks: input.ctx.services.historyService.locks,
479480
},
480481
}),
481482
resolveWorkflowScript: (scriptPath) =>

‎src/common/utils/tools/tools.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export interface ToolConfiguration {
155155
runtime: Runtime;
156156
/** OS notifications for the notify tool. Absent: the Node layer (configs that tests build by hand). */
157157
desktopNotifications?: DesktopNotifications;
158-
/** Session locks of the todo and review pane writes. Absent: the Node layer (configs that tests build by hand). */
158+
/** Session locks of the todo, review pane and patch-applied watermark writes. Absent: the Node layer (configs that tests build by hand). */
159159
sessionLocks?: SessionLocks;
160160
/** Session files of the todo and review pane state. Absent: the Node layer (configs that tests build by hand). */
161161
sessionFiles?: SessionFiles;

‎src/node/services/gitPatchArtifactService.test.ts‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
upsertSubagentGitPatchArtifact,
1818
} from "@/node/services/subagentGitPatchArtifacts";
1919
import { TestTempDir } from "@/node/services/tools/testHelpers";
20+
import { nodeSessionLocks } from "@/node/services/sessionLocks/nodeSessionLocks";
21+
import type { SessionLocks } from "@/node/services/sessionLocks/sessionLocks";
2022

2123
describe("upsertProjectArtifact", () => {
2224
it("appends unmatched project artifacts instead of dropping them", () => {
@@ -97,7 +99,7 @@ describe("GitPatchArtifactService coordination", () => {
9799
totalCommitCount: 0,
98100
}),
99101
});
100-
const service = new GitPatchArtifactService(config);
102+
const service = new GitPatchArtifactService(config, nodeSessionLocks);
101103
const completed: string[] = [];
102104
const discovery = spyOn(agentDefinitions, "readAgentDefinition");
103105
const upsert = spyOn(patchArtifacts, "upsertSubagentGitPatchArtifact");
@@ -152,7 +154,18 @@ describe("GitPatchArtifactService coordination", () => {
152154
});
153155
return cfg;
154156
});
155-
const service = new GitPatchArtifactService(config);
157+
// The parent's index writes take the injected workspace FIFO, not the Node default.
158+
const lockedWorkspaceIds: string[] = [];
159+
const locks: SessionLocks = {
160+
...nodeSessionLocks,
161+
workspaceFiles: {
162+
withLock: <T>(workspaceId: string, operation: () => Promise<T>) => {
163+
lockedWorkspaceIds.push(workspaceId);
164+
return nodeSessionLocks.workspaceFiles.withLock(workspaceId, operation);
165+
},
166+
},
167+
};
168+
const service = new GitPatchArtifactService(config, locks);
156169
const parentSessionDir = path.join(config.sessionsDir, "parent");
157170
for (const continuation of [false, true]) {
158171
if (continuation) {
@@ -178,11 +191,13 @@ describe("GitPatchArtifactService coordination", () => {
178191
expect(artifact?.projectArtifacts[0]?.headCommitSha).toBe(git("rev-parse", "HEAD"));
179192
expect(completed).toBe(true);
180193
}
194+
expect(lockedWorkspaceIds.length).toBeGreaterThan(0);
195+
expect(new Set(lockedWorkspaceIds)).toEqual(new Set(["parent"]));
181196
});
182197

183198
it("waits for an in-flight apply operation before refreshing the stable task artifact", async () => {
184199
using tempDir = new TestTempDir("git-patch-artifact-lock");
185-
const service = new GitPatchArtifactService(new Config(tempDir.path));
200+
const service = new GitPatchArtifactService(new Config(tempDir.path), nodeSessionLocks);
186201
let releaseApply: (() => void) | undefined;
187202
const applyGate = new Promise<void>((resolve) => {
188203
releaseApply = resolve;
@@ -268,7 +283,7 @@ describe("GitPatchArtifactService coordination", () => {
268283
});
269284

270285
const snapshot = config.loadConfigOrDefault();
271-
const service = new GitPatchArtifactService(config);
286+
const service = new GitPatchArtifactService(config, nodeSessionLocks);
272287
const loadConfigSpy = spyOn(config, "loadConfigOrDefault");
273288
const completedChildIds: string[] = [];
274289
let resolveFreshCompletion: (() => void) | undefined;

‎src/node/services/gitPatchArtifactService.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { shellQuote } from "@/common/utils/shell";
3737
import { getErrorMessage } from "@/common/utils/errors";
3838
import { PlatformPaths } from "@/common/utils/paths";
3939
import { PlatformPaths as NodePlatformPaths } from "@/node/utils/paths.main";
40+
import type { SessionLocks } from "@/node/services/sessionLocks/sessionLocks";
4041
import {
4142
getWorkspaceProjectRepos,
4243
getWorkspaceProjectStorageKeys,
@@ -321,7 +322,11 @@ export class GitPatchArtifactService {
321322
private readonly operationLocks = new MutexMap<string>();
322323
private readonly pendingJobsByTaskId = new Map<string, Promise<void>>();
323324

324-
constructor(private readonly config: Config) {}
325+
constructor(
326+
private readonly config: Config,
327+
/** The workspace FIFO of the parent's patch artifact index writes (phase 4: SessionLocks). */
328+
private readonly locks: SessionLocks
329+
) {}
325330

326331
withOperationLock<T>(childWorkspaceId: string, operation: () => Promise<T>): Promise<T> {
327332
assert(childWorkspaceId.length > 0, "withOperationLock: childWorkspaceId must be non-empty");
@@ -455,6 +460,7 @@ export class GitPatchArtifactService {
455460

456461
const artifact = await upsertSubagentGitPatchArtifact({
457462
workspaceId: parentWorkspaceId,
463+
locks: this.locks,
458464
workspaceSessionDir: parentSessionDir,
459465
childTaskId: childWorkspaceId,
460466
updater: (existing) => {
@@ -504,6 +510,7 @@ export class GitPatchArtifactService {
504510
try {
505511
await upsertSubagentGitPatchArtifact({
506512
workspaceId: parentWorkspaceId,
513+
locks: this.locks,
507514
workspaceSessionDir: parentSessionDir,
508515
childTaskId: childWorkspaceId,
509516
updater: (existing) => {
@@ -538,6 +545,7 @@ export class GitPatchArtifactService {
538545
} catch (error: unknown) {
539546
await upsertSubagentGitPatchArtifact({
540547
workspaceId: parentWorkspaceId,
548+
locks: this.locks,
541549
workspaceSessionDir: parentSessionDir,
542550
childTaskId: childWorkspaceId,
543551
updater: (existing) => {
@@ -590,6 +598,7 @@ export class GitPatchArtifactService {
590598
): Promise<SubagentGitPatchArtifact> => {
591599
return await upsertSubagentGitPatchArtifact({
592600
workspaceId: parentWorkspaceId,
601+
locks: this.locks,
593602
workspaceSessionDir: parentSessionDir,
594603
childTaskId: childWorkspaceId,
595604
updater,

‎src/node/services/subagentFailureArtifacts.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import writeFileAtomic from "@/node/utils/writeFileAtomic";
55

66
import { getErrorMessage } from "@/common/utils/errors";
77
import { log } from "@/node/services/log";
8-
import { workspaceFileLocks } from "@/node/utils/concurrency/workspaceFileLocks";
8+
import { nodeSessionLocks } from "@/node/services/sessionLocks/nodeSessionLocks";
9+
import type { SessionLocks } from "@/node/services/sessionLocks/sessionLocks";
910

1011
/**
1112
* Durable terminal-failure artifacts for sub-agent tasks.
@@ -175,8 +176,12 @@ export async function upsertSubagentFailureArtifact(params: {
175176
workflowOwnedAncestorWorkspaceIds?: string[];
176177
model?: string;
177178
nowMs?: number;
179+
/** The workspace FIFO (phase 4: SessionLocks). Absent: the Node layer. */
180+
locks?: SessionLocks;
178181
}): Promise<void> {
179-
await workspaceFileLocks.withLock(params.workspaceId, async () => {
182+
// mutation-equivalent: locks is a SessionLocks object or undefined, so ?? and || agree
183+
const fileLocks = (params.locks ?? nodeSessionLocks).workspaceFiles;
184+
await fileLocks.withLock(params.workspaceId, async () => {
180185
const nowMs = params.nowMs ?? Date.now();
181186

182187
const file = await readSubagentFailureArtifactsFile(params.workspaceSessionDir);

‎src/node/services/subagentGitPatchArtifacts.ts‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import type {
88
SubagentGitProjectPatchArtifact,
99
} from "@/common/utils/tools/toolDefinitions";
1010
import { log } from "@/node/services/log";
11-
import { workspaceFileLocks } from "@/node/utils/concurrency/workspaceFileLocks";
11+
import { nodeSessionLocks } from "@/node/services/sessionLocks/nodeSessionLocks";
12+
import type { SessionLocks } from "@/node/services/sessionLocks/sessionLocks";
1213

1314
export interface SubagentGitPatchArtifactsFile {
1415
version: 2;
@@ -348,8 +349,12 @@ export async function updateSubagentGitPatchArtifactsFile(params: {
348349
workspaceId: string;
349350
workspaceSessionDir: string;
350351
update: (file: SubagentGitPatchArtifactsFile) => void;
352+
/** The workspace FIFO (phase 4: SessionLocks). Absent: the Node layer. */
353+
locks?: SessionLocks;
351354
}): Promise<SubagentGitPatchArtifactsFile> {
352-
return workspaceFileLocks.withLock(params.workspaceId, async () => {
355+
// mutation-equivalent: locks is a SessionLocks object or undefined, so ?? and || agree
356+
const fileLocks = (params.locks ?? nodeSessionLocks).workspaceFiles;
357+
return fileLocks.withLock(params.workspaceId, async () => {
353358
const file = await readSubagentGitPatchArtifactsFile(params.workspaceSessionDir);
354359
params.update(file);
355360
file.version = SUBAGENT_GIT_PATCH_ARTIFACTS_FILE_VERSION;
@@ -375,12 +380,15 @@ export async function upsertSubagentGitPatchArtifact(params: {
375380
workspaceSessionDir: string;
376381
childTaskId: string;
377382
updater: (existing: SubagentGitPatchArtifact | null) => SubagentGitPatchArtifact;
383+
/** The workspace FIFO (phase 4: SessionLocks). Absent: the Node layer. */
384+
locks?: SessionLocks;
378385
}): Promise<SubagentGitPatchArtifact> {
379386
let updated: SubagentGitPatchArtifact | null = null;
380387

381388
await updateSubagentGitPatchArtifactsFile({
382389
workspaceId: params.workspaceId,
383390
workspaceSessionDir: params.workspaceSessionDir,
391+
locks: params.locks,
384392
update: (file) => {
385393
const existing = file.artifactsByChildTaskId[params.childTaskId] ?? null;
386394
updated = normalizeSubagentGitPatchArtifact(params.updater(existing));
@@ -401,12 +409,15 @@ export async function markSubagentGitPatchArtifactApplied(params: {
401409
childTaskId: string;
402410
projectPath: string;
403411
appliedAtMs: number;
412+
/** The workspace FIFO (phase 4: SessionLocks). Absent: the Node layer. */
413+
locks?: SessionLocks;
404414
}): Promise<SubagentGitPatchArtifact | null> {
405415
let updated: SubagentGitPatchArtifact | null = null;
406416

407417
await updateSubagentGitPatchArtifactsFile({
408418
workspaceId: params.workspaceId,
409419
workspaceSessionDir: params.workspaceSessionDir,
420+
locks: params.locks,
410421
update: (file) => {
411422
const existing = file.artifactsByChildTaskId[params.childTaskId] ?? null;
412423
if (!existing) {

‎src/node/services/subagentReportArtifacts.ts‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import writeFileAtomic from "@/node/utils/writeFileAtomic";
66
import { coerceThinkingLevel, type ThinkingLevel } from "@/common/types/thinking";
77

88
import { log } from "@/node/services/log";
9-
import { workspaceFileLocks } from "@/node/utils/concurrency/workspaceFileLocks";
9+
import { nodeSessionLocks } from "@/node/services/sessionLocks/nodeSessionLocks";
10+
import type { SessionLocks } from "@/node/services/sessionLocks/sessionLocks";
1011

1112
export interface SubagentReportArtifactsFile {
1213
version: 1;
@@ -262,12 +263,21 @@ export async function readSubagentReportArtifactStrict(
262263
}
263264
}
264265

266+
/** The workspace FIFO of the index writes: the caller's SessionLocks (phase 4), else the Node layer. */
267+
function workspaceFileLocksOf(locks: SessionLocks | undefined): SessionLocks["workspaceFiles"] {
268+
// mutation-equivalent: locks is a SessionLocks object or undefined, so ?? and || agree
269+
return (locks ?? nodeSessionLocks).workspaceFiles;
270+
}
271+
265272
export async function updateSubagentReportArtifactsFile(params: {
266273
workspaceId: string;
267274
workspaceSessionDir: string;
268275
update: (file: SubagentReportArtifactsFile) => void;
276+
/** The workspace FIFO (phase 4: SessionLocks). Absent: the Node layer. */
277+
locks?: SessionLocks;
269278
}): Promise<SubagentReportArtifactsFile> {
270-
return workspaceFileLocks.withLock(params.workspaceId, async () => {
279+
const fileLocks = workspaceFileLocksOf(params.locks);
280+
return fileLocks.withLock(params.workspaceId, async () => {
271281
const file = await readSubagentReportArtifactsFile(params.workspaceSessionDir);
272282
params.update(file);
273283

@@ -302,10 +312,13 @@ export async function upsertSubagentReportArtifact(params: {
302312
nowMs?: number;
303313
/** The reporting attempt (see SubagentReportArtifactIndexEntry.taskAttemptId). */
304314
taskAttemptId?: string;
315+
/** The workspace FIFO (phase 4: SessionLocks). Absent: the Node layer. */
316+
locks?: SessionLocks;
305317
}): Promise<SubagentReportArtifactIndexEntry> {
306318
let updated: SubagentReportArtifactIndexEntry | null = null;
307319

308-
await workspaceFileLocks.withLock(params.workspaceId, async () => {
320+
const fileLocks = workspaceFileLocksOf(params.locks);
321+
await fileLocks.withLock(params.workspaceId, async () => {
309322
const nowMs = params.nowMs ?? Date.now();
310323

311324
const model =

‎src/node/services/subagentTranscriptArtifacts.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import writeFileAtomic from "@/node/utils/writeFileAtomic";
66
import type { ThinkingLevel } from "@/common/types/thinking";
77

88
import { log } from "@/node/services/log";
9-
import { workspaceFileLocks } from "@/node/utils/concurrency/workspaceFileLocks";
9+
import { nodeSessionLocks } from "@/node/services/sessionLocks/nodeSessionLocks";
10+
import type { SessionLocks } from "@/node/services/sessionLocks/sessionLocks";
1011

1112
export interface SubagentTranscriptArtifactsFile {
1213
version: 1;
@@ -112,8 +113,12 @@ export async function updateSubagentTranscriptArtifactsFile(params: {
112113
workspaceId: string;
113114
workspaceSessionDir: string;
114115
update: (file: SubagentTranscriptArtifactsFile) => void;
116+
/** The workspace FIFO (phase 4: SessionLocks). Absent: the Node layer. */
117+
locks?: SessionLocks;
115118
}): Promise<SubagentTranscriptArtifactsFile> {
116-
return workspaceFileLocks.withLock(params.workspaceId, async () => {
119+
// mutation-equivalent: locks is a SessionLocks object or undefined, so ?? and || agree
120+
const fileLocks = (params.locks ?? nodeSessionLocks).workspaceFiles;
121+
return fileLocks.withLock(params.workspaceId, async () => {
117122
const file = await readSubagentTranscriptArtifactsFile(params.workspaceSessionDir);
118123
params.update(file);
119124

@@ -137,12 +142,15 @@ export async function upsertSubagentTranscriptArtifactIndexEntry(params: {
137142
updater: (
138143
existing: SubagentTranscriptArtifactIndexEntry | null
139144
) => SubagentTranscriptArtifactIndexEntry;
145+
/** The workspace FIFO (phase 4: SessionLocks). Absent: the Node layer. */
146+
locks?: SessionLocks;
140147
}): Promise<SubagentTranscriptArtifactIndexEntry> {
141148
let updated: SubagentTranscriptArtifactIndexEntry | null = null;
142149

143150
await updateSubagentTranscriptArtifactsFile({
144151
workspaceId: params.workspaceId,
145152
workspaceSessionDir: params.workspaceSessionDir,
153+
locks: params.locks,
146154
update: (file) => {
147155
const existing = file.artifactsByChildTaskId[params.childTaskId] ?? null;
148156
updated = params.updater(existing);

‎src/node/services/taskGitPatchEngine.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { log } from "@/node/services/log";
3030
import { Config } from "@/node/config";
3131
import { coerceNonEmptyString, findWorkspaceEntry } from "@/node/services/taskUtils";
3232
import { getWorkspaceProjectRepos } from "@/node/services/workspaceProjectRepos";
33+
import type { SessionLocks } from "@/node/services/sessionLocks/sessionLocks";
3334

3435
import { streamToString } from "@/node/runtime/streamUtils";
3536
import { nodeRuntimeFactory } from "@/node/services/runtimeFactory/nodeRuntimeFactory";
@@ -39,7 +40,13 @@ export type TaskApplyGitPatchResult = z.infer<typeof TaskApplyGitPatchToolResult
3940

4041
export type TaskGitPatchApplyConfig = Pick<
4142
ToolConfiguration,
42-
"workspaceId" | "cwd" | "runtime" | "runtimeTempDir" | "workspaceSessionDir" | "trusted"
43+
| "workspaceId"
44+
| "cwd"
45+
| "runtime"
46+
| "runtimeTempDir"
47+
| "workspaceSessionDir"
48+
| "trusted"
49+
| "sessionLocks"
4350
>;
4451

4552
export interface GitPatchArtifactLookup {
@@ -1248,6 +1255,8 @@ async function applyProjectPatch(params: {
12481255
expectedHeadSha?: string;
12491256
isReplay: boolean;
12501257
abortSignal?: AbortSignal;
1258+
/** The artifact workspace's FIFO for the applied watermark write. Absent: the Node layer. */
1259+
locks?: SessionLocks;
12511260
}): Promise<{ success: boolean; projectResult: TaskApplyGitPatchProjectResult }> {
12521261
const taskIdError = validatePatchRuntimePathComponent(params.taskId, "task_id");
12531262
const storageKeyError = validatePatchRuntimePathComponent(
@@ -1668,6 +1677,7 @@ async function applyProjectPatch(params: {
16681677
childTaskId: params.taskId,
16691678
projectPath: params.projectArtifact.projectPath,
16701679
appliedAtMs: Date.now(),
1680+
locks: params.locks,
16711681
});
16721682
}
16731683

@@ -1988,6 +1998,7 @@ async function applyPatch(
19881998
expectedHeadSha,
19891999
isReplay,
19902000
abortSignal: options.abortSignal,
2001+
locks: config.sessionLocks,
19912002
});
19922003
projectResults.push(applyResult.projectResult);
19932004
if (!applyResult.success) {

‎src/node/services/taskService.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5212,7 +5212,7 @@ export class TaskService implements AgentTaskIntegration {
52125212
(this.parkedPeerSendsByTarget.get(targetId)?.length ?? 0),
52135213
});
52145214
this.terminalAttentionStore = terminalAttentionStore ?? new TerminalAttentionStore(config);
5215-
this.gitPatchArtifactService = new GitPatchArtifactService(config);
5215+
this.gitPatchArtifactService = new GitPatchArtifactService(config, historyService.locks);
52165216

52175217
this.aiService.on("stream-end", (payload: unknown) => {
52185218
if (!isStreamEndEvent(payload)) return;
@@ -19917,6 +19917,7 @@ export class TaskService implements AgentTaskIntegration {
1991719917
for (const ancestorWorkspaceId of ancestorWorkspaceIds) {
1991819918
try {
1991919919
await upsertSubagentFailureArtifact({
19920+
locks: this.historyService.locks,
1992019921
workspaceId: ancestorWorkspaceId,
1992119922
workspaceSessionDir: path.join(this.config.sessionsDir, ancestorWorkspaceId),
1992219923
childTaskId: workspaceId,
@@ -21400,6 +21401,7 @@ export class TaskService implements AgentTaskIntegration {
2140021401
try {
2140121402
const ancestorSessionDir = path.join(this.config.sessionsDir, ancestorWorkspaceId);
2140221403
await upsertSubagentReportArtifact({
21404+
locks: this.historyService.locks,
2140321405
workspaceId: ancestorWorkspaceId,
2140421406
workspaceSessionDir: ancestorSessionDir,
2140521407
childTaskId: childWorkspaceId,

0 commit comments

Comments
 (0)