@bli-cockpit/cli 0.2.293 → 0.2.294
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/adapters/codex-attribution.js +28 -7
- package/dist/adapters/codex-segment-discovery.js +12 -3
- package/dist/adapters/codex-session-revision.js +58 -3
- package/dist/adapters/local-sources.js +1 -0
- package/dist/adapters/opencode-store.js +201 -0
- package/dist/adapters/raw-evidence-attribution-gaps.js +5 -0
- package/dist/adapters/raw-evidence-codex-reader.js +25 -3
- package/dist/adapters/raw-evidence-transcript-body.js +7 -2
- package/dist/adapters/raw-evidence-transcript-file.js +19 -0
- package/dist/adapters/raw-evidence-transcript-stream.js +6 -3
- package/dist/adapters/raw-evidence.js +13 -0
- package/dist/adapters/t3-projection-source.js +96 -0
- package/dist/commands/backfill-checkpoint.js +1 -1
- package/dist/commands/backfill-issues.js +5 -0
- package/dist/commands/backfill-output.js +6 -0
- package/dist/commands/backfill-report.js +3 -3
- package/dist/commands/backfill-scan.js +2 -0
- package/dist/commands/backfill-scope.js +2 -0
- package/dist/commands/backfill-upload.js +3 -2
- package/dist/commands/backfill.js +20 -3
- package/dist/commands/codex-revision-facts.js +3 -3
- package/dist/commands/collection-report.js +6 -0
- package/dist/commands/local-args-collector-status.js +2 -2
- package/dist/commands/local-args-collector-work.js +2 -2
- package/dist/commands/local-help-commands-tower.js +1 -1
- package/dist/commands/local-help.js +3 -3
- package/dist/commands/opencode-sync.js +163 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-failures.js +4 -0
- package/dist/commands/session-sync-record.js +1 -1
- package/dist/commands/session-sync-upload.js +2 -1
- package/dist/commands/session-sync.js +18 -1
- package/dist/commands/sessions.js +27 -8
- package/dist/commands/sources-check.js +54 -47
- package/dist/commands/sources-discovery.js +35 -4
- package/dist/commands/sources-verdict.js +3 -3
- package/dist/local-state-status.js +18 -1
- package/dist/opencode-inventory.js +203 -0
- package/dist/opencode-policy.js +29 -0
- package/dist/slim/slim-transcript.js +3 -2
- package/dist/source-opt-out.js +2 -1
- package/dist/spool/local-spool-parse.js +1 -1
- package/dist/state-dir-audit.js +2 -0
- package/dist/sync-health-class.js +2 -0
- package/dist/upload-envelope-build.js +1 -0
- package/dist/upload-evidence-delivery-summary.js +10 -0
- package/dist/upload-session-reports-wire.js +15 -8
- package/package.json +4 -4
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Bounded session-only SQLite projections. No settings, credentials, DB copies or long-lived transactions. */
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
const MAX_ROWS = 10_000;
|
|
8
|
+
const MAX_TEXT_BYTES = 32 * 1024 * 1024;
|
|
9
|
+
const sha = (text) => createHash("sha256").update(text).digest("hex");
|
|
10
|
+
function withDatabase(homeDir, absent, read) {
|
|
11
|
+
const file = path.join(homeDir, ".t3", "userdata", "statev2.sqlite");
|
|
12
|
+
if (!existsSync(file))
|
|
13
|
+
return absent;
|
|
14
|
+
let db;
|
|
15
|
+
try {
|
|
16
|
+
const { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
|
|
17
|
+
db = new DatabaseSync(file, { readOnly: true });
|
|
18
|
+
return read(db);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
const reason = error instanceof Error && error.message.startsWith("t3_projection_") ? error.message : "t3_projection_unreadable";
|
|
22
|
+
console.error("[t3-projection] source unavailable", JSON.stringify({ reason }));
|
|
23
|
+
throw new Error(reason);
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
db?.close();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** The small identity inventory lets singleton native sessions use the same composite path. */
|
|
30
|
+
export function t3CodexProjectionSessionIds(homeDir = os.homedir()) {
|
|
31
|
+
return withDatabase(homeDir, new Set(), db => {
|
|
32
|
+
const rows = db.prepare(`SELECT DISTINCT json_extract(payload_json, '$.nativeThreadRef.nativeId') AS native_id
|
|
33
|
+
FROM orchestration_v2_projection_provider_threads WHERE driver = 'codex'
|
|
34
|
+
AND json_extract(payload_json, '$.nativeThreadRef.driver') = 'codex'
|
|
35
|
+
AND json_extract(payload_json, '$.nativeThreadRef.strength') = 'strong'
|
|
36
|
+
ORDER BY native_id LIMIT ${MAX_ROWS + 1}`).all();
|
|
37
|
+
if (rows.length > MAX_ROWS)
|
|
38
|
+
throw new Error("t3_projection_inventory_capped");
|
|
39
|
+
return new Set(rows.flatMap(r => typeof r["native_id"] === "string" && r["native_id"] ? [r["native_id"]] : []));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** One SELECT pins the WAL snapshot; only explicit observed assistant and strong native identity fields leave SQLite. */
|
|
43
|
+
export function readT3CodexProjection(nativeSessionId, homeDir = os.homedir()) {
|
|
44
|
+
return withDatabase(homeDir, [], db => {
|
|
45
|
+
const rows = db.prepare(`SELECT m.message_id, m.thread_id, m.created_at, m.updated_at,
|
|
46
|
+
CASE WHEN length(CAST(json_extract(m.payload_json, '$.text') AS BLOB)) <= ${MAX_TEXT_BYTES}
|
|
47
|
+
THEN json_extract(m.payload_json, '$.text') END AS text,
|
|
48
|
+
length(CAST(json_extract(m.payload_json, '$.text') AS BLOB)) AS text_bytes,
|
|
49
|
+
json_extract(i.payload_json, '$.nativeItemRef.nativeId') AS item_id,
|
|
50
|
+
json_extract(t.payload_json, '$.nativeTurnRef.nativeId') AS turn_id
|
|
51
|
+
FROM orchestration_v2_projection_provider_threads AS p
|
|
52
|
+
CROSS JOIN orchestration_v2_projection_turn_items AS i ON i.thread_id = p.thread_id AND i.provider_thread_id = p.provider_thread_id
|
|
53
|
+
JOIN orchestration_v2_projection_provider_turns AS t ON t.provider_turn_id = i.provider_turn_id AND t.provider_thread_id = p.provider_thread_id
|
|
54
|
+
JOIN orchestration_v2_projection_messages AS m ON m.message_id = json_extract(i.payload_json, '$.messageId') AND m.thread_id = i.thread_id AND m.run_id = i.run_id
|
|
55
|
+
WHERE p.driver = 'codex' AND json_extract(p.payload_json, '$.nativeThreadRef.nativeId') = ?
|
|
56
|
+
AND json_extract(p.payload_json, '$.nativeThreadRef.driver') = 'codex'
|
|
57
|
+
AND json_extract(p.payload_json, '$.nativeThreadRef.strength') = 'strong'
|
|
58
|
+
AND i.type = 'assistant_message' AND m.role = 'assistant' AND m.streaming = 0
|
|
59
|
+
AND json_extract(i.payload_json, '$.nativeItemRef.driver') = 'codex'
|
|
60
|
+
AND json_extract(i.payload_json, '$.nativeItemRef.strength') = 'strong'
|
|
61
|
+
AND json_extract(t.payload_json, '$.nativeTurnRef.driver') = 'codex'
|
|
62
|
+
AND json_extract(t.payload_json, '$.nativeTurnRef.strength') = 'strong'
|
|
63
|
+
ORDER BY m.created_at, m.message_id, turn_id, item_id LIMIT ${MAX_ROWS + 1}`).iterate(nativeSessionId);
|
|
64
|
+
const messages = [];
|
|
65
|
+
// CROSS JOIN pins the small provider-thread inventory outermost; otherwise
|
|
66
|
+
// installed SQLite scans every unrelated turn-item payload before filtering.
|
|
67
|
+
let textBytes = 0, projectionBytes = 0, returned = 0;
|
|
68
|
+
for (const r of rows) {
|
|
69
|
+
if (++returned > MAX_ROWS)
|
|
70
|
+
throw new Error("t3_projection_messages_capped");
|
|
71
|
+
if (typeof r["text_bytes"] === "number" && r["text_bytes"] > MAX_TEXT_BYTES)
|
|
72
|
+
throw new Error("t3_projection_bytes_capped");
|
|
73
|
+
if (typeof r["text"] !== "string" || !r["text"].trim())
|
|
74
|
+
continue;
|
|
75
|
+
textBytes += Buffer.byteLength(r["text"]);
|
|
76
|
+
if (textBytes > MAX_TEXT_BYTES)
|
|
77
|
+
throw new Error("t3_projection_bytes_capped");
|
|
78
|
+
for (const field of ["message_id", "thread_id", "item_id", "turn_id", "created_at", "updated_at"]) {
|
|
79
|
+
if (typeof r[field] !== "string" || !r[field])
|
|
80
|
+
throw new Error("t3_projection_mapping_invalid");
|
|
81
|
+
}
|
|
82
|
+
const message = { schema_version: "t3-message.v1", native_session_id: nativeSessionId,
|
|
83
|
+
native_turn_id: r["turn_id"], native_item_id: r["item_id"],
|
|
84
|
+
t3_thread_id: r["thread_id"], t3_message_id: r["message_id"],
|
|
85
|
+
role: "assistant", created_at: r["created_at"], updated_at: r["updated_at"],
|
|
86
|
+
text_sha256: sha(r["text"]), text: r["text"], provenance: "t3-0.1.7:orchestration_v2_projection_messages" };
|
|
87
|
+
projectionBytes += Buffer.byteLength(JSON.stringify(message));
|
|
88
|
+
if (projectionBytes > MAX_TEXT_BYTES)
|
|
89
|
+
throw new Error("t3_projection_bytes_capped");
|
|
90
|
+
messages.push(message);
|
|
91
|
+
}
|
|
92
|
+
return messages;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/** Hashes include source identity, timestamps and original text revisions, not database/WAL file stamps. */
|
|
96
|
+
export function t3ProjectionRevision(messages) { return sha(JSON.stringify(messages)); }
|
|
@@ -62,7 +62,7 @@ function deliveredCursorEntry(prior, candidate, objectKey, now) {
|
|
|
62
62
|
const entry = {
|
|
63
63
|
...prior,
|
|
64
64
|
file_hash_sha256: candidate.content_hash_sha256,
|
|
65
|
-
uploaded_source_revision: isRedactionStubObjectKey(objectKey) ? prior?.uploaded_source_revision ?? null : candidate.content_hash_sha256,
|
|
65
|
+
uploaded_source_revision: candidate.t3_projection_incomplete || isRedactionStubObjectKey(objectKey) ? prior?.uploaded_source_revision ?? null : candidate.content_hash_sha256,
|
|
66
66
|
file_mtime_ms: candidate.session_file_mtime_ms,
|
|
67
67
|
byte_size: candidate.byte_size,
|
|
68
68
|
// The whole file was delivered, so the incremental reader starts at its end.
|
|
@@ -48,6 +48,11 @@ async function addCodexStoreScanIssues(issues, codexAttribution) {
|
|
|
48
48
|
// here would erase a failure in another configured home or T3 discovery.
|
|
49
49
|
countScanIssue(issues, "codex_directory_read_failed", codexAttribution.directory_read_failed_count, "global");
|
|
50
50
|
countScanIssue(issues, "codex_session_stat_failed", codexAttribution.stat_failed_count, "global");
|
|
51
|
+
if (codexAttribution.projection_failure_reason)
|
|
52
|
+
countScanIssue(issues, codexAttribution.projection_failure_reason, 1, "global");
|
|
53
|
+
for (const result of codexAttribution.results)
|
|
54
|
+
if (result.t3_projection_failure_reason)
|
|
55
|
+
countScanIssue(issues, result.t3_projection_failure_reason, 1, "global");
|
|
51
56
|
}
|
|
52
57
|
/** The same census for the Claude store, which also has sidecars to account for. */
|
|
53
58
|
async function addClaudeStoreScanIssues(issues, claudeAttribution, candidates) {
|
|
@@ -16,6 +16,12 @@ export function writeDryRunSummary(io, options) {
|
|
|
16
16
|
writeLine(io.stdout, `Verify after upload: ${options.dashboardUrl}/my-work`);
|
|
17
17
|
}
|
|
18
18
|
export function writeHumanBackfillResult(result, io) {
|
|
19
|
+
if (result.opencode) {
|
|
20
|
+
const source = result.opencode;
|
|
21
|
+
writeLine(io.stdout, `OpenCode: ${source.discovered} discovered, ${source.eligible} eligible, ${source.delivered} delivered, ${source.pending} pending, ${source.withheld} withheld (${source.coverage}).`);
|
|
22
|
+
for (const reason of source.reasons)
|
|
23
|
+
writeLine(io.stderr, `OpenCode: ${reason}`);
|
|
24
|
+
}
|
|
19
25
|
if (result.status === "complete") {
|
|
20
26
|
writeLine(io.stdout, `PASS: ${result.counts.backfilled} backfilled, ${result.counts.skipped} skipped (table). Verify: ${result.verify_url}`);
|
|
21
27
|
writeReasonTable(io, result.counts.reasons);
|
|
@@ -18,7 +18,7 @@ import { summarizeStubReoffer } from "./backfill-stub-reoffer.js";
|
|
|
18
18
|
* blocks completion on its own — every completion-gating computation below
|
|
19
19
|
* excludes it explicitly via `oversizedCandidateKeys`/`blockingScanIssues`.
|
|
20
20
|
*/
|
|
21
|
-
export async function reportBackfillOutcome(command, io, ctx, upload) {
|
|
21
|
+
export async function reportBackfillOutcome(command, io, ctx, upload, nativePartial = false) {
|
|
22
22
|
let { blockedAt, failureReason } = upload;
|
|
23
23
|
const posted = await postBackfillSessionReport(command, io, ctx, upload);
|
|
24
24
|
if (!posted.acknowledged && !blockedAt) {
|
|
@@ -36,8 +36,8 @@ export async function reportBackfillOutcome(command, io, ctx, upload) {
|
|
|
36
36
|
ctx,
|
|
37
37
|
upload,
|
|
38
38
|
posted,
|
|
39
|
-
stoppedEarly: Boolean(blockedAt),
|
|
40
|
-
failureReason,
|
|
39
|
+
stoppedEarly: Boolean(blockedAt) || nativePartial,
|
|
40
|
+
failureReason: failureReason ?? (nativePartial ? "opencode_backfill_pending" : undefined),
|
|
41
41
|
});
|
|
42
42
|
if (completion.status === "complete" && command.all && !command.stubPass) {
|
|
43
43
|
await writeAllHistoryCompletionMarker(ctx);
|
|
@@ -269,6 +269,8 @@ function normalizeCodexCandidate(result) {
|
|
|
269
269
|
source: "codex",
|
|
270
270
|
session_id: result.codex_session_id,
|
|
271
271
|
...(result.segment_paths ? { segment_paths: result.segment_paths } : {}),
|
|
272
|
+
...(result.t3_projection ? { t3_projection: true } : {}),
|
|
273
|
+
...(result.t3_projection_incomplete ? { t3_projection_incomplete: true } : {}),
|
|
272
274
|
session_id_source: result.session_id_source,
|
|
273
275
|
file_path: result.file_path,
|
|
274
276
|
state: result.state,
|
|
@@ -98,6 +98,8 @@ async function discoverBackfillWorktrees(roots, command) {
|
|
|
98
98
|
});
|
|
99
99
|
}
|
|
100
100
|
export function selectedSources(source) {
|
|
101
|
+
if (source === "opencode")
|
|
102
|
+
return []; // Native inventory and cursor are handled by the shared native pass.
|
|
101
103
|
if (source === "codex")
|
|
102
104
|
return ["codex"];
|
|
103
105
|
if (source === "claude")
|
|
@@ -19,10 +19,10 @@ const BACKFILL_MAX_CONSECUTIVE_FAILURES = 3;
|
|
|
19
19
|
* missing" would fail every batch it happens to share with genuinely uploaded
|
|
20
20
|
* siblings, so it is excluded via `oversizedCandidateKeys` throughout.
|
|
21
21
|
*/
|
|
22
|
-
export async function uploadBackfillBatches(command, io, lock, ctx) {
|
|
22
|
+
export async function uploadBackfillBatches(command, io, lock, ctx, sharedBudget) {
|
|
23
23
|
const uploadable = uploadableCandidates(ctx.scan.candidates);
|
|
24
24
|
const batches = planUploadBatches(command, uploadable);
|
|
25
|
-
const rawEvidenceBudget = {
|
|
25
|
+
const rawEvidenceBudget = sharedBudget ?? {
|
|
26
26
|
remainingBytes: RAW_EVIDENCE_DEFAULT_BYTE_BUDGET,
|
|
27
27
|
remainingObjects: RAW_EVIDENCE_DEFAULT_OBJECT_BUDGET,
|
|
28
28
|
};
|
|
@@ -231,6 +231,7 @@ function backfillBatchSyncOptions(options) {
|
|
|
231
231
|
.map((candidate) => ({
|
|
232
232
|
local_path: candidate.file_path,
|
|
233
233
|
codex_session_id: candidate.session_id,
|
|
234
|
+
...(candidate.t3_projection ? { t3_projection: true } : {}),
|
|
234
235
|
...(candidate.segment_paths ? { segment_paths: candidate.segment_paths, revision_hash: candidate.content_hash_sha256 ?? undefined, revision_bytes: candidate.byte_size } : {}),
|
|
235
236
|
})),
|
|
236
237
|
codexAttributionScan: options.codexAttribution ?? undefined,
|
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
* both earned a durable pointer and were acknowledged by the server, so a
|
|
38
38
|
* misjudged "resolved" loses that history for good.
|
|
39
39
|
*/
|
|
40
|
+
import { RAW_EVIDENCE_DEFAULT_BYTE_BUDGET, RAW_EVIDENCE_DEFAULT_OBJECT_BUDGET } from "../adapters/raw-evidence.js";
|
|
41
|
+
import { syncOpenCodeSessions } from "./opencode-sync.js";
|
|
40
42
|
import { acquireBackfillLock } from "../backfill-lock.js";
|
|
41
43
|
import { writeBackfillCursor } from "../cursors/backfill-cursor.js";
|
|
42
44
|
import { DEFAULT_BACKFILL_SINCE_DAYS, backfillRetryCommand, defaultBackfillWindowNotice, } from "./backfill-command-line.js";
|
|
@@ -105,8 +107,16 @@ export async function runBackfill(command, io) {
|
|
|
105
107
|
if (scanned.kind === "blocked")
|
|
106
108
|
return scanned.result;
|
|
107
109
|
const plan = await planBackfillRun(command, io, scanned);
|
|
108
|
-
if (plan.kind === "result")
|
|
110
|
+
if (plan.kind === "result") {
|
|
111
|
+
if (command.dryRun && (!command.source || command.source === "opencode") && plan.result.status !== "blocked") {
|
|
112
|
+
plan.result.opencode = await syncOpenCodeSessions({ homeDir: scanned.paths.home_dir, worktrees: scanned.worktrees, collectionRoots: scanned.collectionRoots,
|
|
113
|
+
dashboardUrl: scanned.dashboardUrl, fetchImpl: io.fetch ?? globalThis.fetch, dryRun: true,
|
|
114
|
+
sinceMs: command.all ? undefined : now.getTime() - (command.sinceDays ?? DEFAULT_BACKFILL_SINCE_DAYS) * 86400000 });
|
|
115
|
+
if (plan.result.opencode.coverage === "partial")
|
|
116
|
+
plan.result.status = "partial";
|
|
117
|
+
}
|
|
109
118
|
return plan.result;
|
|
119
|
+
}
|
|
110
120
|
const { paths } = scanned;
|
|
111
121
|
// BLI-11112: the sync's stub pass does not take the backfill lock. It runs
|
|
112
122
|
// under the collection lock, which already serialises it against any
|
|
@@ -144,8 +154,15 @@ export async function runBackfill(command, io) {
|
|
|
144
154
|
// BLI-11102: mark server-named stubs as not delivered, under the lock
|
|
145
155
|
// because sync writes the same cursor files.
|
|
146
156
|
await markStubReoffers(paths, scanned.stubReoffer, now);
|
|
147
|
-
const
|
|
148
|
-
|
|
157
|
+
const budget = { remainingBytes: RAW_EVIDENCE_DEFAULT_BYTE_BUDGET, remainingObjects: RAW_EVIDENCE_DEFAULT_OBJECT_BUDGET };
|
|
158
|
+
const upload = await uploadBackfillBatches(command, io, lock, scanned, budget);
|
|
159
|
+
const opencode = (!command.source || command.source === "opencode") ? await syncOpenCodeSessions({ homeDir: paths.home_dir, worktrees: scanned.worktrees, collectionRoots: scanned.collectionRoots,
|
|
160
|
+
dashboardUrl: scanned.dashboardUrl, fetchImpl: io.fetch ?? globalThis.fetch, budget, deadlineMs: command.deadlineMs,
|
|
161
|
+
sinceMs: command.all ? undefined : now.getTime() - (command.sinceDays ?? DEFAULT_BACKFILL_SINCE_DAYS) * 86400000 }) : undefined;
|
|
162
|
+
const result = await reportBackfillOutcome(command, io, scanned, upload, opencode?.coverage === "partial");
|
|
163
|
+
if (opencode)
|
|
164
|
+
result.opencode = opencode;
|
|
165
|
+
return result;
|
|
149
166
|
}
|
|
150
167
|
finally {
|
|
151
168
|
await collectionLock.handle.release();
|
|
@@ -5,9 +5,9 @@ import { imageStateKey } from "../image-description-state.js";
|
|
|
5
5
|
import { openCodexSessionRevision } from "../adapters/codex-session-revision.js";
|
|
6
6
|
import { createSlimmer } from "../slim/slim-transcript.js";
|
|
7
7
|
import { scanLines, decideLinePlan, writeLinePlan } from "../adapters/raw-evidence-sanitize-lines.js";
|
|
8
|
-
export function codexRevisionFacts(paths, mode, describedKeys, slimAware) {
|
|
8
|
+
export function codexRevisionFacts(paths, mode, describedKeys, slimAware, t3Projection = false) {
|
|
9
9
|
const form = async (drop) => {
|
|
10
|
-
const source = await openCodexSessionRevision(paths);
|
|
10
|
+
const source = await openCodexSessionRevision(paths, { t3Projection });
|
|
11
11
|
try {
|
|
12
12
|
const lines = { async *lines() {
|
|
13
13
|
const slimmer = createSlimmer("codex");
|
|
@@ -46,7 +46,7 @@ export function codexRevisionFacts(paths, mode, describedKeys, slimAware) {
|
|
|
46
46
|
let full, slim;
|
|
47
47
|
return {
|
|
48
48
|
slimAware,
|
|
49
|
-
async rawSha256() { const source = await openCodexSessionRevision(paths); try {
|
|
49
|
+
async rawSha256() { const source = await openCodexSessionRevision(paths, { t3Projection }); try {
|
|
50
50
|
return source.sha256;
|
|
51
51
|
}
|
|
52
52
|
finally {
|
|
@@ -32,6 +32,12 @@ function attributionReportLine(summary) {
|
|
|
32
32
|
export function writeAgentSessionSummary(io, summary) {
|
|
33
33
|
writeLine(io.stdout, sourceFunnelLine("Codex", summary.codex));
|
|
34
34
|
writeLine(io.stdout, sourceFunnelLine("Claude", summary.claude));
|
|
35
|
+
if (summary.opencode) {
|
|
36
|
+
const source = summary.opencode;
|
|
37
|
+
writeLine(io.stdout, `OpenCode sessions: ${source.discovered} discovered, ${source.eligible} eligible, ${source.delivered} delivered, ${source.pending} pending, ${source.withheld} withheld (${source.coverage}).`);
|
|
38
|
+
for (const reason of source.reasons)
|
|
39
|
+
writeLine(io.stdout, `OpenCode: ${reason}`);
|
|
40
|
+
}
|
|
35
41
|
const claudeDiagnostics = claudeDiagnosticsLine(summary);
|
|
36
42
|
if (claudeDiagnostics)
|
|
37
43
|
writeLine(io.stdout, claudeDiagnostics);
|
|
@@ -69,8 +69,8 @@ export function parseSessionsArgs(args) {
|
|
|
69
69
|
});
|
|
70
70
|
assertNoPositionals(values.positionals, "sessions");
|
|
71
71
|
const source = values.flags.get("--source");
|
|
72
|
-
if (source !== undefined && source !== "codex" && source !== "claude") {
|
|
73
|
-
throw new Error("--source must be 'codex' or '
|
|
72
|
+
if (source !== undefined && source !== "codex" && source !== "claude" && source !== "opencode") {
|
|
73
|
+
throw new Error("--source must be 'codex', 'claude' or 'opencode'. Omit it to scan all sources.");
|
|
74
74
|
}
|
|
75
75
|
const sinceDays = optionalPositiveInteger(values.flags.get("--since-days"), "--since-days");
|
|
76
76
|
const all = values.booleans.has("--all");
|
|
@@ -151,8 +151,8 @@ export function parseBackfillArgs(args) {
|
|
|
151
151
|
});
|
|
152
152
|
assertNoPositionals(values.positionals, "backfill");
|
|
153
153
|
const source = values.flags.get("--source");
|
|
154
|
-
if (source !== undefined && source !== "codex" && source !== "claude") {
|
|
155
|
-
throw new Error("--source must be 'codex' or '
|
|
154
|
+
if (source !== undefined && source !== "codex" && source !== "claude" && source !== "opencode") {
|
|
155
|
+
throw new Error("--source must be 'codex', 'claude' or 'opencode'. Omit it to scan all sources.");
|
|
156
156
|
}
|
|
157
157
|
const sinceDays = optionalPositiveInteger(values.flags.get("--since-days"), "--since-days");
|
|
158
158
|
const all = values.booleans.has("--all");
|
|
@@ -251,7 +251,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
251
251
|
"usage",
|
|
252
252
|
[
|
|
253
253
|
"Usage: cockpit usage people [--by-repo | --by-topic | --by-subject | --by-task | --by-client] [--min-confidence high|medium|low] [--person <email|me>] [--all] [--since <n>d|<n>h|<iso>] [--detail] [--until <iso>] [--include-automated] [--json]",
|
|
254
|
-
" cockpit usage sessions [--person <exact-name>] [--task <BLI-NNNN> | --subject <name>] [--repo <label>] [--tool claude|codex] [--since <window>] [--until <iso>] [--cursor <cursor>] [--limit <n>] [--json]",
|
|
254
|
+
" cockpit usage sessions [--person <exact-name>] [--task <BLI-NNNN> | --subject <name>] [--repo <label>] [--tool claude|codex|opencode] [--since <window>] [--until <iso>] [--cursor <cursor>] [--limit <n>] [--json]",
|
|
255
255
|
" cockpit usage session <id> [--from-window <n>] [--json]",
|
|
256
256
|
" --by-topic groups work types; --by-subject groups open subjects from session summaries.",
|
|
257
257
|
"",
|
|
@@ -146,9 +146,9 @@ const COMMAND_SUMMARY_LINES = [
|
|
|
146
146
|
" cockpit correct --claim <claimId> --text \"<what is wrong>\" [--for <person>] [--version <pageId>] [--supersedes <id>] [--dashboard-url <url>] [--json]",
|
|
147
147
|
" cockpit notes [list|show <id>|shelf|shelves|folders|mkdir <path>|rmdir <path>|rename <path>|upload <paths...> [--wait]|paste|share <id>|unshare <id>|summarize <id>|move <id>|place <id>] [--folder <path>] [--series <shelf>] [--kind <kind>] [--since <YYYY-MM-DD>] [--until <YYYY-MM-DD>] [--limit <n>] [--file <path>] [--name <n>] [--exclude \"<sentence>\"] [--to \"<shelf>\"|--clear-shelf] [--apply] [--yes] [--dashboard-url <url>] [--json]",
|
|
148
148
|
" cockpit notes [accounts [--show-webhook <account-id>]|connect --provider fellow --workspace <sub> --key-stdin [--name <n>]|connect --provider wisprflow|recordings <note-id>|recording <id>|webhook <account-id> --secret-stdin|detach <account-id>|sync <account-id>] [--json] (the key and the signing secret are read from stdin, never a flag)",
|
|
149
|
-
" cockpit backfill (--since-days <n>|--all) [--source codex|claude] [--dry-run] [--max-files <n>] [--max-depth <n>] [--max-repos <n>] [--lock-wait <seconds>] [--yes] [--workspace <path>] [--json]",
|
|
149
|
+
" cockpit backfill (--since-days <n>|--all) [--source codex|claude|opencode] [--dry-run] [--max-files <n>] [--max-depth <n>] [--max-repos <n>] [--lock-wait <seconds>] [--yes] [--workspace <path>] [--json]",
|
|
150
150
|
" cockpit status [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
151
|
-
" cockpit sessions [--source codex|claude] [--since-days <n>|--all] [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
151
|
+
" cockpit sessions [--source codex|claude|opencode] [--since-days <n>|--all] [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
152
152
|
" cockpit serve [--port <port>] [--workspace <path>]",
|
|
153
153
|
" cockpit autostart [install|uninstall|status] [--workspace <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
|
|
154
154
|
" cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--workspace <path>] [--json]",
|
|
@@ -190,7 +190,7 @@ const COMMAND_SUMMARY_LINES = [
|
|
|
190
190
|
" cockpit manager <clients|client <client>|run <run-id>|list <client>|queue <client>|show <id>|add <client> --title <t>|edit <id> --version <v>|done|reopen|link|approve|dismiss|proposal|archive|unarchive> [--timeline] [--dashboard-url <url>] [--json]",
|
|
191
191
|
" cockpit image <describe <file> [--session <id>]|show <sha256-or-prefix>> [--dashboard-url <url>] [--json]",
|
|
192
192
|
" cockpit setup [--since <duration|date>] [--until <date>] [--machines] [--details] [--person <name|email>] [--compare <name,name>] [--json]",
|
|
193
|
-
" cockpit usage sessions [--person <exact-name>] [--task <BLI-NNNN> | --subject <name>] [--repo <label>] [--tool claude|codex] [--since <window>] [--until <iso>] [--cursor <cursor>] [--limit <n>] [--json]",
|
|
193
|
+
" cockpit usage sessions [--person <exact-name>] [--task <BLI-NNNN> | --subject <name>] [--repo <label>] [--tool claude|codex|opencode] [--since <window>] [--until <iso>] [--cursor <cursor>] [--limit <n>] [--json]",
|
|
194
194
|
" cockpit usage session <id> [--from-window <n>] [--json]",
|
|
195
195
|
" --by-topic groups work types; --by-subject groups open subjects from session summaries.",
|
|
196
196
|
" cockpit ticket mutation-status <request-uuid> [--json] (check a lost write response)",
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/** Route OpenCode's third native format through the existing evidence pack, spool, report queue and durable cursor. */
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { getCollectorRuntimePaths, readLocalCollectorConfig, readLocalCollectorSessionFile, startLocalWorkContext, startLocalWorkContextForAttributedTarget } from "../local-state.js";
|
|
5
|
+
import { writeJsonFileAtomic } from "../local-state-files.js";
|
|
6
|
+
import { reportShelvedSessions } from "../shelf-reports.js";
|
|
7
|
+
import { openCodeCollectionPolicy } from "../opencode-policy.js";
|
|
8
|
+
import { RAW_EVIDENCE_DEFAULT_BYTE_BUDGET, RAW_EVIDENCE_DEFAULT_OBJECT_BUDGET } from "../adapters/raw-evidence.js";
|
|
9
|
+
import { cwdInsideRoots } from "../collection-scope.js";
|
|
10
|
+
import { scoreSignalsAgainstWorktrees, sha256 } from "../adapters/attribution-core.js";
|
|
11
|
+
import { outsideRootsWorkspace, isOutsideRootsWorkspace } from "./backfill-outside-roots.js";
|
|
12
|
+
import { syncLocalAmbientEnvelope } from "../upload.js";
|
|
13
|
+
import { readRawEvidenceCursor, writeRawEvidenceCursor } from "../cursors/raw-evidence-cursor.js";
|
|
14
|
+
import { readOpenCodeInventory, acknowledgeOpenCodeUsage, OPENCODE_CURSOR_FILENAME } from "../opencode-inventory.js";
|
|
15
|
+
import { queueCodexSessionReport, flushPendingCodexSessionReports } from "../upload-session-reports.js";
|
|
16
|
+
import { recordSourceRetryFailure, clearSourceRetryFailure } from "../spool/local-spool.js";
|
|
17
|
+
import { describeError } from "../health-detail.js";
|
|
18
|
+
/** The all-history inventory is resumable through existing per-object acknowledgments, never a second delivery engine. */
|
|
19
|
+
export async function syncOpenCodeSessions(options) {
|
|
20
|
+
const paths = getCollectorRuntimePaths(options.homeDir);
|
|
21
|
+
const now = new Date();
|
|
22
|
+
const summary = { source: "opencode", new_since_last_scan: 0, outside_roots: 0, discovered: 0, eligible: 0, delivered: 0, pending: 0, withheld: 0, unique: 0, coverage: "complete", reasons: [] };
|
|
23
|
+
try {
|
|
24
|
+
const policy = await openCodeCollectionPolicy(options);
|
|
25
|
+
if (!policy.enabled) {
|
|
26
|
+
const held = await readOpenCodeInventory({ homeDir: options.homeDir, stateDir: paths.state_dir, metadataOnly: true, include: () => false, sinceMs: options.sinceMs });
|
|
27
|
+
summary.discovered = held.discovered;
|
|
28
|
+
summary.unique = held.discovered;
|
|
29
|
+
summary.withheld = held.withheld;
|
|
30
|
+
summary.coverage = held.failures.length || policy.reason === "owner_check_unavailable" ? "partial" : held.absent ? "absent" : "withheld";
|
|
31
|
+
summary.reasons.push(policy.reason, ...held.failures.map(failure => failure.reason));
|
|
32
|
+
if (summary.coverage === "partial")
|
|
33
|
+
await recordSourceRetryFailure(paths, { source: "opencode", attemptedAt: now.toISOString(), reason: held.failures[0]?.reason ?? policy.reason });
|
|
34
|
+
console.error("[opencode sync] source withheld", JSON.stringify(summary));
|
|
35
|
+
return summary;
|
|
36
|
+
}
|
|
37
|
+
const attributed = (directory) => scoreSignalsAgainstWorktrees({ cwds: [directory], workspaceRoots: [], originUrls: [], branches: [], headShas: [] }, options.worktrees, { collectionRoots: options.collectionRoots });
|
|
38
|
+
const cursor = await readRawEvidenceCursor(paths, { filename: OPENCODE_CURSOR_FILENAME, sessionsOnly: true });
|
|
39
|
+
const inventory = await readOpenCodeInventory({ homeDir: options.homeDir, stateDir: paths.state_dir, metadataOnly: options.dryRun, sinceMs: options.sinceMs, deliveredSessionIds: new Set(Object.entries(cursor.sessions).filter(([, entry]) => entry.uploaded_object_key).map(([id]) => id)), include: policy.include, countOnly: policy.drawer });
|
|
40
|
+
summary.discovered = inventory.discovered;
|
|
41
|
+
summary.withheld = inventory.withheld;
|
|
42
|
+
summary.eligible = inventory.eligible;
|
|
43
|
+
summary.pending = inventory.eligible - inventory.files.length;
|
|
44
|
+
summary.unique = inventory.discovered;
|
|
45
|
+
summary.reasons.push(...new Set(inventory.failures.map((failure) => failure.reason)));
|
|
46
|
+
if (inventory.absent)
|
|
47
|
+
summary.coverage = "absent";
|
|
48
|
+
if (options.dryRun) {
|
|
49
|
+
summary.pending = inventory.eligible;
|
|
50
|
+
if (inventory.failures.length)
|
|
51
|
+
summary.coverage = "partial";
|
|
52
|
+
return summary;
|
|
53
|
+
}
|
|
54
|
+
if (inventory.shelved.length) {
|
|
55
|
+
const [device, config] = await Promise.all([readLocalCollectorSessionFile(paths), readLocalCollectorConfig(paths)]);
|
|
56
|
+
const shelfReport = await reportShelvedSessions({ paths, dashboardUrl: options.dashboardUrl ?? config?.dashboard_url ?? null,
|
|
57
|
+
deviceToken: device?.device_token ?? null, fetchImpl: options.fetchImpl, allow: config?.shelf_metrics_allow ?? [], now,
|
|
58
|
+
shelved: inventory.shelved.map(file => ({ key: `opencode:${file.session.id}`, source: "opencode", reason: "drawer_shelf", facts: file.facts, recencyMs: file.session.updated_at_ms })) });
|
|
59
|
+
if (shelfReport.failed || shelfReport.not_counted)
|
|
60
|
+
summary.reasons.push("opencode_shelf_metrics_pending");
|
|
61
|
+
}
|
|
62
|
+
summary.new_since_last_scan = inventory.files.filter((file) => !cursor.sessions[file.session.id]).length;
|
|
63
|
+
summary.outside_roots = inventory.files.filter((file) => !cwdInsideRoots(file.session.directory, options.collectionRoots)).length;
|
|
64
|
+
const durableSessions = new Set();
|
|
65
|
+
const groups = new Map();
|
|
66
|
+
for (const file of inventory.files) {
|
|
67
|
+
const worktree = attributed(file.session.directory).worktree ?? outsideRootsWorkspace(options.homeDir);
|
|
68
|
+
const group = groups.get(worktree.worktree_fingerprint) ?? { worktree, files: [] };
|
|
69
|
+
group.files.push(file);
|
|
70
|
+
groups.set(worktree.worktree_fingerprint, group);
|
|
71
|
+
}
|
|
72
|
+
const budget = options.budget ?? { remainingBytes: RAW_EVIDENCE_DEFAULT_BYTE_BUDGET, remainingObjects: RAW_EVIDENCE_DEFAULT_OBJECT_BUDGET };
|
|
73
|
+
for (const { worktree, files } of groups.values()) {
|
|
74
|
+
if (options.deadlineMs && Date.now() >= options.deadlineMs) {
|
|
75
|
+
summary.reasons.push("opencode_deadline_deferred");
|
|
76
|
+
summary.pending = summary.eligible - summary.delivered;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
if (isOutsideRootsWorkspace(worktree))
|
|
80
|
+
await fs.mkdir(worktree.repo_root, { recursive: true, mode: 0o700 });
|
|
81
|
+
const contextOptions = { homeDir: options.homeDir, repoRoot: worktree.repo_root, branch: worktree.branch, leaveActiveContext: true };
|
|
82
|
+
const context = isOutsideRootsWorkspace(worktree) ? await startLocalWorkContextForAttributedTarget(contextOptions, worktree) : await startLocalWorkContext(contextOptions);
|
|
83
|
+
const sync = await syncLocalAmbientEnvelope({ homeDir: options.homeDir, repoRoot: worktree.repo_root, dashboardUrl: options.dashboardUrl,
|
|
84
|
+
rawEvidenceBudget: budget, codexSessionFiles: [], claudeSessionFiles: [], nativeSessionFiles: files.map((file) => ({ local_path: file.local_path, session_id: file.session.id, source: "opencode", redaction: file.redaction })), fetch: options.fetchImpl });
|
|
85
|
+
if (sync.status !== "uploaded")
|
|
86
|
+
summary.reasons.push(sync.failure_reason);
|
|
87
|
+
const rows = files.map((file) => {
|
|
88
|
+
const attribution = attributed(file.session.directory);
|
|
89
|
+
const outcome = sync.raw_evidence_outcomes.find((item) => item.kind === "opencode_jsonl" && item.codex_session_id === file.session.id);
|
|
90
|
+
const pointer = outcome && (outcome.upload_state === "uploaded" || outcome.upload_state === "reused_existing") ? outcome.raw_evidence_pointer_id : null;
|
|
91
|
+
if (!pointer)
|
|
92
|
+
summary.reasons.push(outcome?.reason ?? "opencode_delivery_pending");
|
|
93
|
+
return { source: "opencode", codex_session_id: file.session.id, observed_at: now.toISOString(), attribution_state: attribution.state,
|
|
94
|
+
attribution_reason: attribution.reason, attribution_score: attribution.attribution_score, path_score: attribution.path_score, signals: attribution.signals,
|
|
95
|
+
session_file_hash_sha256: file.revision, session_file_byte_size: file.byte_size, session_file_mtime: new Date(file.session.updated_at_ms).toISOString(), session_facts: file.facts,
|
|
96
|
+
repo_fingerprint: attribution.worktree?.repo_fingerprint, worktree_fingerprint: attribution.worktree?.worktree_fingerprint,
|
|
97
|
+
repo_label: attribution.worktree?.repo_label, branch: attribution.worktree?.branch, cwd_basename: path.basename(file.session.directory), cwd_hash: sha256(Buffer.from(file.session.directory)),
|
|
98
|
+
root_scope: cwdInsideRoots(file.session.directory, options.collectionRoots) ? "inside" : "outside",
|
|
99
|
+
raw_evidence_pointer_id: pointer ?? undefined, upload_state: pointer ? "uploaded" : "not_uploaded", upload_reason: pointer ? "durable_evidence_acknowledged" : outcome?.reason ?? "opencode_delivery_pending" };
|
|
100
|
+
});
|
|
101
|
+
// Durable metadata queue precedes cursor movement; an unavailable report endpoint cannot age sessions out.
|
|
102
|
+
await queueCodexSessionReport({ homeDir: options.homeDir, dashboardUrl: sync.dashboard_url, generatedAt: now.toISOString(), workContextId: context.work_context_id,
|
|
103
|
+
repoLabel: worktree.repo_label, branch: worktree.branch, repoFingerprint: worktree.repo_fingerprint, repoOriginUrl: worktree.repo_origin_url,
|
|
104
|
+
worktreeLabel: worktree.worktree_label, worktreeFingerprint: worktree.worktree_fingerprint, worktreeIsPrimary: worktree.worktree_is_primary, sessions: rows });
|
|
105
|
+
for (const [index, file] of files.entries()) {
|
|
106
|
+
const row = rows[index];
|
|
107
|
+
const pointer = row.raw_evidence_pointer_id ?? null;
|
|
108
|
+
if (pointer) {
|
|
109
|
+
summary.delivered += 1;
|
|
110
|
+
durableSessions.add(file.session.id);
|
|
111
|
+
}
|
|
112
|
+
else
|
|
113
|
+
summary.pending += 1;
|
|
114
|
+
cursor.sessions[file.session.id] = { file_hash_sha256: file.revision, file_mtime_ms: file.session.updated_at_ms, byte_size: file.byte_size, byte_offset: pointer ? file.byte_size : 0,
|
|
115
|
+
state: row.attribution_state, reason: row.attribution_reason, worktree_fingerprint: row.worktree_fingerprint ?? null, uploaded_object_key: pointer,
|
|
116
|
+
uploaded_at: pointer ? now.toISOString() : null, uploaded_byte_size: pointer ? file.byte_size : null, last_seen_at: now.toISOString() };
|
|
117
|
+
}
|
|
118
|
+
cursor.updated_at = now.toISOString();
|
|
119
|
+
await writeRawEvidenceCursor(paths, cursor, { filename: OPENCODE_CURSOR_FILENAME, sessionsOnly: true });
|
|
120
|
+
}
|
|
121
|
+
const report = await flushPendingCodexSessionReports({ homeDir: options.homeDir, fetch: options.fetchImpl });
|
|
122
|
+
if (report.posted && report.failed_count === 0) {
|
|
123
|
+
await acknowledgeOpenCodeUsage(paths.state_dir, durableSessions);
|
|
124
|
+
// The shared pack/spool owns acknowledged bytes now; these are disposable redacted reader derivatives.
|
|
125
|
+
for (const file of inventory.files)
|
|
126
|
+
if (durableSessions.has(file.session.id))
|
|
127
|
+
await fs.unlink(file.local_path).catch(() => undefined);
|
|
128
|
+
}
|
|
129
|
+
if (inventory.files.length && (!report.posted || report.failed_count > 0)) {
|
|
130
|
+
summary.reasons.push(report.reason);
|
|
131
|
+
summary.pending += summary.delivered;
|
|
132
|
+
summary.delivered = 0;
|
|
133
|
+
}
|
|
134
|
+
summary.reasons = [...new Set(summary.reasons)];
|
|
135
|
+
if (summary.pending || summary.reasons.length)
|
|
136
|
+
summary.coverage = "partial";
|
|
137
|
+
if (summary.coverage === "partial")
|
|
138
|
+
await recordSourceRetryFailure(paths, { source: "opencode", attemptedAt: now.toISOString(), reason: summary.reasons[0] ?? "opencode_delivery_pending" });
|
|
139
|
+
else
|
|
140
|
+
await clearSourceRetryFailure(paths, "opencode", now.toISOString());
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
summary.pending = Math.max(summary.pending, summary.eligible - summary.delivered);
|
|
144
|
+
summary.coverage = "partial";
|
|
145
|
+
summary.reasons.push("opencode_sync_failed");
|
|
146
|
+
await recordSourceRetryFailure(paths, { source: "opencode", attemptedAt: now.toISOString(), reason: "opencode_sync_failed" });
|
|
147
|
+
console.error("[opencode sync] pass failed; native sessions remain retryable", JSON.stringify({ reason: "opencode_sync_failed", ...describeError(error) }));
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
if (!options.dryRun) {
|
|
151
|
+
try {
|
|
152
|
+
await writeJsonFileAtomic(path.join(paths.state_dir, "opencode-collection-status.json"), { observed_at: now.toISOString(), ...summary });
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
summary.coverage = "partial";
|
|
156
|
+
summary.reasons.push("opencode_coverage_receipt_write_failed");
|
|
157
|
+
console.error("[opencode sync] coverage receipt not written", JSON.stringify({ reason: "opencode_coverage_receipt_write_failed", ...describeError(error) }));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
console.error("[opencode sync] pass finished", JSON.stringify(summary));
|
|
162
|
+
return summary;
|
|
163
|
+
}
|
|
@@ -21,7 +21,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
24
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
24
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.294");
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -170,4 +170,8 @@ export function recordUnexplainedFailure(ledger) {
|
|
|
170
170
|
label: SYNC_FAILED_WITHOUT_REASON,
|
|
171
171
|
rendered: SYNC_FAILED_WITHOUT_REASON,
|
|
172
172
|
});
|
|
173
|
+
}
|
|
174
|
+
/** Keep native partial outcomes in the same closed failure ledger as legacy sources. */
|
|
175
|
+
export function recordOpenCodeFailures(ledger, native) {
|
|
176
|
+
ledger.fail(native.coverage === "partial", "opencode_scan_read_failed", native.reasons[0] ?? "opencode_delivery_pending");
|
|
173
177
|
}
|
|
@@ -322,7 +322,7 @@ function recordSourceObservation(options, result) {
|
|
|
322
322
|
: (prior?.uploaded_byte_size ?? (durableThisSync ? result.byte_size : null));
|
|
323
323
|
const entry = {
|
|
324
324
|
file_hash_sha256: result.content_hash_sha256,
|
|
325
|
-
uploaded_source_revision: durableThisSync && !isRedactionStubObjectKey(uploadedObjectKey)
|
|
325
|
+
uploaded_source_revision: durableThisSync && !("t3_projection_incomplete" in result && result.t3_projection_incomplete) && !isRedactionStubObjectKey(uploadedObjectKey)
|
|
326
326
|
? result.content_hash_sha256 : prior?.uploaded_source_revision ?? null,
|
|
327
327
|
file_mtime_ms: result.session_file_mtime_ms,
|
|
328
328
|
byte_size: result.byte_size,
|
|
@@ -39,7 +39,7 @@ export async function syncEveryTargetWorktree(options) {
|
|
|
39
39
|
// sessionId -> prior durable pointer for damped sessions (drives the
|
|
40
40
|
// growth_damped count + the skip_main decision).
|
|
41
41
|
const dampedClaudePointers = new Map();
|
|
42
|
-
const rawEvidenceBudget = {
|
|
42
|
+
const rawEvidenceBudget = run.rawEvidenceBudget ?? {
|
|
43
43
|
remainingBytes: RAW_EVIDENCE_DEFAULT_BYTE_BUDGET,
|
|
44
44
|
remainingObjects: RAW_EVIDENCE_DEFAULT_OBJECT_BUDGET,
|
|
45
45
|
};
|
|
@@ -180,6 +180,7 @@ function ambientEnvelopeForWorktree(options) {
|
|
|
180
180
|
.map((result) => ({
|
|
181
181
|
local_path: result.file_path,
|
|
182
182
|
codex_session_id: result.codex_session_id,
|
|
183
|
+
...(result.t3_projection ? { t3_projection: true } : {}),
|
|
183
184
|
...(result.segment_paths ? { segment_paths: result.segment_paths, revision_hash: result.content_hash_sha256 ?? undefined, revision_bytes: result.byte_size } : {}),
|
|
184
185
|
})),
|
|
185
186
|
codexAttributionScan: scan.codexAttribution,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createSyncFailureLedger, recordOpenCodeFailures } from "./session-sync-failures.js";
|
|
1
2
|
import { readLocalWorkContextForRepo } from "../local-state.js";
|
|
2
3
|
/**
|
|
3
4
|
* One attributed sync pass: scan both session stores, attribute what they hold,
|
|
@@ -39,6 +40,8 @@ import { readLocalWorkContextForRepo } from "../local-state.js";
|
|
|
39
40
|
* `runtimeFiles`, or the repo tests stay green while the packed CLI breaks.
|
|
40
41
|
*/
|
|
41
42
|
import os from "node:os";
|
|
43
|
+
import { RAW_EVIDENCE_DEFAULT_BYTE_BUDGET, RAW_EVIDENCE_DEFAULT_OBJECT_BUDGET } from "../adapters/raw-evidence.js";
|
|
44
|
+
import { syncOpenCodeSessions } from "./opencode-sync.js";
|
|
42
45
|
import { getCollectorRuntimePaths, readLocalCollectorConfig, readLocalCollectorSessionFile } from "../local-state.js";
|
|
43
46
|
import { reportShelvedSessions } from "../shelf-reports.js";
|
|
44
47
|
import { splitScanByShelf } from "./session-sync-shelf.js";
|
|
@@ -106,7 +109,7 @@ export async function runAttributedWorktreeSync(requested) {
|
|
|
106
109
|
return heldSyncRun(scannedAll);
|
|
107
110
|
const shelfSplit = splitScanByShelf({ scan: scannedAll, worktrees: requested.worktrees, decision: shelfDecision });
|
|
108
111
|
const scanned = shelfSplit.scan;
|
|
109
|
-
const options = { ...requested, worktrees: shelfSplit.worktrees };
|
|
112
|
+
const options = { ...requested, worktrees: shelfSplit.worktrees, rawEvidenceBudget: requested.rawEvidenceBudget ?? { remainingBytes: RAW_EVIDENCE_DEFAULT_BYTE_BUDGET, remainingObjects: RAW_EVIDENCE_DEFAULT_OBJECT_BUDGET } };
|
|
110
113
|
// BLI-17916: roots are a label, never a gate. Outside-root sessions ride this
|
|
111
114
|
// tick's upload under the synthetic workspace, a few per tick, newest first.
|
|
112
115
|
const scan = await admitOutsideRootSessions({
|
|
@@ -141,8 +144,22 @@ export async function runAttributedWorktreeSync(requested) {
|
|
|
141
144
|
reportCountedSessionFacts(sessions);
|
|
142
145
|
const shelfReports = await sendShelfReports(options, paths, shelfSplit, now);
|
|
143
146
|
const run = await finishAttributedSync(options, { paths, now, plan, scan, sessions, worktreePass });
|
|
147
|
+
const opencode = await syncOpenCodeSessions({ homeDir, worktrees: requested.worktrees, collectionRoots: plan.collectionRoots, dashboardUrl: requested.dashboardUrl, fetchImpl: requested.fetchImpl, budget: options.rawEvidenceBudget });
|
|
148
|
+
const failures = createSyncFailureLedger();
|
|
149
|
+
for (const failure of run.failure_records)
|
|
150
|
+
failures.add(failure);
|
|
151
|
+
recordOpenCodeFailures(failures, opencode);
|
|
144
152
|
return {
|
|
145
153
|
...run,
|
|
154
|
+
opencode,
|
|
155
|
+
summary: { ...run.summary, opencode },
|
|
156
|
+
sessions_new_this_tick: run.sessions_new_this_tick + opencode.new_since_last_scan,
|
|
157
|
+
sessions_outside_root: run.sessions_outside_root + opencode.outside_roots,
|
|
158
|
+
ok: run.ok && opencode.coverage !== "partial",
|
|
159
|
+
failure_reasons: [...run.failure_reasons, ...(opencode.coverage === "partial" ? opencode.reasons.length ? opencode.reasons : ["opencode_delivery_pending"] : [])],
|
|
160
|
+
failure_records: failures.sortedRecords(),
|
|
161
|
+
sessions_observed: run.sessions_observed + opencode.eligible,
|
|
162
|
+
sessions_pending_upload: run.sessions_pending_upload + opencode.pending,
|
|
146
163
|
sessions_shelved: shelfSplit.shelved.length,
|
|
147
164
|
packs_refused: worktreePass.packsRefused ?? 0,
|
|
148
165
|
shelf_reports: shelfReports,
|