Skip to content

Commit 072fc09

Browse files
refactor(sync): split transcript cursor decisions
1 parent 51c7033 commit 072fc09

4 files changed

Lines changed: 148 additions & 97 deletions

File tree

docs/intentional-architecture-rewrite-2026-06-27/worklog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,3 +592,9 @@ Eighty-sixth production slice:
592592
- Split Codex app-server startup into `src/harness/providers/codex/app-server-session.ts`.
593593
- Moved `initialize`, `thread/start`, `turn/start`, root thread/turn state assignment, provider-session event emission, and prompt/sandbox request construction out of `app-server.ts`.
594594
- Kept `app-server.ts` focused on process lifetime, JSON-RPC transport, request dispatch, notification handling, timeout failure, and cleanup.
595+
596+
Eighty-seventh production slice:
597+
598+
- Split sync transcript snapshot and cursor decision logic into `src/sync/transcript-cursor.ts`.
599+
- Moved transcript file reading, line counting, unchanged/pending/prefix-mismatch decisions, pending ledger entry construction, and failed-entry construction out of `src/sync/sweep.ts`.
600+
- Kept `src/sync/sweep.ts` focused on candidate iteration, eligibility gates, repo locks, ledger loading/reconciliation, Absorb enqueue orchestration, summary recording, and lock cleanup.

src/sync/sweep.ts

