@bli-cockpit/cli 0.2.51 → 0.2.53
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/dist/commands/clean.js +244 -0
- package/dist/commands/doctor.js +73 -0
- package/dist/commands/jarvis.js +101 -9
- package/dist/commands/local-args-collector.js +30 -0
- package/dist/commands/local-args.js +3 -1
- package/dist/commands/local-help.js +26 -0
- package/dist/commands/local.js +3 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-attribution.js +55 -0
- package/dist/commands/session-sync-failures.js +140 -0
- package/dist/commands/session-sync-health.js +102 -0
- package/dist/commands/session-sync-plan.js +81 -0
- package/dist/commands/session-sync-record.js +279 -0
- package/dist/commands/session-sync-scan.js +209 -0
- package/dist/commands/session-sync-types.js +12 -0
- package/dist/commands/session-sync-upload.js +215 -0
- package/dist/commands/session-sync.js +44 -987
- package/dist/commands/sync-followups.js +140 -2
- package/dist/commands/sync.js +4 -1
- package/dist/cursors/raw-evidence-reconcile-cursor.js +132 -0
- package/dist/disk-prune.js +246 -0
- package/dist/disk-retention.js +157 -0
- package/dist/disk-usage.js +392 -0
- package/dist/evidence-reconcile-client.js +224 -0
- package/dist/log-rotation.js +106 -2
- package/dist/tower-stream.js +5 -1
- package/package.json +3 -3
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading both session stores, and settling what the reading owes.
|
|
3
|
+
*
|
|
4
|
+
* Codex and Claude are scanned once per tick each, under a window this step
|
|
5
|
+
* chooses: the normal live window, the 14-day first-run backfill, or all local
|
|
6
|
+
* history when a retry is pending. It also holds the BLI-3551 split that a
|
|
7
|
+
* scan's RETRY reason and a scan's FAILURE reason are two different claims —
|
|
8
|
+
* `repo_not_on_disk` widens the next window and never fails a sync.
|
|
9
|
+
*/
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { CODEX_ATTRIBUTION_SCAN_WINDOW_SESSION_LIMIT, CODEX_ATTRIBUTION_SCAN_WINDOW_MINUTES, defaultCodexSessionDirs, scanAndAttributeCodexSessions, } from "../adapters/codex-attribution.js";
|
|
12
|
+
import { scanAndAttributeClaudeSessions, } from "../adapters/claude-attribution.js";
|
|
13
|
+
import { CLAUDE_CURSOR_FILENAME } from "../cursors/raw-evidence-cursor.js";
|
|
14
|
+
import { clearSourceRetryFailure, recordSourceRetryFailure, } from "../spool/local-spool.js";
|
|
15
|
+
import { claudeAttributionReadFailureCount, codexAttributionReadFailureCount, } from "./agent-session-report.js";
|
|
16
|
+
const CLAUDE_FIRST_RUN_BACKFILL_MINUTES = 14 * 24 * 60;
|
|
17
|
+
/**
|
|
18
|
+
* Scan and attribute both session sources, then settle each source's scan-retry
|
|
19
|
+
* bookkeeping against what the scan actually found.
|
|
20
|
+
*
|
|
21
|
+
* Codex first, Claude second, and Claude's first-run window is decided between
|
|
22
|
+
* them because it depends on whether the Claude cursor file exists yet.
|
|
23
|
+
*/
|
|
24
|
+
export async function scanAndAttributeBothSources(options) {
|
|
25
|
+
const { plan } = options;
|
|
26
|
+
const codexAttribution = await scanCodexSessionsForSync({
|
|
27
|
+
homeDir: options.homeDir,
|
|
28
|
+
worktrees: options.worktrees,
|
|
29
|
+
now: options.now,
|
|
30
|
+
collectionRoots: plan.collectionRoots,
|
|
31
|
+
retryPending: plan.codexRetryPending,
|
|
32
|
+
allHistorySinceMinutes: plan.allHistorySinceMinutes,
|
|
33
|
+
});
|
|
34
|
+
// First run (D B.4 §8): without a Claude cursor yet, widen the window to 14
|
|
35
|
+
// days so the first sync captures retroactive history instead of only 24h.
|
|
36
|
+
const claudeCursorExists = await fileExists(path.join(options.paths.cursors_dir, CLAUDE_CURSOR_FILENAME));
|
|
37
|
+
const firstRunBackfill = plan.claudeEnabled && !claudeCursorExists;
|
|
38
|
+
const claudeAttribution = await scanClaudeSessionsForSync({
|
|
39
|
+
claudeEnabled: plan.claudeEnabled,
|
|
40
|
+
homeDir: options.homeDir,
|
|
41
|
+
worktrees: options.worktrees,
|
|
42
|
+
now: options.now,
|
|
43
|
+
collectionRoots: plan.collectionRoots,
|
|
44
|
+
retryPending: plan.claudeRetryPending,
|
|
45
|
+
allHistorySinceMinutes: plan.allHistorySinceMinutes,
|
|
46
|
+
firstRunBackfill,
|
|
47
|
+
});
|
|
48
|
+
await reconcileSourceScanRetry({
|
|
49
|
+
paths: options.paths,
|
|
50
|
+
source: "codex",
|
|
51
|
+
attemptedAt: options.now.toISOString(),
|
|
52
|
+
pendingBefore: plan.codexSourceRetryPending,
|
|
53
|
+
reason: sourceScanRetryReason("codex", codexAttribution),
|
|
54
|
+
});
|
|
55
|
+
if (plan.claudeEnabled) {
|
|
56
|
+
await reconcileSourceScanRetry({
|
|
57
|
+
paths: options.paths,
|
|
58
|
+
source: "claude_code",
|
|
59
|
+
attemptedAt: options.now.toISOString(),
|
|
60
|
+
pendingBefore: plan.claudeSourceRetryPending,
|
|
61
|
+
reason: sourceScanRetryReason("claude_code", claudeAttribution),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return { codexAttribution, claudeAttribution, firstRunBackfill };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Scan and attribute Codex sessions for this sync, widening the window to all
|
|
68
|
+
* local history when a retry is pending. Codex has no first-run backfill
|
|
69
|
+
* concept (that is a Claude-only window, see `scanClaudeSessionsForSync`) and
|
|
70
|
+
* is always scanned, unlike Claude which can be disabled entirely.
|
|
71
|
+
*/
|
|
72
|
+
async function scanCodexSessionsForSync(options) {
|
|
73
|
+
const sinceMinutes = options.retryPending
|
|
74
|
+
? options.allHistorySinceMinutes
|
|
75
|
+
: CODEX_ATTRIBUTION_SCAN_WINDOW_MINUTES;
|
|
76
|
+
let scan = await scanAndAttributeCodexSessions({
|
|
77
|
+
sessionsDirs: defaultCodexSessionDirs(options.homeDir),
|
|
78
|
+
worktrees: options.worktrees,
|
|
79
|
+
now: options.now,
|
|
80
|
+
sinceMinutes,
|
|
81
|
+
limit: CODEX_ATTRIBUTION_SCAN_WINDOW_SESSION_LIMIT,
|
|
82
|
+
collectionRoots: options.collectionRoots,
|
|
83
|
+
});
|
|
84
|
+
if (scan.session_limit_applied) {
|
|
85
|
+
// Re-run once, bounded by exactly what the first pass discovered, so a
|
|
86
|
+
// capped scan still returns every session it found instead of silently
|
|
87
|
+
// truncating at the window's default limit.
|
|
88
|
+
scan = await scanAndAttributeCodexSessions({
|
|
89
|
+
sessionsDirs: defaultCodexSessionDirs(options.homeDir),
|
|
90
|
+
worktrees: options.worktrees,
|
|
91
|
+
now: options.now,
|
|
92
|
+
sinceMinutes,
|
|
93
|
+
limit: scan.discovered_file_count,
|
|
94
|
+
collectionRoots: options.collectionRoots,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return scan;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Scan and attribute Claude sessions for this sync — the Claude counterpart to
|
|
101
|
+
* `scanCodexSessionsForSync`. Disabled collection returns an empty scan
|
|
102
|
+
* up front; a first sync (no cursor yet) widens the window to 14 days instead
|
|
103
|
+
* of the normal 24h so it captures retroactive history.
|
|
104
|
+
*/
|
|
105
|
+
async function scanClaudeSessionsForSync(options) {
|
|
106
|
+
if (!options.claudeEnabled)
|
|
107
|
+
return emptyClaudeScan();
|
|
108
|
+
const sinceMinutes = options.retryPending
|
|
109
|
+
? options.allHistorySinceMinutes
|
|
110
|
+
: options.firstRunBackfill
|
|
111
|
+
? CLAUDE_FIRST_RUN_BACKFILL_MINUTES
|
|
112
|
+
: undefined;
|
|
113
|
+
const projectsDir = path.join(options.homeDir, ".claude", "projects");
|
|
114
|
+
let scan = await scanAndAttributeClaudeSessions({
|
|
115
|
+
projectsDir,
|
|
116
|
+
worktrees: options.worktrees,
|
|
117
|
+
now: options.now,
|
|
118
|
+
collectionRoots: options.collectionRoots,
|
|
119
|
+
sinceMinutes,
|
|
120
|
+
});
|
|
121
|
+
if (scan.session_limit_applied) {
|
|
122
|
+
scan = await scanAndAttributeClaudeSessions({
|
|
123
|
+
projectsDir,
|
|
124
|
+
worktrees: options.worktrees,
|
|
125
|
+
now: options.now,
|
|
126
|
+
collectionRoots: options.collectionRoots,
|
|
127
|
+
sinceMinutes,
|
|
128
|
+
limit: scan.discovered_session_count,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return scan;
|
|
132
|
+
}
|
|
133
|
+
export function sourceScanRetryReason(source, scan) {
|
|
134
|
+
const reasons = new Set();
|
|
135
|
+
const failure = sourceScanFailureReason(source, scan);
|
|
136
|
+
if (failure)
|
|
137
|
+
reasons.add(failure);
|
|
138
|
+
// Kept HERE and nowhere else (BLI-3551): a repo that is not on disk is a
|
|
139
|
+
// reason to widen the next scan window, because the transcript fallback can
|
|
140
|
+
// still attribute it. It is not a reason to call this sync failed — see
|
|
141
|
+
// `sourceScanFailureReason`.
|
|
142
|
+
if (scan.results.some((result) => result.reason === "repo_not_on_disk")) {
|
|
143
|
+
reasons.add("repo_not_on_disk");
|
|
144
|
+
}
|
|
145
|
+
return reasons.size > 0 ? [...reasons].sort().join(",") : null;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The part of the scan outcome that is a genuine FAILURE: the session store
|
|
149
|
+
* itself could not be read, so sessions that exist were not seen.
|
|
150
|
+
*
|
|
151
|
+
* Split from {@link sourceScanRetryReason} in BLI-3551. The two used to be one
|
|
152
|
+
* function, so `repo_not_on_disk` — a label the attribution umbrella finding
|
|
153
|
+
* already established is not a defect (nothing was deleted; the transcript
|
|
154
|
+
* names a path git no longer tracks) — failed the sync on every tick for three
|
|
155
|
+
* operators. A retry hint and a failure are different claims.
|
|
156
|
+
*/
|
|
157
|
+
export function sourceScanFailureReason(source, scan) {
|
|
158
|
+
const readFailureCount = source === "codex"
|
|
159
|
+
? codexAttributionReadFailureCount(scan)
|
|
160
|
+
: claudeAttributionReadFailureCount(scan);
|
|
161
|
+
return readFailureCount > 0 ? `${source}_session_store_read_failed` : null;
|
|
162
|
+
}
|
|
163
|
+
async function reconcileSourceScanRetry(options) {
|
|
164
|
+
if (options.reason) {
|
|
165
|
+
await recordSourceRetryFailure(options.paths, {
|
|
166
|
+
source: options.source,
|
|
167
|
+
reason: options.reason,
|
|
168
|
+
attemptedAt: options.attemptedAt,
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (options.pendingBefore) {
|
|
173
|
+
await clearSourceRetryFailure(options.paths, options.source, options.attemptedAt);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function emptyClaudeScan() {
|
|
177
|
+
return {
|
|
178
|
+
results: [],
|
|
179
|
+
discovered_session_count: 0,
|
|
180
|
+
scanned_session_count: 0,
|
|
181
|
+
since_minutes: 0,
|
|
182
|
+
session_limit: 0,
|
|
183
|
+
session_limit_applied: false,
|
|
184
|
+
max_file_bytes: 0,
|
|
185
|
+
max_sidecar_files: 0,
|
|
186
|
+
max_line_buffer_bytes: 0,
|
|
187
|
+
project_dirs_skipped: 0,
|
|
188
|
+
project_dir_read_failed_count: 0,
|
|
189
|
+
session_stat_failed_count: 0,
|
|
190
|
+
sidecar_dir_read_failed_count: 0,
|
|
191
|
+
sidecar_stat_failed_count: 0,
|
|
192
|
+
disabled_reason: "claude_collection_disabled_by_config",
|
|
193
|
+
counts: {
|
|
194
|
+
attributed: 0,
|
|
195
|
+
attributed_fallback: 0,
|
|
196
|
+
ambiguous: 0,
|
|
197
|
+
unattributed: 0,
|
|
198
|
+
skipped: 0,
|
|
199
|
+
mains_oversized: 0,
|
|
200
|
+
oversized_lines_skipped: 0,
|
|
201
|
+
sessions_schema_drift: 0,
|
|
202
|
+
sidecars_capped: 0,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
async function fileExists(filePath) {
|
|
207
|
+
const { stat } = await import("node:fs/promises");
|
|
208
|
+
return stat(filePath).then(() => true, () => false);
|
|
209
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shapes one attributed sync pass hands from step to step.
|
|
3
|
+
*
|
|
4
|
+
* They live together, away from any step, because each one is a contract
|
|
5
|
+
* BETWEEN two steps: the plan the scan reads, the scan the upload reads, the
|
|
6
|
+
* upload the recorder reads. A step that owned its own output type would make
|
|
7
|
+
* the reader open the producer to learn what the consumer receives.
|
|
8
|
+
*
|
|
9
|
+
* Every name here is re-exported from `./session-sync.js`, which is the address
|
|
10
|
+
* callers already know.
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sending this tick's attributed transcripts, one worktree at a time.
|
|
3
|
+
*
|
|
4
|
+
* The whole loop shares ONE raw-evidence budget (D7b), so a parent-folder sync
|
|
5
|
+
* over many worktrees honours a single byte/object cap rather than N times it,
|
|
6
|
+
* and the Claude growth damper lives here because it is the only step that can
|
|
7
|
+
* see both what the cursor already made durable and what is about to be sent.
|
|
8
|
+
*/
|
|
9
|
+
import { startLocalWorkContext, startLocalWorkContextForAttributedTarget, } from "../local-state.js";
|
|
10
|
+
import { LocalUploadBlockedError, syncLocalAmbientEnvelope, } from "../upload.js";
|
|
11
|
+
import { RAW_EVIDENCE_DEFAULT_BYTE_BUDGET, RAW_EVIDENCE_DEFAULT_OBJECT_BUDGET, } from "../adapters/raw-evidence.js";
|
|
12
|
+
import { liveSyncTargetKey, liveSyncTargetWorktrees, matchesLiveSyncWorktree, } from "./session-sync-attribution.js";
|
|
13
|
+
const CLAUDE_DAMP_GROWTH_BYTES = 256 * 1024;
|
|
14
|
+
const CLAUDE_DAMP_MAX_AGE_MS = 6 * 60 * 60 * 1000;
|
|
15
|
+
/**
|
|
16
|
+
* Sync every worktree this tick is responsible for, in order, under one shared
|
|
17
|
+
* raw-evidence budget.
|
|
18
|
+
*
|
|
19
|
+
* The budget is per-sync (D7b), not per-worktree: a parent-folder sync over
|
|
20
|
+
* many worktrees honors a single byte/object cap rather than N times it.
|
|
21
|
+
*/
|
|
22
|
+
export async function syncEveryTargetWorktree(options) {
|
|
23
|
+
const { run, scan } = options;
|
|
24
|
+
const syncWorktrees = liveSyncTargetWorktrees(run.worktrees, [
|
|
25
|
+
...scan.codexAttribution.results,
|
|
26
|
+
...scan.claudeAttribution.results,
|
|
27
|
+
]);
|
|
28
|
+
const discoveredWorktreeKeys = new Set(run.worktrees.map(liveSyncTargetKey));
|
|
29
|
+
// sessionId -> prior durable pointer for damped sessions (drives the
|
|
30
|
+
// growth_damped count + the skip_main decision).
|
|
31
|
+
const dampedClaudePointers = new Map();
|
|
32
|
+
const rawEvidenceBudget = {
|
|
33
|
+
remainingBytes: RAW_EVIDENCE_DEFAULT_BYTE_BUDGET,
|
|
34
|
+
remainingObjects: RAW_EVIDENCE_DEFAULT_OBJECT_BUDGET,
|
|
35
|
+
};
|
|
36
|
+
const outcomes = [];
|
|
37
|
+
let everyWorktreeUploaded = true;
|
|
38
|
+
for (const worktree of syncWorktrees) {
|
|
39
|
+
const outcome = await syncOneWorktree({
|
|
40
|
+
run,
|
|
41
|
+
now: options.now,
|
|
42
|
+
plan: options.plan,
|
|
43
|
+
scan,
|
|
44
|
+
worktree,
|
|
45
|
+
syncWorktrees,
|
|
46
|
+
rawEvidenceBudget,
|
|
47
|
+
// A target git discovery never produced is one the fallback attribution
|
|
48
|
+
// synthesized, and it syncs through its own attributed work context.
|
|
49
|
+
isDiscoveredWorktree: discoveredWorktreeKeys.has(liveSyncTargetKey(worktree)),
|
|
50
|
+
recordDampedClaudeSession: (sessionId, priorPointer) => {
|
|
51
|
+
dampedClaudePointers.set(sessionId, priorPointer);
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
everyWorktreeUploaded =
|
|
55
|
+
everyWorktreeUploaded && outcome.sync.status === "uploaded";
|
|
56
|
+
outcomes.push(outcome);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
outcomes,
|
|
60
|
+
everyWorktreeUploaded,
|
|
61
|
+
claudePriorDurablePointers: claudeDurablePointersFromCursor(options.plan.claudeCursorBefore),
|
|
62
|
+
growthDampedSessionCount: dampedClaudePointers.size,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Every Claude session the cursor already holds a durable pointer for. */
|
|
66
|
+
function claudeDurablePointersFromCursor(claudeCursorBefore) {
|
|
67
|
+
const pointersBySessionId = new Map();
|
|
68
|
+
for (const [sessionId, entry] of Object.entries(claudeCursorBefore.sessions)) {
|
|
69
|
+
if (entry.uploaded_object_key) {
|
|
70
|
+
pointersBySessionId.set(sessionId, entry.uploaded_object_key);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return pointersBySessionId;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Sync one worktree's attributed transcripts, starting a work context first
|
|
77
|
+
* when this target needs one.
|
|
78
|
+
*
|
|
79
|
+
* A newly cloned repo has no work context yet. Capture is permissive and ticket
|
|
80
|
+
* binding comes later, so a `missing_context` refusal starts general ambient
|
|
81
|
+
* capture for that repo and retries, instead of blocking every other repo's
|
|
82
|
+
* sync until someone runs `cockpit start` by hand.
|
|
83
|
+
*/
|
|
84
|
+
async function syncOneWorktree(options) {
|
|
85
|
+
const { run, worktree } = options;
|
|
86
|
+
const attributedSyntheticTarget = options.isDiscoveredWorktree
|
|
87
|
+
? null
|
|
88
|
+
: worktree;
|
|
89
|
+
const contextOptions = {
|
|
90
|
+
homeDir: run.homeDir,
|
|
91
|
+
repoRoot: worktree.repo_root,
|
|
92
|
+
activeTicketId: run.activeTicketId,
|
|
93
|
+
operatorId: run.operatorId,
|
|
94
|
+
sessionId: run.sessionId,
|
|
95
|
+
...(attributedSyntheticTarget ? {} : { branch: run.branch }),
|
|
96
|
+
};
|
|
97
|
+
const ensureContext = () => attributedSyntheticTarget
|
|
98
|
+
? startLocalWorkContextForAttributedTarget(contextOptions, attributedSyntheticTarget)
|
|
99
|
+
: startLocalWorkContext(contextOptions);
|
|
100
|
+
let context = null;
|
|
101
|
+
if (run.startContexts || attributedSyntheticTarget) {
|
|
102
|
+
context = await ensureContext();
|
|
103
|
+
}
|
|
104
|
+
const syncOptions = ambientEnvelopeForWorktree(options);
|
|
105
|
+
let sync;
|
|
106
|
+
try {
|
|
107
|
+
sync = await syncLocalAmbientEnvelope(syncOptions);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (error instanceof LocalUploadBlockedError &&
|
|
111
|
+
error.blocker === "missing_context") {
|
|
112
|
+
context = await ensureContext();
|
|
113
|
+
sync = await syncLocalAmbientEnvelope(syncOptions);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { worktree, context, sync };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Assemble what one worktree is about to upload: its sibling worktree
|
|
123
|
+
* inventory, its Codex mains, its Claude mains and sidecars, and its share of
|
|
124
|
+
* the sync-wide raw-evidence budget.
|
|
125
|
+
*/
|
|
126
|
+
function ambientEnvelopeForWorktree(options) {
|
|
127
|
+
const { run, worktree, scan } = options;
|
|
128
|
+
const claudeSessionFiles = claudeSessionFilesForWorktree({
|
|
129
|
+
worktree,
|
|
130
|
+
claudeResults: scan.claudeAttribution.results,
|
|
131
|
+
claudeCursorBefore: options.plan.claudeCursorBefore,
|
|
132
|
+
now: options.now,
|
|
133
|
+
recordDampedClaudeSession: options.recordDampedClaudeSession,
|
|
134
|
+
});
|
|
135
|
+
return {
|
|
136
|
+
homeDir: run.homeDir,
|
|
137
|
+
repoRoot: worktree.repo_root,
|
|
138
|
+
dashboardUrl: run.dashboardUrl,
|
|
139
|
+
worktreeInventory: worktreeInventoryForRepo(worktree, options.syncWorktrees),
|
|
140
|
+
codexSessionFiles: scan.codexAttribution.results
|
|
141
|
+
.filter((result) => matchesLiveSyncWorktree(result, worktree))
|
|
142
|
+
.map((result) => ({
|
|
143
|
+
local_path: result.file_path,
|
|
144
|
+
codex_session_id: result.codex_session_id,
|
|
145
|
+
})),
|
|
146
|
+
codexAttributionScan: scan.codexAttribution,
|
|
147
|
+
claudeSessionFiles,
|
|
148
|
+
claudeAttributionScan: scan.claudeAttribution,
|
|
149
|
+
rawEvidenceBudget: options.rawEvidenceBudget,
|
|
150
|
+
fetch: run.fetchImpl,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* This worktree's Claude mains and sidecars, with the growth damper applied.
|
|
155
|
+
*
|
|
156
|
+
* A damped main is still reported — `skip_main` means "do not re-upload the
|
|
157
|
+
* body this tick", not "forget this session" — and its prior durable pointer is
|
|
158
|
+
* recorded so the session report can say `reused_existing` instead of flipping
|
|
159
|
+
* the row to not_uploaded.
|
|
160
|
+
*/
|
|
161
|
+
function claudeSessionFilesForWorktree(options) {
|
|
162
|
+
return options.claudeResults
|
|
163
|
+
.filter((result) => matchesLiveSyncWorktree(result, options.worktree))
|
|
164
|
+
.map((result) => {
|
|
165
|
+
const damped = !result.main_file_oversized &&
|
|
166
|
+
shouldDampClaudeMain(result, options.claudeCursorBefore, options.now);
|
|
167
|
+
if (damped) {
|
|
168
|
+
options.recordDampedClaudeSession(result.claude_session_id, options.claudeCursorBefore.sessions[result.claude_session_id]
|
|
169
|
+
?.uploaded_object_key ?? null);
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
local_path: result.file_path,
|
|
173
|
+
claude_session_id: result.claude_session_id,
|
|
174
|
+
main_file_oversized: result.main_file_oversized,
|
|
175
|
+
skip_main: damped,
|
|
176
|
+
sidecar_files: result.sidecar_files
|
|
177
|
+
.filter((sidecar) => !sidecar.skipped_reason)
|
|
178
|
+
.map((sidecar) => ({ local_path: sidecar.local_path })),
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function shouldDampClaudeMain(result, cursor, now) {
|
|
183
|
+
const entry = cursor.sessions[result.claude_session_id];
|
|
184
|
+
if (!entry ||
|
|
185
|
+
!entry.uploaded_object_key ||
|
|
186
|
+
!entry.uploaded_at ||
|
|
187
|
+
entry.uploaded_byte_size == null) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const grew = result.byte_size > entry.uploaded_byte_size;
|
|
191
|
+
if (!grew)
|
|
192
|
+
return false; // unchanged content reuses via the object cursor
|
|
193
|
+
const growth = result.byte_size - entry.uploaded_byte_size;
|
|
194
|
+
const ageMs = now.getTime() - Date.parse(entry.uploaded_at);
|
|
195
|
+
return (growth <= CLAUDE_DAMP_GROWTH_BYTES &&
|
|
196
|
+
Number.isFinite(ageMs) &&
|
|
197
|
+
ageMs <= CLAUDE_DAMP_MAX_AGE_MS);
|
|
198
|
+
}
|
|
199
|
+
function worktreeInventoryForRepo(current, worktrees) {
|
|
200
|
+
return worktrees
|
|
201
|
+
.filter((worktree) => worktree.repo_fingerprint
|
|
202
|
+
? worktree.repo_fingerprint === current.repo_fingerprint
|
|
203
|
+
: worktree.repo_label === current.repo_label)
|
|
204
|
+
.map((worktree) => ({
|
|
205
|
+
repo: worktree.repo_root,
|
|
206
|
+
repo_label: worktree.repo_label,
|
|
207
|
+
repo_fingerprint: worktree.repo_fingerprint,
|
|
208
|
+
repo_origin_url: worktree.repo_origin_url ?? undefined,
|
|
209
|
+
head_sha: worktree.head_sha ?? undefined,
|
|
210
|
+
worktree_label: worktree.worktree_label,
|
|
211
|
+
worktree_fingerprint: worktree.worktree_fingerprint,
|
|
212
|
+
worktree_is_primary: worktree.worktree_is_primary,
|
|
213
|
+
branch: worktree.branch,
|
|
214
|
+
}));
|
|
215
|
+
}
|