@bli-cockpit/cli 0.2.52 → 0.2.54
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 +58 -5
- package/dist/commands/doctor.js +7 -2
- package/dist/commands/local-args-collector.js +12 -3
- package/dist/commands/local-help.js +12 -4
- 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 +47 -2
- package/dist/cursors/raw-evidence-reconcile-cursor.js +132 -0
- package/dist/disk-usage.js +55 -0
- package/dist/evidence-reconcile-client.js +224 -0
- package/package.json +3 -3
|
@@ -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
|
+
}
|