Lines changed: 19 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1-
import { readFile } from "node:fs/promises";
2-
31
import type { SessionCandidate, SweepApp } from "./discovery/index.js";
42
import {
3+
type LedgerEntry,
54
type SyncLedger,
6-
countLines,
7-
syncCursor,
85
freshLedgerEntry,
96
ledgerKey,
107
reconcileLedger,
11-
sha256,
128
} from "./ledger.js";
9+
import {
10+
type SyncCursorDecision,
11+
type TranscriptSnapshot,
12+
evaluateSyncCursor,
13+
failedLedgerEntry,
14+
pendingLedgerEntry,
15+
readTranscriptSnapshot,
16+
} from "./transcript-cursor.js";
1317
import {
1418
loadLedgerForRepo,
1519
writeLedger,
@@ -68,21 +72,6 @@ export type StartSyncAbsorbFn = (
6872
args: StartSyncAbsorbArgs,
6973
) => Promise<StartSyncAbsorbResult>;
7074

71-
interface TranscriptSnapshot {
72-
content: Buffer;
73-
currentSize: number;
74-
currentLine: number;
75-
}
76-
77-
type TranscriptReadResult =
78-
| { ok: true; snapshot: TranscriptSnapshot }
79-
| { ok: false; reason: string };
80-
81-
type SyncCursorDecision =
82-
| { kind: "skip"; reason: string }
83-
| { kind: "needs_attention"; reason: string; entry: ReturnType<typeof markPrefixMismatch> }
84-
| { kind: "ready"; fromLine: number; toLine: number };
85-
8675
export async function executeSyncSweep(args: {
8776
candidates: SessionCandidate[];
8877
syncSince: Date | null;
@@ -225,73 +214,16 @@ function candidateEligibility(
225214
return null;
226215
}
227216

228-
async function readTranscriptSnapshot(
229-
candidate: SessionCandidate,
230-
): Promise<TranscriptReadResult> {
231-
try {
232-
const content = await readFile(candidate.transcriptPath);
233-
return {
234-
ok: true,
235-
snapshot: {
236-
content,
237-
currentSize: content.length,
238-
currentLine: countLines(content.toString("utf8")),
239-
},
240-
};
241-
} catch (err: unknown) {
242-
const reason = `read-failed: ${err instanceof Error ? err.message : String(err)}`;
243-
return { ok: false, reason };
244-
}
245-
}
246-
247-
function evaluateSyncCursor(
248-
entry: SyncLedger["sessions"][string],
249-
snapshot: TranscriptSnapshot,
250-
): SyncCursorDecision {
251-
if (entry.status === "pending") {
252-
return { kind: "skip", reason: "sync-already-pending" };
253-
}
254-
255-
if (snapshot.currentSize <= entry.lastAbsorbedSize) {
256-
return { kind: "skip", reason: "unchanged" };
257-
}
258-
259-
const prefixHash = sha256(snapshot.content.subarray(0, entry.lastAbsorbedSize));
260-
if (prefixHash !== entry.lastAbsorbedPrefixHash) {
261-
return {
262-
kind: "needs_attention",
263-
reason: "prefix-mismatch",
264-
entry: markPrefixMismatch(entry),
265-
};
266-
}
267-
268-
return {
269-
kind: "ready",
270-
fromLine: entry.lastAbsorbedLine + 1,
271-
toLine: snapshot.currentLine,
272-
};
273-
}
274-
275-
function markPrefixMismatch(
276-
entry: SyncLedger["sessions"][string],
277-
): SyncLedger["sessions"][string] {
278-
return {
279-
...entry,
280-
status: "needs_attention",
281-
lastError: "transcript prefix no longer matches ledger cursor",
282-
};
283-
}
284-
285217
async function enqueueAbsorb(args: {
286218
candidate: SessionCandidate;
287-
entry: SyncLedger["sessions"][string];
219+
entry: LedgerEntry;
288220
decision: Extract<SyncCursorDecision, { kind: "ready" }>;
289221
snapshot: TranscriptSnapshot;
290222
now: Date;
291223
startAbsorb: StartSyncAbsorbFn;
292224
}): Promise<
293-
| { ok: true; jobId: string; entry: SyncLedger["sessions"][string] }
294-
| { ok: false; reason: string; entry: SyncLedger["sessions"][string] }
225+
| { ok: true; jobId: string; entry: LedgerEntry }
226+
| { ok: false; reason: string; entry: LedgerEntry }
295227
> {
296228
const result = await args.startAbsorb({
297229
candidate: args.candidate,
@@ -306,28 +238,18 @@ async function enqueueAbsorb(args: {
306238
return {
307239
ok: false,
308240
reason: "absorb-start-failed",
309-
entry: {
310-
...args.entry,
311-
status: "failed",
312-
lastError: result.error,
313-
},
241+
entry: failedLedgerEntry(args.entry, result.error),
314242
};
315243
}
316-
const pendingCursor = syncCursor(args.snapshot.content, args.snapshot.currentLine);
317244
return {
318245
ok: true,
319246
jobId: result.jobId,
320-
entry: {
321-
...args.entry,
322-
status: "pending",
323-
pendingToSize: pendingCursor.size,
324-
pendingToLine: pendingCursor.line,
325-
pendingPrefixHash: pendingCursor.prefixHash,
326-
pendingJobId: result.jobId,
327-
pendingStartedAt: args.now.toISOString(),
328-
lastJobId: result.jobId,
329-
lastError: undefined,
330-
},
247+
entry: pendingLedgerEntry({
248+
entry: args.entry,
249+
snapshot: args.snapshot,
250+
jobId: result.jobId,
251+
now: args.now,
252+
}),
331253
};
332254
}
333255

src/sync/transcript-cursor.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { readFile } from "node:fs/promises";
2+
3+
import type { SessionCandidate } from "./discovery/index.js";
4+
import {
5+
type LedgerEntry,
6+
countLines,
7+
sha256,
8+
syncCursor,
9+
} from "./ledger.js";
10+
11+
export interface TranscriptSnapshot {
12+
content: Buffer;
13+
currentSize: number;
14+
currentLine: number;
15+
}
16+
17+
export type TranscriptReadResult =
18+
| { ok: true; snapshot: TranscriptSnapshot }
19+
| { ok: false; reason: string };
20+
21+
export type SyncCursorDecision =
22+
| { kind: "skip"; reason: string }
23+
| { kind: "needs_attention"; reason: string; entry: LedgerEntry }
24+
| { kind: "ready"; fromLine: number; toLine: number };
25+
26+
export async function readTranscriptSnapshot(
27+
candidate: SessionCandidate,
28+
): Promise<TranscriptReadResult> {
29+
try {
30+
const content = await readFile(candidate.transcriptPath);
31+
return {
32+
ok: true,
33+
snapshot: {
34+
content,
35+
currentSize: content.length,
36+
currentLine: countLines(content.toString("utf8")),
37+
},
38+
};
39+
} catch (err: unknown) {
40+
const reason = `read-failed: ${err instanceof Error ? err.message : String(err)}`;
41+
return { ok: false, reason };
42+
}
43+
}
44+
45+
export function evaluateSyncCursor(
46+
entry: LedgerEntry,
47+
snapshot: TranscriptSnapshot,
48+
): SyncCursorDecision {
49+
if (entry.status === "pending") {
50+
return { kind: "skip", reason: "sync-already-pending" };
51+
}
52+
53+
if (snapshot.currentSize <= entry.lastAbsorbedSize) {
54+
return { kind: "skip", reason: "unchanged" };
55+
}
56+
57+
const prefixHash = sha256(snapshot.content.subarray(0, entry.lastAbsorbedSize));
58+
if (prefixHash !== entry.lastAbsorbedPrefixHash) {
59+
return {
60+
kind: "needs_attention",
61+
reason: "prefix-mismatch",
62+
entry: markPrefixMismatch(entry),
63+
};
64+
}
65+
66+
return {
67+
kind: "ready",
68+
fromLine: entry.lastAbsorbedLine + 1,
69+
toLine: snapshot.currentLine,
70+
};
71+
}
72+
73+
export function pendingLedgerEntry(args: {
74+
entry: LedgerEntry;
75+
snapshot: TranscriptSnapshot;
76+
jobId: string;
77+
now: Date;
78+
}): LedgerEntry {
79+
const pendingCursor = syncCursor(args.snapshot.content, args.snapshot.currentLine);
80+
return {
81+
...args.entry,
82+
status: "pending",
83+
pendingToSize: pendingCursor.size,
84+
pendingToLine: pendingCursor.line,
85+
pendingPrefixHash: pendingCursor.prefixHash,
86+
pendingJobId: args.jobId,
87+
pendingStartedAt: args.now.toISOString(),
88+
lastJobId: args.jobId,
89+
lastError: undefined,
90+
};
91+
}
92+
93+
export function failedLedgerEntry(entry: LedgerEntry, error: string): LedgerEntry {
94+
return {
95+
...entry,
96+
status: "failed",
97+
lastError: error,
98+
};
99+
}
100+
101+
function markPrefixMismatch(entry: LedgerEntry): LedgerEntry {
102+
return {
103+
...entry,
104+
status: "needs_attention",
105+
lastError: "transcript prefix no longer matches ledger cursor",
106+
};
107+
}

test/architecture-boundaries.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,22 @@ describe("architecture boundaries", () => {
370370
expect(syncSweep).not.toContain("sync.lock");
371371
});
372372

373+
it("keeps sync transcript cursor decisions out of the sweep coordinator", async () => {
374+
const syncSweep = await readSource("src/sync/sweep.ts");
375+
const transcriptCursor = await readSource("src/sync/transcript-cursor.ts");
376+
377+
expect(existsSync(join(ROOT, "src/sync/transcript-cursor.ts"))).toBe(true);
378+
expect(syncSweep).toContain("transcript-cursor.js");
379+
expect(syncSweep).not.toContain("from \"node:fs/promises\"");
380+
expect(syncSweep).not.toContain("function readTranscriptSnapshot");
381+
expect(syncSweep).not.toContain("function evaluateSyncCursor");
382+
expect(syncSweep).not.toContain("lastAbsorbedPrefixHash");
383+
expect(syncSweep).not.toContain("pendingPrefixHash");
384+
expect(transcriptCursor).toContain("export async function readTranscriptSnapshot");
385+
expect(transcriptCursor).toContain("export function evaluateSyncCursor");
386+
expect(transcriptCursor).toContain("export function pendingLedgerEntry");
387+
});
388+
373389
it("keeps local process signaling in the platform layer", async () => {
374390
const jobsService = await readSource("src/services/jobs/jobs.ts");
375391
const viewerJobs = await readSource("src/viewer/jobs.ts");

0 commit comments

Comments
 (0)