@bli-cockpit/cli 0.2.5 → 0.2.8
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/README.md +20 -8
- package/dist/adapters/attribution-core.js +152 -31
- package/dist/adapters/claude-attribution.js +14 -7
- package/dist/adapters/codex-attribution.js +8 -2
- package/dist/adapters/raw-evidence.js +33 -12
- package/dist/agent-rules.js +24 -10
- package/dist/autostart.js +460 -20
- package/dist/commands/backfill.js +582 -87
- package/dist/commands/doctor.js +72 -60
- package/dist/commands/local-args.js +19 -2
- package/dist/commands/local.js +418 -241
- package/dist/commands/public-root.js +6 -1
- package/dist/commands/session-sync.js +259 -54
- package/dist/cursors/backfill-cursor.js +169 -1
- package/dist/cursors/raw-evidence-cursor.js +16 -2
- package/dist/evidence-upload-client.js +139 -107
- package/dist/local-state.js +108 -6
- package/dist/onboarding-roots.js +42 -17
- package/dist/process-runner.js +103 -0
- package/dist/raw-evidence-attribution-policy.js +25 -0
- package/dist/repo-identity.js +142 -26
- package/dist/root-normalization.js +29 -16
- package/dist/spool/local-spool.js +335 -58
- package/dist/upload.js +309 -66
- package/package.json +2 -2
|
@@ -7,6 +7,11 @@ export async function runCockpitCli(argv, io) {
|
|
|
7
7
|
return 0;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
if (command === "--version" || command === "-V" || command === "version") {
|
|
11
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.8");
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
if (rootCommandNames.has(command)) {
|
|
11
16
|
return runLocalCockpitCli(argv, io);
|
|
12
17
|
}
|
|
@@ -31,7 +36,7 @@ function cockpitHelp() {
|
|
|
31
36
|
"Agent setup: `cockpit onboard` refreshes AGENTS.md/CLAUDE.md rules; use `cockpit agent-rules install --workspace ~/BLI` for repair.",
|
|
32
37
|
"Dashboard URL is optional for normal production use; pass `--dashboard-url` only for staging/custom dashboards or forced re-pairing.",
|
|
33
38
|
"Manual collector path: `install`, `login`, `start [--ticket <id>] [--topic <label>] [--intent <intent>] [--phase <phase>]`, `sync`, `status`, `agent-rules`.",
|
|
34
|
-
"Maintainer release path: merge to main
|
|
39
|
+
"Maintainer release path: merge to main, publish to `next` with `cockpit release`, canary Windows plus Apple Silicon, then deliberately promote that exact version to `latest`.",
|
|
35
40
|
].join("\n");
|
|
36
41
|
}
|
|
37
42
|
|
|
@@ -5,24 +5,112 @@
|
|
|
5
5
|
// Behavior-preserving extraction: moved verbatim from local.ts, no logic change.
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import { getCollectorRuntimePaths, startLocalWorkContext,
|
|
8
|
+
import { getCollectorRuntimePaths, readLocalCollectorConfig, startLocalWorkContext, startLocalWorkContextForAttributedTarget, } from "../local-state.js";
|
|
9
9
|
import { CODEX_SESSION_ATTRIBUTION_STATE_RANK, } from "@bli-cockpit/telemetry-core";
|
|
10
|
-
import { LocalUploadBlockedError,
|
|
10
|
+
import { flushPendingCodexSessionReports, LocalUploadBlockedError, queueCodexSessionReport, syncLocalAmbientEnvelope, } from "../upload.js";
|
|
11
11
|
import { CODEX_ATTRIBUTION_SCAN_WINDOW_SESSION_LIMIT, CODEX_ATTRIBUTION_SCAN_WINDOW_MINUTES, defaultCodexSessionDirs, scanAndAttributeCodexSessions, } from "../adapters/codex-attribution.js";
|
|
12
12
|
import { scanAndAttributeClaudeSessions } from "../adapters/claude-attribution.js";
|
|
13
13
|
import { RAW_EVIDENCE_DEFAULT_BYTE_BUDGET, RAW_EVIDENCE_DEFAULT_OBJECT_BUDGET } from "../adapters/raw-evidence.js";
|
|
14
14
|
import { CLAUDE_CURSOR_FILENAME, countStaleSessions, emptyRawEvidenceCursorState, readRawEvidenceCursor, recordSessionObservation, writeRawEvidenceCursor } from "../cursors/raw-evidence-cursor.js";
|
|
15
15
|
import { normalizeCollectionRoots } from "../root-normalization.js";
|
|
16
|
+
import { clearSourceRetryFailure, readLocalUploadSpoolState, recordSourceRetryFailure, } from "../spool/local-spool.js";
|
|
17
|
+
import { isLiveRawEvidenceSyncAttribution, } from "../raw-evidence-attribution-policy.js";
|
|
16
18
|
const CLAUDE_FIRST_RUN_BACKFILL_MINUTES = 14 * 24 * 60;
|
|
17
19
|
const CLAUDE_DAMP_GROWTH_BYTES = 256 * 1024;
|
|
18
20
|
const CLAUDE_DAMP_MAX_AGE_MS = 6 * 60 * 60 * 1000;
|
|
21
|
+
/**
|
|
22
|
+
* Live sync remains reason-allowlisted even though historical backfill accepts
|
|
23
|
+
* every deterministic fallback state.
|
|
24
|
+
*/
|
|
25
|
+
export function isLiveSyncCollectableAttributionState(state, reason) {
|
|
26
|
+
return isLiveRawEvidenceSyncAttribution(state, reason);
|
|
27
|
+
}
|
|
28
|
+
function cursorHasUndurableCollectableSession(cursor) {
|
|
29
|
+
return Object.values(cursor.sessions).some((entry) => liveSyncCursorEntryRequiresRetry(entry));
|
|
30
|
+
}
|
|
31
|
+
export function liveSyncCursorEntryRequiresRetry(entry) {
|
|
32
|
+
return (!entry.uploaded_object_key &&
|
|
33
|
+
(isLiveSyncCollectableAttributionState(entry.state, entry.reason) ||
|
|
34
|
+
entry.reason === "repo_not_on_disk"));
|
|
35
|
+
}
|
|
36
|
+
function allLocalHistorySinceMinutes(now) {
|
|
37
|
+
// A cutoff just before the Unix epoch is effectively unbounded for Codex and
|
|
38
|
+
// Claude session files while keeping date arithmetic finite and portable.
|
|
39
|
+
return Math.ceil(now.getTime() / 60_000) + 24 * 60;
|
|
40
|
+
}
|
|
41
|
+
function codexAttributionReadFailureCount(scan) {
|
|
42
|
+
return scan.directory_read_failed_count + scan.stat_failed_count;
|
|
43
|
+
}
|
|
44
|
+
function claudeAttributionReadFailureCount(scan) {
|
|
45
|
+
return (scan.project_dir_read_failed_count +
|
|
46
|
+
scan.session_stat_failed_count +
|
|
47
|
+
scan.sidecar_dir_read_failed_count +
|
|
48
|
+
scan.sidecar_stat_failed_count);
|
|
49
|
+
}
|
|
50
|
+
export function sourceScanRetryReason(source, scan) {
|
|
51
|
+
const reasons = new Set();
|
|
52
|
+
const readFailureCount = source === "codex"
|
|
53
|
+
? codexAttributionReadFailureCount(scan)
|
|
54
|
+
: claudeAttributionReadFailureCount(scan);
|
|
55
|
+
if (readFailureCount > 0) {
|
|
56
|
+
reasons.add(`${source}_session_store_read_failed`);
|
|
57
|
+
}
|
|
58
|
+
if (scan.results.some((result) => result.reason === "repo_not_on_disk")) {
|
|
59
|
+
reasons.add("repo_not_on_disk");
|
|
60
|
+
}
|
|
61
|
+
return reasons.size > 0 ? [...reasons].sort().join(",") : null;
|
|
62
|
+
}
|
|
63
|
+
async function reconcileSourceScanRetry(options) {
|
|
64
|
+
if (options.reason) {
|
|
65
|
+
await recordSourceRetryFailure(options.paths, {
|
|
66
|
+
source: options.source,
|
|
67
|
+
reason: options.reason,
|
|
68
|
+
attemptedAt: options.attemptedAt,
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (options.pendingBefore) {
|
|
73
|
+
await clearSourceRetryFailure(options.paths, options.source, options.attemptedAt);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function matchesLiveSyncWorktree(result, target) {
|
|
77
|
+
return (isLiveRawEvidenceSyncAttribution(result.state, result.reason) &&
|
|
78
|
+
result.worktree !== null &&
|
|
79
|
+
liveSyncTargetKey(result.worktree) === liveSyncTargetKey(target));
|
|
80
|
+
}
|
|
81
|
+
function liveSyncTargetKey(worktree) {
|
|
82
|
+
return `${worktree.repo_fingerprint}:${worktree.worktree_fingerprint}`;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Folder and deleted-repo fallbacks can intentionally synthesize a worktree
|
|
86
|
+
* identity that is not present in git discovery. Include those identities as
|
|
87
|
+
* sync targets so a wrapper-root session is not observed and then stranded.
|
|
88
|
+
*/
|
|
89
|
+
export function liveSyncTargetWorktrees(discovered, results) {
|
|
90
|
+
const targets = [...discovered];
|
|
91
|
+
const seen = new Set(discovered.map(liveSyncTargetKey));
|
|
92
|
+
for (const result of results) {
|
|
93
|
+
const targetKey = result.worktree
|
|
94
|
+
? liveSyncTargetKey(result.worktree)
|
|
95
|
+
: null;
|
|
96
|
+
if (!isLiveSyncCollectableAttributionState(result.state, result.reason) ||
|
|
97
|
+
!result.worktree ||
|
|
98
|
+
!targetKey ||
|
|
99
|
+
seen.has(targetKey)) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
seen.add(targetKey);
|
|
103
|
+
targets.push(result.worktree);
|
|
104
|
+
}
|
|
105
|
+
return targets;
|
|
106
|
+
}
|
|
19
107
|
/**
|
|
20
108
|
* Shared dual-source sync orchestration for single-repo and parent-folder
|
|
21
109
|
* modes. Codex AND Claude Code sessions are scanned and attributed once across
|
|
22
|
-
* every discovered worktree; each worktree syncs
|
|
23
|
-
* transcripts (
|
|
24
|
-
* ambiguous/unattributed/skipped remainder is reported with reason labels and
|
|
25
|
-
* `source` discriminator instead of being duplicated into every repo or
|
|
110
|
+
* every discovered worktree; each worktree syncs its exact and deterministic
|
|
111
|
+
* fallback-attributed transcripts (Codex + Claude main + sidecars). The
|
|
112
|
+
* ambiguous/unattributed/skipped remainder is reported with reason labels and
|
|
113
|
+
* a `source` discriminator instead of being duplicated into every repo or
|
|
26
114
|
* silently dropped. The session row's upload state maps ONLY from the main-file
|
|
27
115
|
* outcome (D3); sidecar outcomes aggregate into CLI counts.
|
|
28
116
|
*/
|
|
@@ -32,36 +120,103 @@ export async function runAttributedWorktreeSync(options) {
|
|
|
32
120
|
const paths = getCollectorRuntimePaths(options.homeDir);
|
|
33
121
|
const config = await readLocalCollectorConfig(paths).catch(() => null);
|
|
34
122
|
const claudeEnabled = config?.collect_claude_jsonl !== false;
|
|
35
|
-
const collectionRoots =
|
|
36
|
-
const
|
|
123
|
+
const collectionRoots = normalizeCollectionRoots(options.collectionRoots ?? config?.default_repo_paths ?? []);
|
|
124
|
+
const [codexCursorBefore, claudeCursorBefore, uploadSpool] = await Promise.all([
|
|
125
|
+
readRawEvidenceCursor(paths).catch(() => emptyRawEvidenceCursorState()),
|
|
126
|
+
claudeEnabled
|
|
127
|
+
? readRawEvidenceCursor(paths, {
|
|
128
|
+
filename: CLAUDE_CURSOR_FILENAME,
|
|
129
|
+
}).catch(() => emptyRawEvidenceCursorState())
|
|
130
|
+
: Promise.resolve(emptyRawEvidenceCursorState()),
|
|
131
|
+
readLocalUploadSpoolState(paths).catch(() => null),
|
|
132
|
+
]);
|
|
133
|
+
const legacyRetryPending = Boolean(uploadSpool?.pending_uploads.some((entry) => entry.raw_evidence_file_count > 0 && entry.retry_sources.length === 0));
|
|
134
|
+
const codexSourceRetryPending = Boolean(uploadSpool?.pending_source_retries.some((entry) => entry.source === "codex"));
|
|
135
|
+
const claudeSourceRetryPending = Boolean(uploadSpool?.pending_source_retries.some((entry) => entry.source === "claude_code"));
|
|
136
|
+
const codexRetryPending = legacyRetryPending ||
|
|
137
|
+
codexSourceRetryPending ||
|
|
138
|
+
Boolean(uploadSpool?.pending_uploads.some((entry) => entry.retry_sources.includes("codex"))) ||
|
|
139
|
+
cursorHasUndurableCollectableSession(codexCursorBefore);
|
|
140
|
+
const claudeRetryPending = claudeEnabled &&
|
|
141
|
+
(legacyRetryPending ||
|
|
142
|
+
claudeSourceRetryPending ||
|
|
143
|
+
Boolean(uploadSpool?.pending_uploads.some((entry) => entry.retry_sources.includes("claude_code"))) ||
|
|
144
|
+
cursorHasUndurableCollectableSession(claudeCursorBefore));
|
|
145
|
+
const allHistorySinceMinutes = allLocalHistorySinceMinutes(now);
|
|
146
|
+
let codexAttribution = await scanAndAttributeCodexSessions({
|
|
37
147
|
sessionsDirs: defaultCodexSessionDirs(homeDir),
|
|
38
148
|
worktrees: options.worktrees,
|
|
39
149
|
now,
|
|
40
|
-
sinceMinutes:
|
|
150
|
+
sinceMinutes: codexRetryPending
|
|
151
|
+
? allHistorySinceMinutes
|
|
152
|
+
: CODEX_ATTRIBUTION_SCAN_WINDOW_MINUTES,
|
|
41
153
|
limit: CODEX_ATTRIBUTION_SCAN_WINDOW_SESSION_LIMIT,
|
|
42
154
|
collectionRoots,
|
|
43
155
|
});
|
|
156
|
+
if (codexAttribution.session_limit_applied) {
|
|
157
|
+
codexAttribution = await scanAndAttributeCodexSessions({
|
|
158
|
+
sessionsDirs: defaultCodexSessionDirs(homeDir),
|
|
159
|
+
worktrees: options.worktrees,
|
|
160
|
+
now,
|
|
161
|
+
sinceMinutes: codexRetryPending
|
|
162
|
+
? allHistorySinceMinutes
|
|
163
|
+
: CODEX_ATTRIBUTION_SCAN_WINDOW_MINUTES,
|
|
164
|
+
limit: codexAttribution.discovered_file_count,
|
|
165
|
+
collectionRoots,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
44
168
|
// First run (D B.4 §8): without a Claude cursor yet, widen the window to 14
|
|
45
169
|
// days so the first sync captures retroactive history instead of only 24h.
|
|
46
170
|
const claudeCursorExists = await fileExists(path.join(paths.cursors_dir, CLAUDE_CURSOR_FILENAME));
|
|
47
171
|
const firstRunBackfill = claudeEnabled && !claudeCursorExists;
|
|
48
|
-
|
|
172
|
+
let claudeAttribution = claudeEnabled
|
|
49
173
|
? await scanAndAttributeClaudeSessions({
|
|
50
174
|
projectsDir: path.join(homeDir, ".claude", "projects"),
|
|
51
175
|
worktrees: options.worktrees,
|
|
52
176
|
now,
|
|
53
177
|
collectionRoots,
|
|
54
|
-
sinceMinutes:
|
|
55
|
-
?
|
|
56
|
-
:
|
|
178
|
+
sinceMinutes: claudeRetryPending
|
|
179
|
+
? allHistorySinceMinutes
|
|
180
|
+
: firstRunBackfill
|
|
181
|
+
? CLAUDE_FIRST_RUN_BACKFILL_MINUTES
|
|
182
|
+
: undefined,
|
|
57
183
|
})
|
|
58
184
|
: emptyClaudeScan();
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
185
|
+
if (claudeAttribution.session_limit_applied) {
|
|
186
|
+
claudeAttribution = await scanAndAttributeClaudeSessions({
|
|
187
|
+
projectsDir: path.join(homeDir, ".claude", "projects"),
|
|
188
|
+
worktrees: options.worktrees,
|
|
189
|
+
now,
|
|
190
|
+
collectionRoots,
|
|
191
|
+
sinceMinutes: claudeRetryPending
|
|
192
|
+
? allHistorySinceMinutes
|
|
193
|
+
: firstRunBackfill
|
|
194
|
+
? CLAUDE_FIRST_RUN_BACKFILL_MINUTES
|
|
195
|
+
: undefined,
|
|
196
|
+
limit: claudeAttribution.discovered_session_count,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
await reconcileSourceScanRetry({
|
|
200
|
+
paths,
|
|
201
|
+
source: "codex",
|
|
202
|
+
attemptedAt: now.toISOString(),
|
|
203
|
+
pendingBefore: codexSourceRetryPending,
|
|
204
|
+
reason: sourceScanRetryReason("codex", codexAttribution),
|
|
205
|
+
});
|
|
206
|
+
if (claudeEnabled) {
|
|
207
|
+
await reconcileSourceScanRetry({
|
|
208
|
+
paths,
|
|
209
|
+
source: "claude_code",
|
|
210
|
+
attemptedAt: now.toISOString(),
|
|
211
|
+
pendingBefore: claudeSourceRetryPending,
|
|
212
|
+
reason: sourceScanRetryReason("claude_code", claudeAttribution),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const syncWorktrees = liveSyncTargetWorktrees(options.worktrees, [
|
|
216
|
+
...codexAttribution.results,
|
|
217
|
+
...claudeAttribution.results,
|
|
218
|
+
]);
|
|
219
|
+
const discoveredWorktreeKeys = new Set(options.worktrees.map(liveSyncTargetKey));
|
|
65
220
|
// sessionId -> prior durable pointer for damped sessions (drives the
|
|
66
221
|
// growth_damped count + the skip_main decision).
|
|
67
222
|
const dampedClaudePointers = new Map();
|
|
@@ -83,22 +238,27 @@ export async function runAttributedWorktreeSync(options) {
|
|
|
83
238
|
};
|
|
84
239
|
const outcomes = [];
|
|
85
240
|
let ok = true;
|
|
86
|
-
for (const worktree of
|
|
241
|
+
for (const worktree of syncWorktrees) {
|
|
242
|
+
const attributedSyntheticTarget = discoveredWorktreeKeys.has(liveSyncTargetKey(worktree))
|
|
243
|
+
? null
|
|
244
|
+
: worktree;
|
|
245
|
+
const contextOptions = {
|
|
246
|
+
homeDir: options.homeDir,
|
|
247
|
+
repoRoot: worktree.repo_root,
|
|
248
|
+
activeTicketId: options.activeTicketId,
|
|
249
|
+
operatorId: options.operatorId,
|
|
250
|
+
sessionId: options.sessionId,
|
|
251
|
+
...(attributedSyntheticTarget ? {} : { branch: options.branch }),
|
|
252
|
+
};
|
|
253
|
+
const ensureContext = () => attributedSyntheticTarget
|
|
254
|
+
? startLocalWorkContextForAttributedTarget(contextOptions, attributedSyntheticTarget)
|
|
255
|
+
: startLocalWorkContext(contextOptions);
|
|
87
256
|
let context = null;
|
|
88
|
-
if (options.startContexts) {
|
|
89
|
-
context = await
|
|
90
|
-
homeDir: options.homeDir,
|
|
91
|
-
repoRoot: worktree.repo_root,
|
|
92
|
-
branch: options.branch,
|
|
93
|
-
activeTicketId: options.activeTicketId,
|
|
94
|
-
operatorId: options.operatorId,
|
|
95
|
-
sessionId: options.sessionId,
|
|
96
|
-
});
|
|
257
|
+
if (options.startContexts || attributedSyntheticTarget) {
|
|
258
|
+
context = await ensureContext();
|
|
97
259
|
}
|
|
98
260
|
const claudeSessionFiles = claudeAttribution.results
|
|
99
|
-
.filter((result) => result
|
|
100
|
-
result.worktree?.worktree_fingerprint ===
|
|
101
|
-
worktree.worktree_fingerprint)
|
|
261
|
+
.filter((result) => matchesLiveSyncWorktree(result, worktree))
|
|
102
262
|
.map((result) => {
|
|
103
263
|
const damped = !result.main_file_oversized &&
|
|
104
264
|
shouldDampClaudeMain(result, claudeCursorBefore, now);
|
|
@@ -120,11 +280,9 @@ export async function runAttributedWorktreeSync(options) {
|
|
|
120
280
|
homeDir: options.homeDir,
|
|
121
281
|
repoRoot: worktree.repo_root,
|
|
122
282
|
dashboardUrl: options.dashboardUrl,
|
|
123
|
-
worktreeInventory: worktreeInventoryForRepo(worktree,
|
|
283
|
+
worktreeInventory: worktreeInventoryForRepo(worktree, syncWorktrees),
|
|
124
284
|
codexSessionFiles: codexAttribution.results
|
|
125
|
-
.filter((result) => result
|
|
126
|
-
result.worktree?.worktree_fingerprint ===
|
|
127
|
-
worktree.worktree_fingerprint)
|
|
285
|
+
.filter((result) => matchesLiveSyncWorktree(result, worktree))
|
|
128
286
|
.map((result) => ({
|
|
129
287
|
local_path: result.file_path,
|
|
130
288
|
codex_session_id: result.codex_session_id,
|
|
@@ -146,11 +304,7 @@ export async function runAttributedWorktreeSync(options) {
|
|
|
146
304
|
// `cockpit start` by hand.
|
|
147
305
|
if (error instanceof LocalUploadBlockedError &&
|
|
148
306
|
error.blocker === "missing_context") {
|
|
149
|
-
context = await
|
|
150
|
-
homeDir: options.homeDir,
|
|
151
|
-
repoRoot: worktree.repo_root,
|
|
152
|
-
branch: options.branch,
|
|
153
|
-
});
|
|
307
|
+
context = await ensureContext();
|
|
154
308
|
sync = await syncLocalAmbientEnvelope(syncOptions);
|
|
155
309
|
}
|
|
156
310
|
else {
|
|
@@ -167,6 +321,33 @@ export async function runAttributedWorktreeSync(options) {
|
|
|
167
321
|
now,
|
|
168
322
|
claudePriorDurablePointers,
|
|
169
323
|
});
|
|
324
|
+
// Persist the metadata-only report before advancing either source cursor.
|
|
325
|
+
// If the process exits after this point, the spool keeps an exact retry copy.
|
|
326
|
+
// If queueing itself fails, the cursors remain at their prior retryable
|
|
327
|
+
// positions instead of aging an unreported session out of the live window.
|
|
328
|
+
const firstUploaded = outcomes.find((outcome) => outcome.sync.status === "uploaded");
|
|
329
|
+
const hadPendingSessionReports = (uploadSpool?.pending_session_reports.length ?? 0) > 0;
|
|
330
|
+
let queuedCurrentSessionReport = false;
|
|
331
|
+
if (firstUploaded && sessions.length > 0) {
|
|
332
|
+
const queued = await queueCodexSessionReport({
|
|
333
|
+
homeDir: options.homeDir,
|
|
334
|
+
dashboardUrl: firstUploaded.sync.dashboard_url,
|
|
335
|
+
generatedAt: now.toISOString(),
|
|
336
|
+
workContextId: firstUploaded.sync.work_context_id,
|
|
337
|
+
repoLabel: firstUploaded.worktree.repo_label,
|
|
338
|
+
branch: firstUploaded.worktree.branch,
|
|
339
|
+
repoFingerprint: firstUploaded.worktree.repo_fingerprint,
|
|
340
|
+
repoOriginUrl: firstUploaded.worktree.repo_origin_url,
|
|
341
|
+
worktreeLabel: firstUploaded.worktree.worktree_label,
|
|
342
|
+
worktreeFingerprint: firstUploaded.worktree.worktree_fingerprint,
|
|
343
|
+
worktreeIsPrimary: firstUploaded.worktree.worktree_is_primary,
|
|
344
|
+
sessions,
|
|
345
|
+
});
|
|
346
|
+
queuedCurrentSessionReport = Boolean(queued);
|
|
347
|
+
}
|
|
348
|
+
const reportRequired = sessions.length > 0 ||
|
|
349
|
+
hadPendingSessionReports ||
|
|
350
|
+
queuedCurrentSessionReport;
|
|
170
351
|
// The sessions cursor is an optimization; a broken local state dir must not
|
|
171
352
|
// turn already-completed syncs into a CLI crash.
|
|
172
353
|
let codexStaleCount = 0;
|
|
@@ -180,6 +361,9 @@ export async function runAttributedWorktreeSync(options) {
|
|
|
180
361
|
source: "codex",
|
|
181
362
|
sessionIdOf: (result) => result.codex_session_id,
|
|
182
363
|
now,
|
|
364
|
+
priorCursor: codexCursorBefore,
|
|
365
|
+
terminalizeMissingUndurable: codexRetryPending &&
|
|
366
|
+
codexAttributionReadFailureCount(codexAttribution) === 0,
|
|
183
367
|
});
|
|
184
368
|
codexCursor.updated_at = now.toISOString();
|
|
185
369
|
await writeRawEvidenceCursor(paths, codexCursor);
|
|
@@ -197,6 +381,8 @@ export async function runAttributedWorktreeSync(options) {
|
|
|
197
381
|
sessionIdOf: (result) => result.claude_session_id,
|
|
198
382
|
now,
|
|
199
383
|
priorCursor: claudeCursorBefore,
|
|
384
|
+
terminalizeMissingUndurable: claudeRetryPending &&
|
|
385
|
+
claudeAttributionReadFailureCount(claudeAttribution) === 0,
|
|
200
386
|
});
|
|
201
387
|
claudeCursorBefore.updated_at = now.toISOString();
|
|
202
388
|
await writeRawEvidenceCursor(paths, claudeCursorBefore, {
|
|
@@ -208,13 +394,9 @@ export async function runAttributedWorktreeSync(options) {
|
|
|
208
394
|
// Best-effort: a broken Claude cursor must not fail the sync.
|
|
209
395
|
}
|
|
210
396
|
}
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
? await postCodexSessionReport({
|
|
397
|
+
const report = hadPendingSessionReports || queuedCurrentSessionReport
|
|
398
|
+
? await flushPendingCodexSessionReports({
|
|
214
399
|
homeDir: options.homeDir,
|
|
215
|
-
repoRoot: firstUploaded.worktree.repo_root,
|
|
216
|
-
dashboardUrl: options.dashboardUrl,
|
|
217
|
-
sessions,
|
|
218
400
|
fetch: options.fetchImpl,
|
|
219
401
|
now,
|
|
220
402
|
})
|
|
@@ -232,6 +414,19 @@ export async function runAttributedWorktreeSync(options) {
|
|
|
232
414
|
growthDamped: dampedClaudePointers.size,
|
|
233
415
|
report,
|
|
234
416
|
});
|
|
417
|
+
ok =
|
|
418
|
+
ok &&
|
|
419
|
+
!codexAttribution.session_limit_applied &&
|
|
420
|
+
!claudeAttribution.session_limit_applied &&
|
|
421
|
+
outcomes.every(({ sync }) => sync.raw_evidence_failed_count === 0 &&
|
|
422
|
+
!sync.raw_evidence_retry_required &&
|
|
423
|
+
sync.raw_evidence_deferred_byte_budget === 0 &&
|
|
424
|
+
sync.raw_evidence_deferred_object_budget === 0) &&
|
|
425
|
+
codexAttributionReadFailureCount(codexAttribution) === 0 &&
|
|
426
|
+
claudeAttributionReadFailureCount(claudeAttribution) === 0 &&
|
|
427
|
+
sourceScanRetryReason("codex", codexAttribution) === null &&
|
|
428
|
+
sourceScanRetryReason("claude_code", claudeAttribution) === null &&
|
|
429
|
+
(!reportRequired || report.posted);
|
|
235
430
|
return { ok, outcomes, codexAttribution, claudeAttribution, summary };
|
|
236
431
|
}
|
|
237
432
|
export const ATTRIBUTION_STATE_RANK = CODEX_SESSION_ATTRIBUTION_STATE_RANK;
|
|
@@ -374,6 +569,20 @@ function buildAgentSessionReport(options) {
|
|
|
374
569
|
*/
|
|
375
570
|
function recordSourceObservations(options) {
|
|
376
571
|
const seen = new Set(options.results.map((result) => options.sessionIdOf(result)));
|
|
572
|
+
if (options.terminalizeMissingUndurable) {
|
|
573
|
+
for (const [sessionId, entry] of Object.entries(options.cursor.sessions)) {
|
|
574
|
+
if (seen.has(sessionId) ||
|
|
575
|
+
!liveSyncCursorEntryRequiresRetry(entry)) {
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
options.cursor.sessions[sessionId] = {
|
|
579
|
+
...entry,
|
|
580
|
+
state: "skipped",
|
|
581
|
+
reason: "retry_source_missing",
|
|
582
|
+
last_seen_at: options.now.toISOString(),
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
}
|
|
377
586
|
const stale = countStaleSessions(options.cursor, seen);
|
|
378
587
|
for (const result of options.results) {
|
|
379
588
|
const sessionId = options.sessionIdOf(result);
|
|
@@ -436,7 +645,7 @@ function shouldDampClaudeMain(result, cursor, now) {
|
|
|
436
645
|
}
|
|
437
646
|
function buildAgentSessionSummary(options) {
|
|
438
647
|
const sidecarOutcomes = options.outcomes.flatMap((outcome) => outcome.sync.raw_evidence_outcomes.filter((upload) => upload.kind === "claude_jsonl_sidecar"));
|
|
439
|
-
const attributedClaude = options.claudeAttribution.results.filter((result) => result.state
|
|
648
|
+
const attributedClaude = options.claudeAttribution.results.filter((result) => isLiveRawEvidenceSyncAttribution(result.state, result.reason));
|
|
440
649
|
const sidecarsCollected = attributedClaude.reduce((total, result) => total +
|
|
441
650
|
result.sidecar_files.filter((sidecar) => !sidecar.skipped_reason).length, 0);
|
|
442
651
|
const sidecarsSkipped = options.claudeAttribution.results.reduce((total, result) => total +
|
|
@@ -449,6 +658,7 @@ function buildAgentSessionSummary(options) {
|
|
|
449
658
|
unattributed: options.codexAttribution.counts.unattributed,
|
|
450
659
|
skipped: options.codexAttribution.counts.skipped,
|
|
451
660
|
stale: options.codexStaleCount,
|
|
661
|
+
read_failures: codexAttributionReadFailureCount(options.codexAttribution),
|
|
452
662
|
};
|
|
453
663
|
const claude = {
|
|
454
664
|
scanned: options.claudeAttribution.scanned_session_count,
|
|
@@ -458,6 +668,7 @@ function buildAgentSessionSummary(options) {
|
|
|
458
668
|
unattributed: options.claudeAttribution.counts.unattributed,
|
|
459
669
|
skipped: options.claudeAttribution.counts.skipped,
|
|
460
670
|
stale: options.claudeStaleCount,
|
|
671
|
+
read_failures: claudeAttributionReadFailureCount(options.claudeAttribution),
|
|
461
672
|
sidecars_collected: sidecarsCollected,
|
|
462
673
|
sidecars_uploaded: sidecarOutcomes.filter((upload) => upload.upload_state === "uploaded" ||
|
|
463
674
|
upload.upload_state === "reused_existing").length,
|
|
@@ -534,12 +745,6 @@ function worktreeInventoryForRepo(current, worktrees) {
|
|
|
534
745
|
branch: worktree.branch,
|
|
535
746
|
}));
|
|
536
747
|
}
|
|
537
|
-
function fallbackCollectionRoots(savedRoots, worktrees) {
|
|
538
|
-
return normalizeCollectionRoots([
|
|
539
|
-
...savedRoots,
|
|
540
|
-
...worktrees.map((worktree) => worktree.requested_path || worktree.repo_root),
|
|
541
|
-
]);
|
|
542
|
-
}
|
|
543
748
|
async function fileExists(filePath) {
|
|
544
749
|
const { stat } = await import("node:fs/promises");
|
|
545
750
|
return stat(filePath).then(() => true, () => false);
|