@bli-cockpit/cli 0.2.292 → 0.2.293
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 +30 -12
- package/dist/adapters/codex-segment-discovery.js +66 -0
- package/dist/adapters/codex-session-revision.js +139 -0
- package/dist/adapters/codex-session-signals.js +2 -2
- package/dist/adapters/pi-session-facts.js +28 -0
- package/dist/adapters/pi-session-reader.js +132 -0
- package/dist/adapters/raw-evidence-codex-reader.js +17 -7
- package/dist/adapters/raw-evidence-transcript-body.js +3 -0
- package/dist/adapters/raw-evidence-transcript-file.js +27 -4
- package/dist/adapters/raw-evidence-transcript-stream.js +33 -1
- 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 +8 -20
- package/dist/commands/backfill-scan.js +18 -40
- package/dist/commands/backfill-upload.js +1 -0
- package/dist/commands/codex-revision-facts.js +64 -0
- 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 +1 -1
- package/dist/commands/local-help-notes.js +2 -1
- package/dist/commands/notes-writes.js +24 -0
- package/dist/commands/notes.js +3 -2
- package/dist/commands/public-root.js +1 -1
- 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 +1 -0
- package/dist/commands/sources-discovery.js +63 -14
- package/dist/commands/sources-reconciliation.js +7 -5
- package/dist/commands/sources-verdict.js +3 -0
- package/dist/cursors/raw-evidence-cursor.js +1 -0
- package/dist/image-description-session.js +4 -2
- package/dist/native-session-roots.js +31 -0
- package/dist/t3-native-homes.js +64 -0
- package/package.json +4 -4
package/dist/commands/notes.js
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
import os from "node:os";
|
|
49
49
|
import { loadPairedSession } from "../tower-client.js";
|
|
50
50
|
import { listNotes, listShelves, showNote, showShelf } from "./notes-reads.js";
|
|
51
|
-
import { uploadNotes, pasteNote, shareNote, summarizeNote, moveNote, placeNote } from "./notes-writes.js";
|
|
51
|
+
import { uploadNotes, pasteNote, shareNote, summarizeNote, moveNote, placeNote, correctNote } from "./notes-writes.js";
|
|
52
52
|
import { runFolderCommand } from "./notes-folders.js";
|
|
53
53
|
import { armNotesWebhook, connectNotetaker, detachNotetaker, listNotesAccounts, showNotesWebhook, syncNotetaker, syncStatusNotetakers, } from "./notes-accounts.js";
|
|
54
54
|
import { showBotUpcoming } from "./notes-bot-upcoming.js";
|
|
@@ -60,7 +60,7 @@ import { defaultRecordDeps, runNotesRecord } from "./notes-record.js";
|
|
|
60
60
|
import { runNotesApp } from "./notes-app.js";
|
|
61
61
|
import { defaultExec, errorMessage } from "./cli-io.js";
|
|
62
62
|
export { listNotes, listShelves, showNote, showShelf } from "./notes-reads.js";
|
|
63
|
-
export { uploadNotes, pasteNote, shareNote, summarizeNote, moveNote, placeNote } from "./notes-writes.js";
|
|
63
|
+
export { uploadNotes, pasteNote, shareNote, summarizeNote, moveNote, placeNote, correctNote } from "./notes-writes.js";
|
|
64
64
|
export { armNotesWebhook, connectNotetaker, detachNotetaker, listNotesAccounts, showNotesWebhook, syncNotetaker, syncStatusNotetakers, } from "./notes-accounts.js";
|
|
65
65
|
export { showGranolaStatus } from "./notes-granola-status.js";
|
|
66
66
|
export { showTranscriptionStats } from "./notes-transcription-stats.js";
|
|
@@ -80,6 +80,7 @@ const NOTES_VERBS = {
|
|
|
80
80
|
paste: pasteNote,
|
|
81
81
|
share: shareNote,
|
|
82
82
|
unshare: shareNote,
|
|
83
|
+
correct: correctNote,
|
|
83
84
|
summarize: summarizeNote,
|
|
84
85
|
move: moveNote,
|
|
85
86
|
place: placeNote,
|
|
@@ -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.293");
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -317,11 +317,13 @@ function recordSourceObservation(options, result) {
|
|
|
317
317
|
const uploadedAt = uploadedThisSync
|
|
318
318
|
? options.now.toISOString()
|
|
319
319
|
: (prior?.uploaded_at ?? (durableThisSync ? options.now.toISOString() : null));
|
|
320
|
-
const uploadedByteSize = uploadedThisSync
|
|
320
|
+
const uploadedByteSize = uploadedThisSync || (durableThisSync && options.source === "codex")
|
|
321
321
|
? result.byte_size
|
|
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)
|
|
326
|
+
? result.content_hash_sha256 : prior?.uploaded_source_revision ?? null,
|
|
325
327
|
file_mtime_ms: result.session_file_mtime_ms,
|
|
326
328
|
byte_size: result.byte_size,
|
|
327
329
|
// Durable byte offset reflects how many bytes are durable remotely (the
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nativeSessionRoots } from "../native-session-roots.js";
|
|
1
2
|
/**
|
|
2
3
|
* Reading both session stores, and settling what the reading owes.
|
|
3
4
|
*
|
|
@@ -9,7 +10,7 @@
|
|
|
9
10
|
*/
|
|
10
11
|
import path from "node:path";
|
|
11
12
|
import { saveRootWorkScan } from "../root-work.js";
|
|
12
|
-
import { CODEX_ATTRIBUTION_SCAN_WINDOW_SESSION_LIMIT, CODEX_ATTRIBUTION_SCAN_WINDOW_MINUTES,
|
|
13
|
+
import { CODEX_ATTRIBUTION_SCAN_WINDOW_SESSION_LIMIT, CODEX_ATTRIBUTION_SCAN_WINDOW_MINUTES, scanAndAttributeCodexSessions, } from "../adapters/codex-attribution.js";
|
|
13
14
|
import { scanAndAttributeClaudeSessions, } from "../adapters/claude-attribution.js";
|
|
14
15
|
import { CLAUDE_CURSOR_FILENAME } from "../cursors/raw-evidence-cursor.js";
|
|
15
16
|
import { isRedactionStubObjectKey } from "../cursors/raw-evidence-stub-entry.js";
|
|
@@ -31,9 +32,10 @@ const CLAUDE_DEFAULT_WINDOW_MINUTES = 24 * 60;
|
|
|
31
32
|
*/
|
|
32
33
|
export async function scanAndAttributeBothSources(options) {
|
|
33
34
|
const { plan } = options;
|
|
35
|
+
const roots = nativeSessionRoots(options.homeDir);
|
|
34
36
|
const codexScanned = await scanCodexSessionsForSync({
|
|
35
37
|
codexEnabled: plan.codexEnabled,
|
|
36
|
-
|
|
38
|
+
roots,
|
|
37
39
|
worktrees: options.worktrees,
|
|
38
40
|
now: options.now,
|
|
39
41
|
collectionRoots: plan.collectionRoots,
|
|
@@ -48,7 +50,7 @@ export async function scanAndAttributeBothSources(options) {
|
|
|
48
50
|
const firstRunBackfill = plan.claudeEnabled && !claudeCursorExists;
|
|
49
51
|
const claudeScanned = await scanClaudeSessionsForSync({
|
|
50
52
|
claudeEnabled: plan.claudeEnabled,
|
|
51
|
-
|
|
53
|
+
roots,
|
|
52
54
|
worktrees: options.worktrees,
|
|
53
55
|
now: options.now,
|
|
54
56
|
collectionRoots: plan.collectionRoots,
|
|
@@ -58,6 +60,10 @@ export async function scanAndAttributeBothSources(options) {
|
|
|
58
60
|
outsideRootCatchUpPending: plan.outsideRootCatchUpPending,
|
|
59
61
|
stateDir: options.paths.state_dir,
|
|
60
62
|
});
|
|
63
|
+
if (roots.discoveryFailed) {
|
|
64
|
+
codexScanned.directory_read_failed_count += 1;
|
|
65
|
+
claudeScanned.project_dir_read_failed_count += 1;
|
|
66
|
+
}
|
|
61
67
|
// BLI-23590: under `roots_only` a session outside every approved root leaves the result HERE, before the retry
|
|
62
68
|
// bookkeeping, the local work scan, the upload, the report or the git scan can see it.
|
|
63
69
|
let codexAttribution = codexScanned;
|
|
@@ -117,7 +123,7 @@ async function scanCodexSessionsForSync(options) {
|
|
|
117
123
|
const readMemo = await openTranscriptReadMemo(options.stateDir, "codex_attribution").catch(() => undefined);
|
|
118
124
|
try {
|
|
119
125
|
let scan = await scanAndAttributeCodexSessions({
|
|
120
|
-
sessionsDirs:
|
|
126
|
+
sessionsDirs: options.roots.codex,
|
|
121
127
|
worktrees: options.worktrees,
|
|
122
128
|
now: options.now,
|
|
123
129
|
sinceMinutes,
|
|
@@ -130,7 +136,7 @@ async function scanCodexSessionsForSync(options) {
|
|
|
130
136
|
// capped scan still returns every session it found instead of silently
|
|
131
137
|
// truncating at the window's default limit.
|
|
132
138
|
scan = await scanAndAttributeCodexSessions({
|
|
133
|
-
sessionsDirs:
|
|
139
|
+
sessionsDirs: options.roots.codex,
|
|
134
140
|
worktrees: options.worktrees,
|
|
135
141
|
now: options.now,
|
|
136
142
|
sinceMinutes,
|
|
@@ -159,12 +165,13 @@ async function scanClaudeSessionsForSync(options) {
|
|
|
159
165
|
: options.firstRunBackfill
|
|
160
166
|
? CLAUDE_FIRST_RUN_BACKFILL_MINUTES
|
|
161
167
|
: undefined, options.outsideRootCatchUpPending, CLAUDE_DEFAULT_WINDOW_MINUTES);
|
|
162
|
-
const projectsDir =
|
|
168
|
+
const { claudeProjects: projectsDir, claudeProjectDirs: projectsDirs } = options.roots;
|
|
163
169
|
// BLI-22797: one memo for both passes, settled once, so an unchanged file is never opened.
|
|
164
170
|
const readMemo = await openTranscriptReadMemo(options.stateDir, "attribution").catch(() => undefined);
|
|
165
171
|
try {
|
|
166
172
|
let scan = await scanAndAttributeClaudeSessions({
|
|
167
173
|
projectsDir,
|
|
174
|
+
projectsDirs,
|
|
168
175
|
worktrees: options.worktrees,
|
|
169
176
|
now: options.now,
|
|
170
177
|
collectionRoots: options.collectionRoots,
|
|
@@ -174,6 +181,7 @@ async function scanClaudeSessionsForSync(options) {
|
|
|
174
181
|
if (scan.session_limit_applied) {
|
|
175
182
|
scan = await scanAndAttributeClaudeSessions({
|
|
176
183
|
projectsDir,
|
|
184
|
+
projectsDirs,
|
|
177
185
|
worktrees: options.worktrees,
|
|
178
186
|
now: options.now,
|
|
179
187
|
collectionRoots: options.collectionRoots,
|
|
@@ -34,6 +34,8 @@ export function sessionShelf(result, rules) {
|
|
|
34
34
|
return { shelf: "team", reason: "routing_inactive" };
|
|
35
35
|
if (rules.failed)
|
|
36
36
|
return { shelf: "drawer", reason: "shelf_resolve_failed" };
|
|
37
|
+
if (result.local_cwds && (result.local_cwds.length === 0 || result.local_cwds.some(cwd => !cwd || resolveShelfWith(rules, cwd).shelf !== "team")))
|
|
38
|
+
return { shelf: "drawer", reason: "shelf_identity_mismatch" };
|
|
37
39
|
if (result.state === "ambiguous")
|
|
38
40
|
return { shelf: "drawer", reason: "ambiguous_attribution" };
|
|
39
41
|
const byWorktree = result.worktree && !isOutsideRootsWorkspace(result.worktree)
|
|
@@ -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.segment_paths ? { segment_paths: result.segment_paths, revision_hash: result.content_hash_sha256 ?? undefined, revision_bytes: result.byte_size } : {}),
|
|
183
184
|
})),
|
|
184
185
|
codexAttributionScan: scan.codexAttribution,
|
|
185
186
|
claudeSessionFiles,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { openCodexSessionRevision } from "../adapters/codex-session-revision.js";
|
|
2
|
+
import { nativeSessionRoots } from "../native-session-roots.js";
|
|
1
3
|
/**
|
|
2
4
|
* Every agent transcript on this machine, named the way the collector names
|
|
3
5
|
* its upload (BLI-11122).
|
|
@@ -29,13 +31,7 @@ import { SESSION_FILE_UUID_PATTERN, sanitizeSessionId, sessionIdFromFileName, sh
|
|
|
29
31
|
import { isSecretLikePath, safeKeySegment } from "../adapters/raw-evidence-keys.js";
|
|
30
32
|
import { describeError } from "../health-detail.js";
|
|
31
33
|
export function sourcesRoots(homeDir, pathApi = path) {
|
|
32
|
-
return
|
|
33
|
-
codex: [
|
|
34
|
-
pathApi.join(homeDir, ".codex", "sessions"),
|
|
35
|
-
pathApi.join(homeDir, ".codex", "archived_sessions"),
|
|
36
|
-
],
|
|
37
|
-
claudeProjects: pathApi.join(homeDir, ".claude", "projects"),
|
|
38
|
-
};
|
|
34
|
+
return nativeSessionRoots(homeDir, pathApi);
|
|
39
35
|
}
|
|
40
36
|
/** Which kind of transcript a path is, or null for anything the collector does not upload as one. */
|
|
41
37
|
export function classifyTranscriptPath(filePath, roots, pathApi = path) {
|
|
@@ -46,7 +42,13 @@ export function classifyTranscriptPath(filePath, roots, pathApi = path) {
|
|
|
46
42
|
if (rel && !rel.startsWith("..") && !pathApi.isAbsolute(rel))
|
|
47
43
|
return { kind: "codex_rollout" };
|
|
48
44
|
}
|
|
49
|
-
const
|
|
45
|
+
const claudeRoot = (roots.claudeProjectDirs ?? [roots.claudeProjects]).find((root) => {
|
|
46
|
+
const relative = pathApi.relative(root, filePath);
|
|
47
|
+
return relative && !relative.startsWith("..") && !pathApi.isAbsolute(relative);
|
|
48
|
+
});
|
|
49
|
+
if (!claudeRoot)
|
|
50
|
+
return null;
|
|
51
|
+
const rel = pathApi.relative(claudeRoot, filePath);
|
|
50
52
|
if (!rel || rel.startsWith("..") || pathApi.isAbsolute(rel))
|
|
51
53
|
return null;
|
|
52
54
|
const parts = rel.split(pathApi.sep);
|
|
@@ -128,15 +130,18 @@ function trimmed(value) {
|
|
|
128
130
|
}
|
|
129
131
|
/**
|
|
130
132
|
* Walk every root, name every transcript. `olderThanMs` keeps only files whose
|
|
131
|
-
* mtime is before it
|
|
133
|
+
* revision mtime is before it. Native identities are read before that filter
|
|
134
|
+
* so an older segment cannot be misreported as the whole retained session.
|
|
132
135
|
*/
|
|
133
136
|
export async function discoverLocalTranscripts(options) {
|
|
134
137
|
const roots = sourcesRoots(options.homeDir);
|
|
135
|
-
const result = { files: [], newer_count: 0, directory_read_failed: 0, file_read_failed: 0 };
|
|
138
|
+
const result = { files: [], newer_count: 0, directory_read_failed: roots.discoveryFailed ? 1 : 0, file_read_failed: 0 };
|
|
136
139
|
let firstFailure = null;
|
|
137
140
|
const recordFailure = (error) => { firstFailure ??= describeError(error); };
|
|
138
141
|
const found = await findTranscriptFiles(options, roots, result, recordFailure);
|
|
139
142
|
await nameTranscriptFiles(found, result, recordFailure);
|
|
143
|
+
await groupCodexRevisions(result);
|
|
144
|
+
result.files = result.files.filter(f => options.olderThanMs === null || f.mtime_ms < options.olderThanMs);
|
|
140
145
|
if (result.directory_read_failed > 0 || result.file_read_failed > 0) {
|
|
141
146
|
console.error("[collector sources] some transcripts could not be read; they are left out of the answer", JSON.stringify({
|
|
142
147
|
reason: "sources_discovery_incomplete",
|
|
@@ -153,12 +158,17 @@ function shapeOrder(shape) {
|
|
|
153
158
|
}
|
|
154
159
|
async function findTranscriptFiles(options, roots, result, recordFailure) {
|
|
155
160
|
const found = [];
|
|
156
|
-
const stack = options.skipAll ? [] : [...(options.skipCodex ? [] : roots.codex), ...(options.skipClaude ? [] : [roots.claudeProjects])];
|
|
161
|
+
const stack = options.skipAll ? [] : [...(options.skipCodex ? [] : roots.codex), ...(options.skipClaude ? [] : (roots.claudeProjectDirs ?? [roots.claudeProjects]))];
|
|
157
162
|
const seen = new Set();
|
|
163
|
+
const visitedDirectories = new Set();
|
|
158
164
|
while (stack.length > 0) {
|
|
159
165
|
const dir = stack.pop();
|
|
160
166
|
let entries;
|
|
161
167
|
try {
|
|
168
|
+
const directoryIdentity = await fs.realpath(dir);
|
|
169
|
+
if (visitedDirectories.has(directoryIdentity))
|
|
170
|
+
continue;
|
|
171
|
+
visitedDirectories.add(directoryIdentity);
|
|
162
172
|
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
163
173
|
}
|
|
164
174
|
catch (error) {
|
|
@@ -187,10 +197,8 @@ async function findTranscriptFiles(options, roots, result, recordFailure) {
|
|
|
187
197
|
});
|
|
188
198
|
if (!stat)
|
|
189
199
|
continue;
|
|
190
|
-
if (options.olderThanMs !== null && stat.mtimeMs >= options.olderThanMs)
|
|
200
|
+
if (options.olderThanMs !== null && stat.mtimeMs >= options.olderThanMs)
|
|
191
201
|
result.newer_count += 1;
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
202
|
found.push({ file: full, size: stat.size, mtimeMs: stat.mtimeMs, shape });
|
|
195
203
|
}
|
|
196
204
|
}
|
|
@@ -237,4 +245,45 @@ async function nameTranscriptFiles(found, result, recordFailure) {
|
|
|
237
245
|
key: transcriptKeyFor(source, named.session_id, item.shape.kind === "claude_subagent" ? item.shape.sidecar_stem : null),
|
|
238
246
|
});
|
|
239
247
|
}
|
|
248
|
+
}
|
|
249
|
+
/** One source row/pointer names the complete native session revision, never an arbitrary segment. */
|
|
250
|
+
async function groupCodexRevisions(result) {
|
|
251
|
+
const groups = new Map();
|
|
252
|
+
for (const file of result.files)
|
|
253
|
+
if (file.source === "codex") {
|
|
254
|
+
const group = groups.get(file.session_id) ?? [];
|
|
255
|
+
group.push(file);
|
|
256
|
+
groups.set(file.session_id, group);
|
|
257
|
+
}
|
|
258
|
+
const replacements = new Map();
|
|
259
|
+
for (const [id, files] of groups) {
|
|
260
|
+
if (files.length < 2)
|
|
261
|
+
continue;
|
|
262
|
+
const representative = [...files].sort((a, b) => a.file_path.localeCompare(b.file_path))[0];
|
|
263
|
+
const combined = { ...representative, segment_paths: files.map(f => f.file_path).sort(),
|
|
264
|
+
segment_stamps: files.map(f => ({ path: f.file_path, size: f.byte_size, mtime: f.mtime_ms })),
|
|
265
|
+
byte_size: files.reduce((n, f) => n + f.byte_size, 0), mtime_ms: Math.max(...files.map(f => f.mtime_ms)) };
|
|
266
|
+
try {
|
|
267
|
+
const source = await openCodexSessionRevision(combined.segment_paths);
|
|
268
|
+
try {
|
|
269
|
+
combined.byte_size = source.length;
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
await source.close();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
result.file_read_failed++;
|
|
277
|
+
}
|
|
278
|
+
replacements.set(id, combined);
|
|
279
|
+
}
|
|
280
|
+
const emitted = new Set();
|
|
281
|
+
result.files = result.files.flatMap(file => {
|
|
282
|
+
if (file.source !== "codex" || !replacements.has(file.session_id))
|
|
283
|
+
return [file];
|
|
284
|
+
if (emitted.has(file.session_id))
|
|
285
|
+
return [];
|
|
286
|
+
emitted.add(file.session_id);
|
|
287
|
+
return [replacements.get(file.session_id)];
|
|
288
|
+
});
|
|
240
289
|
}
|
|
@@ -22,7 +22,7 @@ export async function fetchSourceInventory(options) {
|
|
|
22
22
|
throw new Error("invalid_response");
|
|
23
23
|
const valid = body.rows.every((row) => row && typeof row.id === "string" && uuid.test(row.id) &&
|
|
24
24
|
typeof row.source === "string" && (row.codex_session_id === null || typeof row.codex_session_id === "string") &&
|
|
25
|
-
(row.raw_evidence_pointer_id === null || (typeof row.raw_evidence_pointer_id === "string" &&
|
|
25
|
+
(row.raw_evidence_pointer_id === null || (typeof row.raw_evidence_pointer_id === "string" && row.raw_evidence_pointer_id.trim().length > 0)));
|
|
26
26
|
if (!valid)
|
|
27
27
|
throw new Error("invalid_response");
|
|
28
28
|
const next = body.next_after;
|
|
@@ -67,14 +67,16 @@ export async function reconcileSource(input) {
|
|
|
67
67
|
return inventory.status === "exhausted"
|
|
68
68
|
? answer("missing_pointer", "source_rows_without_pointer") : answer("unavailable_evidence", `row_inventory_${inventory.status}`);
|
|
69
69
|
const objects = (remote.files.get(file.key) ?? []).filter(object => !object.stub &&
|
|
70
|
-
(file.kind === "claude_subagent" || (object.ref_id && pointers.has(object.ref_id))));
|
|
70
|
+
(file.kind === "claude_subagent" || ((object.ref_id && pointers.has(object.ref_id)) || pointers.has(object.object_key))));
|
|
71
71
|
if (!objects.length)
|
|
72
72
|
return answer("unavailable_evidence", remote.status === "exhausted" ? "pointer_not_in_current_transcripts_or_stub" : `transcripts_${remote.status}`);
|
|
73
73
|
const split = facts.slimSplit ? await facts.slimSplit() : null;
|
|
74
74
|
const form = split?.slim ?? (facts.slimAware && facts.wholeForm ? await facts.wholeForm() : await facts.sanitized());
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
for (const observed of file.segment_stamps ?? [{ path: file.file_path, size: file.byte_size, mtime: file.mtime_ms }]) {
|
|
76
|
+
const stat = await fs.stat(observed.path);
|
|
77
|
+
if (stat.size !== observed.size || stat.mtimeMs !== observed.mtime)
|
|
78
|
+
return answer("unavailable_evidence", "local_file_changed_during_check");
|
|
79
|
+
}
|
|
78
80
|
if (form && objects.some(object => object.content_hash_sha256 === form.sha256 || object.content_hash_sha256 === split?.full.sha256))
|
|
79
81
|
return answer("revision_verified", "current_redacted_hash_matches_pointer");
|
|
80
82
|
// Sync refreshes the observed file hash even when growth damping retains an older
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
*
|
|
33
33
|
* Read-only: this module reads the file and never writes, moves or deletes it.
|
|
34
34
|
*/
|
|
35
|
+
import { codexRevisionFacts } from "./codex-revision-facts.js";
|
|
35
36
|
import crypto from "node:crypto";
|
|
36
37
|
import fs from "node:fs/promises";
|
|
37
38
|
import path from "node:path";
|
|
@@ -162,6 +163,8 @@ upload = {}) {
|
|
|
162
163
|
const mode = upload.mode ?? IMAGE_HANDLING_MODE;
|
|
163
164
|
const describedKeys = upload.describedKeys ?? new Set();
|
|
164
165
|
const slimAware = upload.slimAware ?? modeDropsImages(mode);
|
|
166
|
+
if (file.segment_paths)
|
|
167
|
+
return codexRevisionFacts(file.segment_paths, mode, describedKeys, slimAware);
|
|
165
168
|
const redactForUpload = (text, original) => redactTranscriptForm(file, text, original);
|
|
166
169
|
let split = null;
|
|
167
170
|
let whole = null;
|
|
@@ -204,6 +204,7 @@ function parseSessionEntry(value) {
|
|
|
204
204
|
return null;
|
|
205
205
|
return {
|
|
206
206
|
file_hash_sha256: optionalString(record["file_hash_sha256"]),
|
|
207
|
+
...(typeof record["uploaded_source_revision"] === "string" ? { uploaded_source_revision: record["uploaded_source_revision"] } : {}),
|
|
207
208
|
file_mtime_ms: optionalNumber(record["file_mtime_ms"]),
|
|
208
209
|
byte_size: optionalNumber(record["byte_size"]),
|
|
209
210
|
byte_offset: optionalNumber(record["byte_offset"]),
|
|
@@ -16,7 +16,7 @@ import { sha256OfBase64Run } from "./image-byte-scan.js";
|
|
|
16
16
|
import { addPdfsToFinds, mergeOccurrences, pdfsOfText } from "./pdf-transcript-step.js";
|
|
17
17
|
import { openPinnedTranscript } from "./adapters/pinned-transcript.js";
|
|
18
18
|
import { openStagedOutput, readStagedText } from "./adapters/staged-output.js";
|
|
19
|
-
import { TranscriptNotStreamable } from "./adapters/transcript-text-stream.js";
|
|
19
|
+
import { TranscriptNotStreamable, TranscriptTooLargeError } from "./adapters/transcript-text-stream.js";
|
|
20
20
|
import { copyLinesStream } from "./slim/keep-line-copy.js";
|
|
21
21
|
import { LineBytesSink, slimLineStream } from "./slim/slim-stream.js";
|
|
22
22
|
import { logSlimmed, slimOrKeep, slimPassOrNull, slimTranscript } from "./slim/slim-transcript.js";
|
|
@@ -133,9 +133,11 @@ async function prepareFileInTwoPasses(run, input) {
|
|
|
133
133
|
const startedAt = Date.now();
|
|
134
134
|
// The file is opened once and its length pinned: the census offsets, the describe payload reads and the swap
|
|
135
135
|
// pass all read these bytes, and a source that stopped being them is set aside (`verifyUnchanged`).
|
|
136
|
-
const source = await openPinnedTranscript(input.filePath, { maxFileBytes: input.maxFileBytes });
|
|
136
|
+
const source = input.openSource ? await input.openSource() : await openPinnedTranscript(input.filePath, { maxFileBytes: input.maxFileBytes });
|
|
137
137
|
let writer;
|
|
138
138
|
try {
|
|
139
|
+
if (input.maxFileBytes && source.length > input.maxFileBytes)
|
|
140
|
+
throw new TranscriptTooLargeError(input.maxFileBytes);
|
|
139
141
|
// Pass one: discover. Nothing of the text is kept; only where each image is and what it is.
|
|
140
142
|
const census = await censusOfLines(source.lines());
|
|
141
143
|
// Describe attempts finish and the swap decisions are settled before a single line is swapped, as with the whole text.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Shared native homes keep sync, backfill and reconciliation on the same inventory. */
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { t3ImportedNativeHomes } from "./t3-native-homes.js";
|
|
4
|
+
export function nativeSessionRoots(homeDir, pathApi = path, environment = process.env) {
|
|
5
|
+
let importedHomes = [];
|
|
6
|
+
let discoveryFailed = false;
|
|
7
|
+
try {
|
|
8
|
+
importedHomes = t3ImportedNativeHomes(homeDir);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
discoveryFailed = true;
|
|
12
|
+
} // The reader logged its reason; existing native homes remain available.
|
|
13
|
+
const expandHome = (value) => value === "~"
|
|
14
|
+
? homeDir
|
|
15
|
+
: /^~[/\\]/u.test(value) ? pathApi.join(homeDir, value.slice(2)) : value;
|
|
16
|
+
const homes = (source, defaultHome, configured) => {
|
|
17
|
+
const extra = configured?.trim();
|
|
18
|
+
// Relative environment values depend on the launching app's cwd. Never guess it.
|
|
19
|
+
return [...new Set([defaultHome, ...importedHomes.filter((entry) => entry.source === source).map((entry) => entry.home), ...(extra && pathApi.isAbsolute(expandHome(extra)) ? [expandHome(extra)] : [])]
|
|
20
|
+
.map((home) => pathApi.normalize(home)))];
|
|
21
|
+
};
|
|
22
|
+
const claudeProjectDirs = homes("claude_code", pathApi.join(homeDir, ".claude"), environment.CLAUDE_CONFIG_DIR)
|
|
23
|
+
.map((home) => pathApi.join(home, "projects"));
|
|
24
|
+
return {
|
|
25
|
+
discoveryFailed,
|
|
26
|
+
codex: homes("codex", pathApi.join(homeDir, ".codex"), environment.CODEX_HOME)
|
|
27
|
+
.flatMap((home) => [pathApi.join(home, "sessions"), pathApi.join(home, "archived_sessions")]),
|
|
28
|
+
claudeProjects: claudeProjectDirs[0],
|
|
29
|
+
claudeProjectDirs,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.293",
|
|
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.91",
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.178",
|
|
32
|
+
"@bli-cockpit/telemetry-core": "0.1.97",
|
|
33
33
|
"unpdf": "1.8.1"
|
|
34
34
|
},
|
|
35
35
|
"repository": {
|