@bli-cockpit/cli 0.2.292 → 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/claude-attribution-discovery.js +22 -0
- package/dist/adapters/claude-attribution-memoized-reads.js +3 -3
- package/dist/adapters/claude-attribution.js +5 -2
- package/dist/adapters/codex-attribution-memoized-reads.js +3 -3
- package/dist/adapters/codex-attribution.js +51 -12
- package/dist/adapters/codex-segment-discovery.js +75 -0
- package/dist/adapters/codex-session-revision.js +194 -0
- package/dist/adapters/codex-session-signals.js +2 -2
- package/dist/adapters/local-sources.js +1 -0
- package/dist/adapters/opencode-store.js +201 -0
- package/dist/adapters/pi-session-facts.js +28 -0
- package/dist/adapters/pi-session-reader.js +132 -0
- package/dist/adapters/raw-evidence-attribution-gaps.js +5 -0
- package/dist/adapters/raw-evidence-codex-reader.js +39 -7
- package/dist/adapters/raw-evidence-transcript-body.js +9 -1
- package/dist/adapters/raw-evidence-transcript-file.js +46 -4
- package/dist/adapters/raw-evidence-transcript-stream.js +36 -1
- package/dist/adapters/raw-evidence.js +13 -0
- package/dist/adapters/t3-projection-source.js +96 -0
- package/dist/collection-scope.js +2 -0
- package/dist/commands/backfill-checkpoint.js +3 -1
- package/dist/commands/backfill-history-scan.js +38 -0
- package/dist/commands/backfill-issues.js +13 -20
- package/dist/commands/backfill-output.js +6 -0
- package/dist/commands/backfill-report.js +3 -3
- package/dist/commands/backfill-scan.js +20 -40
- package/dist/commands/backfill-scope.js +2 -0
- package/dist/commands/backfill-upload.js +4 -2
- package/dist/commands/backfill.js +20 -3
- package/dist/commands/codex-revision-facts.js +64 -0
- 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-args-notes-targets.js +35 -0
- package/dist/commands/local-args-tower-notes.js +7 -18
- package/dist/commands/local-help-commands-tower.js +2 -2
- package/dist/commands/local-help-notes.js +2 -1
- package/dist/commands/local-help.js +3 -3
- package/dist/commands/notes-writes.js +24 -0
- package/dist/commands/notes.js +3 -2
- 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 +3 -1
- package/dist/commands/session-sync-scan.js +14 -6
- package/dist/commands/session-sync-shelf.js +2 -0
- package/dist/commands/session-sync-upload.js +3 -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 +94 -14
- package/dist/commands/sources-reconciliation.js +7 -5
- package/dist/commands/sources-verdict.js +5 -2
- package/dist/cursors/raw-evidence-cursor.js +1 -0
- package/dist/image-description-session.js +4 -2
- package/dist/local-state-status.js +18 -1
- package/dist/native-session-roots.js +31 -0
- 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/t3-native-homes.js +64 -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,203 @@
|
|
|
1
|
+
/** OpenCode's one inventory for sync, history and reconciliation, exported through shared secret guards. */
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import { newSessionFactsAccumulator, observeSessionFactsFile, observeSessionFactsRecord, finalizeSessionFacts, nativeObject, openCodeProviderIdentity } from "@bli-cockpit/telemetry-core";
|
|
6
|
+
import { openCodeDatabasePath, openOpenCodeStore, OpenCodeReadError } from "./adapters/opencode-store.js";
|
|
7
|
+
import { openStagingAllowance, roomToStageUpTo, admitToStaging } from "./staging-gate.js";
|
|
8
|
+
import { sanitizeTextEvidenceForUpload } from "./adapters/raw-evidence-sanitize.js";
|
|
9
|
+
import { readJsonFile, writeJsonFileAtomic } from "./local-state-files.js";
|
|
10
|
+
export const OPENCODE_CURSOR_FILENAME = "opencode-raw-evidence.json";
|
|
11
|
+
/** Policy is decided from native directory metadata before exporting any conversation body. */
|
|
12
|
+
export async function readOpenCodeInventory(options) {
|
|
13
|
+
const result = { discovered: 0, sessions: [], eligible: 0, withheld: 0, files: [], shelved: [], failures: [], absent: false };
|
|
14
|
+
let store = null;
|
|
15
|
+
try {
|
|
16
|
+
const databasePath = options.databasePath ?? process.env.OPENCODE_DB ?? openCodeDatabasePath(options.homeDir, process.env.XDG_DATA_HOME);
|
|
17
|
+
store = await openOpenCodeStore(databasePath);
|
|
18
|
+
if (!store) {
|
|
19
|
+
result.absent = true;
|
|
20
|
+
console.error("[opencode inventory] source path absent", JSON.stringify({ source: "opencode", reason: "opencode_source_path_absent" }));
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
const sessions = [];
|
|
24
|
+
let afterId = "";
|
|
25
|
+
while (true) {
|
|
26
|
+
const page = store.list(afterId);
|
|
27
|
+
if (!page.length)
|
|
28
|
+
break;
|
|
29
|
+
sessions.push(...page.filter(session => options.sinceMs === undefined || session.updated_at_ms >= options.sinceMs));
|
|
30
|
+
afterId = page.at(-1).id;
|
|
31
|
+
}
|
|
32
|
+
result.discovered = sessions.length;
|
|
33
|
+
result.sessions = sessions;
|
|
34
|
+
if (options.metadataOnly) {
|
|
35
|
+
for (const session of sessions) {
|
|
36
|
+
if (!options.include || options.include(session))
|
|
37
|
+
result.eligible += 1;
|
|
38
|
+
else
|
|
39
|
+
result.withheld += 1;
|
|
40
|
+
}
|
|
41
|
+
console.error("[opencode inventory] metadata-only scan complete", JSON.stringify({ discovered: result.discovered, eligible: result.eligible, withheld: result.withheld }));
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
const owners = { ...await readUsageOwners(options.stateDir, "opencode-usage-tentative.json"), ...await readUsageOwners(options.stateDir, "opencode-usage-owners.json") };
|
|
45
|
+
const allowance = await openStagingAllowance(options.stateDir, process.env);
|
|
46
|
+
for (const session of sessions.sort((a, b) => Number(options.deliveredSessionIds?.has(a.id) ?? false) - Number(options.deliveredSessionIds?.has(b.id) ?? false) || a.created_at_ms - b.created_at_ms || a.id.localeCompare(b.id))) {
|
|
47
|
+
if (options.include && !options.include(session)) {
|
|
48
|
+
result.withheld += 1;
|
|
49
|
+
if (options.countOnly?.(session)) {
|
|
50
|
+
try {
|
|
51
|
+
result.shelved.push(await exportNativeSession(store, session, options.stateDir, owners, false));
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
result.failures.push({ session_id: session.id, reason: readReason(error) });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
result.eligible += 1;
|
|
60
|
+
try {
|
|
61
|
+
result.files.push(await exportNativeSession(store, session, options.stateDir, owners, true, allowance, options.exportDirectory));
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
result.failures.push({ session_id: session.id, reason: readReason(error) });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!options.readOnlyAccounting)
|
|
68
|
+
await writeJsonFileAtomic(path.join(options.stateDir, "opencode-usage-tentative.json"), owners);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
result.failures.push({ session_id: null, reason: readReason(error) });
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
store?.close();
|
|
75
|
+
}
|
|
76
|
+
console.error("[opencode inventory] scan finished", JSON.stringify({ source: "opencode", discovered: result.discovered, exported: result.files.length, withheld: result.withheld, failed: result.failures.length, reason: result.absent ? "opencode_source_path_absent" : result.failures.length ? "opencode_inventory_partial" : "opencode_inventory_complete" }));
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
async function exportNativeSession(store, session, stateDir, owners, retainContent = true, allowance, exportDirectory) {
|
|
80
|
+
const directory = exportDirectory ?? path.join(stateDir, "opencode-transcripts");
|
|
81
|
+
if (retainContent)
|
|
82
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
83
|
+
const temporary = path.join(directory, `.export-${crypto.randomUUID()}`);
|
|
84
|
+
const output = retainContent ? await fs.open(temporary, "wx", 0o600) : null;
|
|
85
|
+
const hash = crypto.createHash("sha256");
|
|
86
|
+
const factsAccumulator = newSessionFactsAccumulator("opencode");
|
|
87
|
+
let lines = 0;
|
|
88
|
+
let bytes = 0;
|
|
89
|
+
let message = null;
|
|
90
|
+
let identities = [];
|
|
91
|
+
let redactedRecords = 0;
|
|
92
|
+
const redactionRules = new Map();
|
|
93
|
+
let secretMatches = 0;
|
|
94
|
+
const write = async (record) => {
|
|
95
|
+
const part = record.type === "opencode_part" ? nativeObject(record.part) : {};
|
|
96
|
+
if (part.type === "file" && typeof part.url === "string")
|
|
97
|
+
record = { ...record, attachment_source: {
|
|
98
|
+
representation: part.url.startsWith("data:") ? "inline_data_url" : "reference_only",
|
|
99
|
+
referenced_bytes_read: false,
|
|
100
|
+
reason: part.url.startsWith("data:") ? "native_inline_payload_available" : "external_attachment_bytes_not_read",
|
|
101
|
+
} };
|
|
102
|
+
const masked = sanitizeTextEvidenceForUpload({ text: JSON.stringify(record) + "\n", redactedFields: ["opencode_native_record"] });
|
|
103
|
+
if (masked.status === "redacted" && masked.completenessLabel === "redaction_crashed_stubbed")
|
|
104
|
+
throw new OpenCodeReadError("opencode_redaction_failed");
|
|
105
|
+
if (masked.status === "redacted") {
|
|
106
|
+
redactedRecords += 1;
|
|
107
|
+
secretMatches += masked.redaction.secret_like_match_count;
|
|
108
|
+
for (const rule of masked.redaction.rule_counts) {
|
|
109
|
+
const prior = redactionRules.get(rule.rule_id) ?? { rule_id: rule.rule_id, match_count: 0, redacted_char_count: 0 };
|
|
110
|
+
prior.match_count += rule.match_count;
|
|
111
|
+
prior.redacted_char_count += rule.redacted_char_count;
|
|
112
|
+
redactionRules.set(rule.rule_id, prior);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
hash.update(masked.bytes);
|
|
116
|
+
bytes += masked.bytes.length;
|
|
117
|
+
lines += 1;
|
|
118
|
+
try {
|
|
119
|
+
observeSessionFactsRecord(factsAccumulator, JSON.parse(masked.bytes.toString("utf8")));
|
|
120
|
+
}
|
|
121
|
+
catch { /* Masked crash marker remains evidence, not an invented native record. */ }
|
|
122
|
+
if (output) {
|
|
123
|
+
const deferred = roomToStageUpTo(allowance, bytes);
|
|
124
|
+
if (deferred)
|
|
125
|
+
throw new OpenCodeReadError(`opencode_staging_deferred_${deferred}`);
|
|
126
|
+
await output.write(masked.bytes);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const settleUsage = async () => {
|
|
130
|
+
if (!message || message.role !== "assistant")
|
|
131
|
+
return;
|
|
132
|
+
const nativeIdentity = openCodeProviderIdentity(message, identities);
|
|
133
|
+
const identity = nativeIdentity ? crypto.createHash("sha256").update(nativeIdentity).digest("hex") : null;
|
|
134
|
+
const nativeTokens = nativeObject(message.tokens);
|
|
135
|
+
const hasUsage = !message.error && Number.isFinite(nativeObject(message.time).completed) && ["input", "output", "reasoning"].every((key) => typeof nativeTokens[key] === "number" && Number.isFinite(nativeTokens[key]) && Number(nativeTokens[key]) >= 0) && typeof message.modelID === "string";
|
|
136
|
+
const owner = identity ? owners[identity] : null;
|
|
137
|
+
const status = !identity || !hasUsage ? "unknown" : owner && owner !== session.id ? "copied" : "known";
|
|
138
|
+
if (identity && hasUsage && !owner)
|
|
139
|
+
owners[identity] = session.id;
|
|
140
|
+
await write({ type: "opencode_usage", status, identity, owner_session_id: identity ? owners[identity] : null, model: message.modelID ?? null, provider: message.providerID ?? null, tokens: message.tokens ?? null });
|
|
141
|
+
};
|
|
142
|
+
try {
|
|
143
|
+
for (const record of store.records(session.id)) {
|
|
144
|
+
if (record.type === "opencode_message") {
|
|
145
|
+
await settleUsage();
|
|
146
|
+
message = nativeObject(record.info);
|
|
147
|
+
identities = [];
|
|
148
|
+
}
|
|
149
|
+
if (record.type === "opencode_part") {
|
|
150
|
+
const part = nativeObject(record.part);
|
|
151
|
+
if (part.metadata)
|
|
152
|
+
identities.push({ metadata: Object.fromEntries(Object.entries(nativeObject(part.metadata)).map(([name, value]) => [name, { itemId: nativeObject(value).itemId }])) });
|
|
153
|
+
}
|
|
154
|
+
await write(record);
|
|
155
|
+
}
|
|
156
|
+
await settleUsage();
|
|
157
|
+
await write({ type: "opencode_capture", redacted_records: redactedRecords, accounting_scope: "persistent_native_provider_item_ownership_on_this_machine" });
|
|
158
|
+
await output?.close();
|
|
159
|
+
const revision = hash.digest("hex");
|
|
160
|
+
const localPath = retainContent ? path.join(directory, `${session.id}-${revision}.jsonl`) : "";
|
|
161
|
+
if (retainContent) {
|
|
162
|
+
if (allowance) {
|
|
163
|
+
const deferred = admitToStaging(allowance, bytes, { kind: "opencode_jsonl" });
|
|
164
|
+
if (deferred)
|
|
165
|
+
throw new OpenCodeReadError(`opencode_staging_deferred_${deferred}`);
|
|
166
|
+
}
|
|
167
|
+
await fs.rename(temporary, localPath);
|
|
168
|
+
}
|
|
169
|
+
observeSessionFactsFile(factsAccumulator, { bytes, lines });
|
|
170
|
+
const facts = finalizeSessionFacts(factsAccumulator);
|
|
171
|
+
return { session, local_path: localPath, byte_size: bytes, revision, facts,
|
|
172
|
+
...(secretMatches ? { redaction: { schema_version: "raw-evidence-redaction.v1", status: "sanitized", mode: "deterministic_text_replacement",
|
|
173
|
+
applied_by: ["local_collector"], rule_counts: [...redactionRules.values()], secret_like_match_count: secretMatches,
|
|
174
|
+
redacted_fields: ["opencode_native_record"], redacted_ranges: [], sanitized_content_hash_sha256: revision, sanitized_byte_size: bytes } } : {}) };
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
await output?.close().catch(() => undefined);
|
|
178
|
+
if (retainContent)
|
|
179
|
+
await fs.unlink(temporary).catch(() => undefined);
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function readReason(error) { return error instanceof OpenCodeReadError ? error.reason : "opencode_export_failed"; }
|
|
184
|
+
async function readUsageOwners(stateDir, filename) {
|
|
185
|
+
const saved = await readJsonFile(path.join(stateDir, filename)).catch((error) => {
|
|
186
|
+
if (error.code === "ENOENT")
|
|
187
|
+
return {};
|
|
188
|
+
throw new OpenCodeReadError("opencode_accounting_cursor_unreadable");
|
|
189
|
+
});
|
|
190
|
+
if (!saved || typeof saved !== "object" || Array.isArray(saved) || Object.entries(saved).some(([key, owner]) => !/^[a-f0-9]{64}$/.test(key) || typeof owner !== "string" || !/^ses_[A-Za-z0-9]+$/.test(owner)))
|
|
191
|
+
throw new OpenCodeReadError("opencode_accounting_cursor_invalid");
|
|
192
|
+
return saved;
|
|
193
|
+
}
|
|
194
|
+
/** A local usage owner becomes durable only after both evidence and metadata were acknowledged. */
|
|
195
|
+
export async function acknowledgeOpenCodeUsage(stateDir, sessions) {
|
|
196
|
+
const committed = await readUsageOwners(stateDir, "opencode-usage-owners.json");
|
|
197
|
+
const tentative = await readUsageOwners(stateDir, "opencode-usage-tentative.json");
|
|
198
|
+
for (const [identity, owner] of Object.entries(tentative))
|
|
199
|
+
if (sessions.has(owner))
|
|
200
|
+
committed[identity] ??= owner;
|
|
201
|
+
await writeJsonFileAtomic(path.join(stateDir, "opencode-usage-owners.json"), committed);
|
|
202
|
+
console.error("[opencode accounting] acknowledged native identity owners", JSON.stringify({ reason: "durable_native_usage_acknowledged", identities: Object.keys(committed).length, sessions: sessions.size }));
|
|
203
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Apply existing collection choices to native directory metadata before exporting OpenCode evidence. */
|
|
2
|
+
import { getCollectorRuntimePaths, readLocalCollectorConfig } from "./local-state.js";
|
|
3
|
+
import { readSourceSwitches } from "./source-opt-out.js";
|
|
4
|
+
import { settleCollectionScope, sessionInsideRoots } from "./collection-scope.js";
|
|
5
|
+
import { decideShelfMode, sessionShelf } from "./commands/session-sync-shelf.js";
|
|
6
|
+
import { scoreSignalsAgainstWorktrees } from "./adapters/attribution-core.js";
|
|
7
|
+
export async function openCodeCollectionPolicy(options) {
|
|
8
|
+
const paths = getCollectorRuntimePaths(options.homeDir);
|
|
9
|
+
const config = await readLocalCollectorConfig(paths).catch((error) => { if (error.code === "ENOENT")
|
|
10
|
+
return null; throw error; });
|
|
11
|
+
const switches = await readSourceSwitches(config, paths, "opencode_inventory", { fetch: options.fetchImpl, dashboardUrl: options.dashboardUrl });
|
|
12
|
+
const shelf = await decideShelfMode({ homeDir: options.homeDir, dashboardUrl: options.dashboardUrl ?? null, fetchImpl: options.fetchImpl ?? globalThis.fetch });
|
|
13
|
+
const scope = await settleCollectionScope(paths.state_dir, config?.collection_scope);
|
|
14
|
+
const roots = options.collectionRoots ?? config?.default_repo_paths ?? [];
|
|
15
|
+
const enabled = switches.opencode && !switches.held && shelf.mode !== "hold";
|
|
16
|
+
const reason = switches.held || shelf.mode === "hold" ? "owner_check_unavailable" : !switches.opencode ? "raw_evidence_upload_disabled" : "opencode_existing_collection_policy";
|
|
17
|
+
const drawer = (session) => {
|
|
18
|
+
const attribution = scoreSignalsAgainstWorktrees({ cwds: [session.directory], workspaceRoots: [], originUrls: [], branches: [], headShas: [] }, options.worktrees ?? [], { collectionRoots: roots });
|
|
19
|
+
return shelf.mode === "founder" && sessionShelf({ ...attribution, local_cwd: session.directory, session_facts: null }, shelf.rules).shelf === "drawer";
|
|
20
|
+
};
|
|
21
|
+
return { enabled, reason, drawer, include: (session) => {
|
|
22
|
+
if (!enabled)
|
|
23
|
+
return false;
|
|
24
|
+
const attribution = scoreSignalsAgainstWorktrees({ cwds: [session.directory], workspaceRoots: [], originUrls: [], branches: [], headShas: [] }, options.worktrees ?? [], { collectionRoots: roots });
|
|
25
|
+
if (scope.scope === "roots_only" && !sessionInsideRoots({ ...attribution, local_cwd: session.directory }, roots))
|
|
26
|
+
return false;
|
|
27
|
+
return !drawer(session);
|
|
28
|
+
} };
|
|
29
|
+
}
|
|
@@ -36,7 +36,7 @@ import { LineEdits, SeenBlocks, isJsonObject } from "./slim-support.js";
|
|
|
36
36
|
* once, which is intended: the stored bytes are no longer what the new rules would produce.
|
|
37
37
|
* `slim-rules-version.test.ts` fails when a rule changes without this moving.
|
|
38
38
|
*/
|
|
39
|
-
export const SLIM_RULES_VERSION = "slim-rules.
|
|
39
|
+
export const SLIM_RULES_VERSION = "slim-rules.v3";
|
|
40
40
|
/** The text an image marker starts with; a line carrying one is a modified line (an image became a marker). */
|
|
41
41
|
const IMAGE_MARKER_START = "[image sha256:";
|
|
42
42
|
export function createSlimmer(kind, rulesForTests) {
|
|
@@ -49,8 +49,9 @@ export function createSlimmer(kind, rulesForTests) {
|
|
|
49
49
|
const applyRules = rulesForTests ?? ((record, edits) => {
|
|
50
50
|
if (kind === "claude_code")
|
|
51
51
|
slimClaudeRecord(record, claude, seen, edits);
|
|
52
|
-
else
|
|
52
|
+
else if (kind === "codex")
|
|
53
53
|
slimCodexRecord(record, codex, seen, edits);
|
|
54
|
+
// OpenCode keeps every native field; only the shared acknowledged-image swap changes its payload.
|
|
54
55
|
});
|
|
55
56
|
return {
|
|
56
57
|
slimLine(line) {
|
package/dist/source-opt-out.js
CHANGED
|
@@ -46,7 +46,7 @@ export function resolveSourceSwitches(config, standing) {
|
|
|
46
46
|
const labels = configOptOutLabels(config);
|
|
47
47
|
const verdict = labels.length > 0 ? optOutVerdict(standing) : null;
|
|
48
48
|
if (verdict && !verdict.obeyed) {
|
|
49
|
-
return { codex: true, claude: true, codexReason: null, claudeReason: null, ignored: labels, verdict: verdict.reason, held: false };
|
|
49
|
+
return { opencode: true, codex: true, claude: true, codexReason: null, claudeReason: null, ignored: labels, verdict: verdict.reason, held: false };
|
|
50
50
|
}
|
|
51
51
|
const all = config?.raw_evidence_upload === "disabled";
|
|
52
52
|
const codexReason = config?.collect_codex_jsonl === false
|
|
@@ -56,6 +56,7 @@ export function resolveSourceSwitches(config, standing) {
|
|
|
56
56
|
? CLAUDE_OPT_OUT_REASON
|
|
57
57
|
: all ? ALL_SOURCES_OPT_OUT_REASON : null;
|
|
58
58
|
return {
|
|
59
|
+
opencode: !all,
|
|
59
60
|
codex: codexReason === null,
|
|
60
61
|
claude: claudeReason === null,
|
|
61
62
|
codexReason,
|
|
@@ -100,7 +100,7 @@ export function parseMetadataOnlySessionReportRows(value, fieldPath) {
|
|
|
100
100
|
return sessions;
|
|
101
101
|
}
|
|
102
102
|
function parseRetrySource(value, fieldPath) {
|
|
103
|
-
if (value === "codex" || value === "claude_code")
|
|
103
|
+
if (value === "codex" || value === "claude_code" || value === "opencode")
|
|
104
104
|
return value;
|
|
105
105
|
return invalidUploadSpoolState(fieldPath, '"codex" or "claude_code"');
|
|
106
106
|
}
|
package/dist/state-dir-audit.js
CHANGED
|
@@ -37,6 +37,8 @@ export const STUDIES_DIR_SUFFIX = "-studies";
|
|
|
37
37
|
* collector name; none does today, and the test pins that.
|
|
38
38
|
*/
|
|
39
39
|
export const STATE_DIR_RULES = [
|
|
40
|
+
{ name: "opencode-transcripts", match: "exact", class: "state", bound: "redacted native reader derivatives; existing staging allowance and state ceiling govern every write; current derivatives removed after durable evidence and metadata acknowledgments" },
|
|
41
|
+
{ name: "opencode-", match: "prefix", class: "state", bound: "native identity cursors and metadata-only coverage receipt; overwritten by collection" },
|
|
40
42
|
{ name: "raw-evidence", match: "exact", class: "staging", bound: "staged copies go the tick the server commits them; 256 MiB cap; the staging gate stops new copies at the cap, on a low disk or over the 512 MiB ceiling; uncommitted evidence is never deleted, only counted" },
|
|
41
43
|
{ name: "sync.log", match: "prefix", class: "logs", bound: "rotated every tick at 10 MiB, at most 2 archives (20 MB) per stream" },
|
|
42
44
|
{ name: "sync.err.log", match: "prefix", class: "logs", bound: "rotated every tick at 10 MiB, at most 2 archives (20 MB) per stream" },
|
|
@@ -68,6 +68,7 @@ export const SYNC_FAILURE_LABELS = [
|
|
|
68
68
|
/** The session store itself could not be read during the scan. */
|
|
69
69
|
"codex_scan_read_failed",
|
|
70
70
|
"claude_scan_read_failed",
|
|
71
|
+
"opencode_scan_read_failed",
|
|
71
72
|
/** Sessions were observed and their metadata report did not reach the door. */
|
|
72
73
|
"session_report_unposted",
|
|
73
74
|
/** The gate is real and this file forgot to say why — a bug, made visible. */
|
|
@@ -104,6 +105,7 @@ export const SYNC_FAILURE_LABEL_CLASSES = {
|
|
|
104
105
|
claude_session_read_failed: "sync_failed",
|
|
105
106
|
codex_scan_read_failed: "sync_failed",
|
|
106
107
|
claude_scan_read_failed: "sync_failed",
|
|
108
|
+
opencode_scan_read_failed: "sync_failed",
|
|
107
109
|
session_report_unposted: "sync_failed",
|
|
108
110
|
sync_failed_reason_not_recorded: "sync_failed",
|
|
109
111
|
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Imported native references reveal homes without opening T3 settings or credentials. */
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
const MAX_IMPORTED_REFERENCES = 10_000;
|
|
6
|
+
/** Read only the installed 0.1.7 import-path projection, including live WAL state. */
|
|
7
|
+
export function t3ImportedNativeHomes(homeDir) {
|
|
8
|
+
const databasePath = path.join(homeDir, ".t3", "userdata", "statev2.sqlite");
|
|
9
|
+
if (!existsSync(databasePath))
|
|
10
|
+
return [];
|
|
11
|
+
let database;
|
|
12
|
+
try {
|
|
13
|
+
// Optional runtime door: the public CLI still supports Node 20. A T3 store on
|
|
14
|
+
// that runtime must name the missing reader rather than report complete coverage.
|
|
15
|
+
const { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
|
|
16
|
+
database = new DatabaseSync(databasePath, { readOnly: true });
|
|
17
|
+
// Read explicit session columns only. No messages, settings, auth tables,
|
|
18
|
+
// attached databases or application write pragmas. One statement is a WAL snapshot.
|
|
19
|
+
const references = database.prepare(`
|
|
20
|
+
SELECT DISTINCT json_extract(reference.value, '$.provider') AS provider,
|
|
21
|
+
json_extract(reference.value, '$.filePath') AS file_path
|
|
22
|
+
FROM provider_session_runtime AS session,
|
|
23
|
+
json_each(session.runtime_payload_json, '$.importedTranscripts') AS reference
|
|
24
|
+
LIMIT ${MAX_IMPORTED_REFERENCES + 1}
|
|
25
|
+
`).all();
|
|
26
|
+
if (references.length > MAX_IMPORTED_REFERENCES)
|
|
27
|
+
throw new Error("t3_import_inventory_capped");
|
|
28
|
+
const homes = new Map();
|
|
29
|
+
for (const reference of references) {
|
|
30
|
+
const home = nativeHomeFromImport(reference.provider, reference.file_path);
|
|
31
|
+
if (home)
|
|
32
|
+
homes.set(`${home.source}:${home.home}`, home);
|
|
33
|
+
}
|
|
34
|
+
console.error("[native-session-roots] T3 import homes discovered", JSON.stringify({
|
|
35
|
+
reason: "t3_import_homes_read", references: references.length, unique_homes: homes.size,
|
|
36
|
+
}));
|
|
37
|
+
return [...homes.values()];
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
// Never turn an unreadable/capped inventory into a green empty scan.
|
|
41
|
+
const reason = error instanceof Error && error.message === "t3_import_inventory_capped"
|
|
42
|
+
? "t3_import_inventory_capped" : "t3_import_inventory_unreadable";
|
|
43
|
+
console.error("[native-session-roots] T3 discovery failed", JSON.stringify({ reason,
|
|
44
|
+
error_code: error?.code ?? "unknown" }));
|
|
45
|
+
throw new Error(reason);
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
database?.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function nativeHomeFromImport(provider, filePath) {
|
|
52
|
+
if (typeof filePath !== "string" || !path.isAbsolute(filePath) || !filePath.endsWith(".jsonl"))
|
|
53
|
+
return null;
|
|
54
|
+
const parts = path.normalize(filePath).split(path.sep);
|
|
55
|
+
const source = provider === "codex" ? "codex" : provider === "claudeAgent" ? "claude_code" : null;
|
|
56
|
+
if (!source)
|
|
57
|
+
return null;
|
|
58
|
+
const marker = source === "codex"
|
|
59
|
+
? Math.max(parts.lastIndexOf("sessions"), parts.lastIndexOf("archived_sessions"))
|
|
60
|
+
: parts.lastIndexOf("projects");
|
|
61
|
+
if (marker < 1)
|
|
62
|
+
return null;
|
|
63
|
+
return { source, home: parts.slice(0, marker).join(path.sep) || path.parse(filePath).root };
|
|
64
|
+
}
|
|
@@ -164,6 +164,7 @@ async function collectEnvelopeSources(options, config, paths, repoRoot, uploadCo
|
|
|
164
164
|
rawEvidenceIncludeClaudeJsonl: switches.claude ? options.rawEvidenceIncludeClaudeJsonl : false,
|
|
165
165
|
rawEvidenceCodexSessionFiles: options.codexSessionFiles,
|
|
166
166
|
rawEvidenceCodexAttributionScan: options.codexAttributionScan,
|
|
167
|
+
rawEvidenceNativeSessionFiles: options.nativeSessionFiles,
|
|
167
168
|
rawEvidenceClaudeSessionFiles: options.claudeSessionFiles,
|
|
168
169
|
rawEvidenceClaudeAttributionScan: options.claudeAttributionScan,
|
|
169
170
|
rawEvidenceSkipContentHashes: reusableContentHashes(options, {
|
|
@@ -9,9 +9,12 @@ import { DELIVERY_BACKOFF_HOLDING_REASON, summarizeStuckEvidence, terminalDelive
|
|
|
9
9
|
*/
|
|
10
10
|
export function retrySourcesForFailedSync(options, facts) {
|
|
11
11
|
const sources = new Set();
|
|
12
|
+
if (options.nativeSessionFiles?.length)
|
|
13
|
+
sources.add("opencode");
|
|
12
14
|
if ((options.codexSessionFiles?.length ?? 0) > 0 ||
|
|
13
15
|
(options.codexAttributionScan?.directory_read_failed_count ?? 0) > 0 ||
|
|
14
16
|
(options.codexAttributionScan?.stat_failed_count ?? 0) > 0 ||
|
|
17
|
+
Boolean(options.codexAttributionScan?.projection_failure_reason) ||
|
|
15
18
|
facts?.evidence_completeness.source_counts.some((count) => count.source.startsWith("codex_") &&
|
|
16
19
|
count.scanned_count + count.included_count + count.reused_count > 0)) {
|
|
17
20
|
sources.add("codex");
|
|
@@ -99,9 +102,16 @@ export function hasRetryableEvidenceGap(facts, outcomes) {
|
|
|
99
102
|
* Retryable and failed are separate questions: a hold remains missing evidence
|
|
100
103
|
* without making the scheduled tick itself a failed attempt.
|
|
101
104
|
*/
|
|
105
|
+
const retryableT3ProjectionFailures = new Set([
|
|
106
|
+
"t3_projection_unreadable", "t3_projection_inventory_capped", "t3_projection_messages_capped",
|
|
107
|
+
"t3_projection_bytes_capped", "t3_projection_mapping_invalid",
|
|
108
|
+
"t3_native_identity_index_capped",
|
|
109
|
+
]);
|
|
102
110
|
function isRetryableEvidenceSkipReason(reason) {
|
|
103
111
|
if (reason === DELIVERY_BACKOFF_HOLDING_REASON)
|
|
104
112
|
return true;
|
|
113
|
+
if (retryableT3ProjectionFailures.has(reason))
|
|
114
|
+
return true;
|
|
105
115
|
return /(?:read|stat|directory)_failed|session_limit_overflow/iu.test(reason);
|
|
106
116
|
}
|
|
107
117
|
/** Every distinct reason behind the gap, sorted, as one spool-ready label. */
|
|
@@ -81,7 +81,21 @@ export async function reportCodexSessionAttributions(options) {
|
|
|
81
81
|
return emptyCodexSessionReportResult("no_sessions_observed");
|
|
82
82
|
}
|
|
83
83
|
const chunks = [];
|
|
84
|
-
const sessionChunks =
|
|
84
|
+
const sessionChunks = [];
|
|
85
|
+
let current = [];
|
|
86
|
+
let currentBytes = 0;
|
|
87
|
+
for (const session of options.sessions) {
|
|
88
|
+
const bytes = Buffer.byteLength(JSON.stringify(session));
|
|
89
|
+
if (current.length && (current.length >= CODEX_SESSION_REPORT_MAX_SESSIONS || currentBytes + bytes > 3 * 1024 * 1024)) {
|
|
90
|
+
sessionChunks.push(current);
|
|
91
|
+
current = [];
|
|
92
|
+
currentBytes = 0;
|
|
93
|
+
}
|
|
94
|
+
current.push(session);
|
|
95
|
+
currentBytes += bytes;
|
|
96
|
+
}
|
|
97
|
+
if (current.length)
|
|
98
|
+
sessionChunks.push(current);
|
|
85
99
|
for (const [index, sessions] of sessionChunks.entries()) {
|
|
86
100
|
const chunk = await postCodexSessionAttributionChunk({
|
|
87
101
|
...options,
|
|
@@ -253,13 +267,6 @@ export function emptyCodexSessionReportResult(reason) {
|
|
|
253
267
|
chunks: [],
|
|
254
268
|
};
|
|
255
269
|
}
|
|
256
|
-
function chunkArray(items, size) {
|
|
257
|
-
const chunks = [];
|
|
258
|
-
for (let offset = 0; offset < items.length; offset += size) {
|
|
259
|
-
chunks.push(items.slice(offset, offset + size));
|
|
260
|
-
}
|
|
261
|
-
return chunks;
|
|
262
|
-
}
|
|
263
270
|
/**
|
|
264
271
|
* How many rows the server must acknowledge, counted per distinct session
|
|
265
272
|
* rather than per row — the same session observed twice in one chunk is still
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.294",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-verb-help.mjs && node ../../scripts/assert-public-cli-surface-doors.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-exit-contract.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-cli-doctor-json.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@bli-cockpit/memory-mcp": "0.1.
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
32
|
-
"@bli-cockpit/telemetry-core": "0.1.
|
|
30
|
+
"@bli-cockpit/memory-mcp": "0.1.92",
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.179",
|
|
32
|
+
"@bli-cockpit/telemetry-core": "0.1.98",
|
|
33
33
|
"unpdf": "1.8.1"
|
|
34
34
|
},
|
|
35
35
|
"repository": {
|