@phnx-labs/agents-cli 1.21.3 → 1.22.1
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/CHANGELOG.md +136 -0
- package/README.md +32 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/computer-actions.js +1 -0
- package/dist/commands/doctor.js +34 -2
- package/dist/commands/exec.d.ts +27 -0
- package/dist/commands/exec.js +123 -6
- package/dist/commands/models.js +36 -1
- package/dist/commands/projects.js +120 -62
- package/dist/commands/sessions-backfill.d.ts +32 -0
- package/dist/commands/sessions-backfill.js +186 -0
- package/dist/commands/sessions.d.ts +17 -1
- package/dist/commands/sessions.js +317 -18
- package/dist/commands/teams.js +1 -1
- package/dist/commands/worktree.d.ts +3 -3
- package/dist/commands/worktree.js +35 -4
- package/dist/lib/app-bundle-install.d.ts +17 -0
- package/dist/lib/app-bundle-install.js +94 -0
- package/dist/lib/daemon.d.ts +5 -1
- package/dist/lib/daemon.js +63 -14
- package/dist/lib/devices/doctor-findings.js +12 -4
- package/dist/lib/devices/doctor-overview-cache.d.ts +45 -0
- package/dist/lib/devices/doctor-overview-cache.js +168 -0
- package/dist/lib/devices/fleet.js +7 -2
- package/dist/lib/devices/resolve-target.d.ts +6 -0
- package/dist/lib/devices/resolve-target.js +9 -3
- package/dist/lib/devices/self-host.d.ts +9 -0
- package/dist/lib/devices/self-host.js +61 -0
- package/dist/lib/exec.js +39 -8
- package/dist/lib/hosts/dispatch.d.ts +12 -0
- package/dist/lib/hosts/dispatch.js +23 -6
- package/dist/lib/hosts/passthrough.js +8 -6
- package/dist/lib/hosts/reconnect.d.ts +38 -0
- package/dist/lib/hosts/reconnect.js +85 -4
- package/dist/lib/hosts/run-target.js +14 -2
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.js +27 -23
- package/dist/lib/model-tiers.d.ts +54 -0
- package/dist/lib/model-tiers.js +229 -0
- package/dist/lib/models.d.ts +3 -0
- package/dist/lib/models.js +44 -7
- package/dist/lib/pricing/prices.json +16 -1
- package/dist/lib/project-focus.d.ts +42 -0
- package/dist/lib/project-focus.js +80 -0
- package/dist/lib/project-schedule.d.ts +75 -0
- package/dist/lib/project-schedule.js +110 -0
- package/dist/lib/project-status.d.ts +7 -0
- package/dist/lib/project-status.js +9 -0
- package/dist/lib/projects.d.ts +11 -2
- package/dist/lib/projects.js +57 -9
- package/dist/lib/redact.d.ts +2 -0
- package/dist/lib/redact.js +22 -0
- package/dist/lib/remote-agents-json.d.ts +2 -0
- package/dist/lib/remote-agents-json.js +3 -3
- package/dist/lib/rotate.d.ts +84 -1
- package/dist/lib/rotate.js +155 -5
- package/dist/lib/runner.d.ts +4 -2
- package/dist/lib/runner.js +13 -4
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/install-helper.js +28 -31
- package/dist/lib/session/bash-command.js +60 -9
- package/dist/lib/session/db.d.ts +7 -1
- package/dist/lib/session/db.js +301 -32
- package/dist/lib/session/discover.d.ts +40 -7
- package/dist/lib/session/discover.js +144 -83
- package/dist/lib/session/parse.d.ts +8 -1
- package/dist/lib/session/parse.js +83 -32
- package/dist/lib/session/remote-list.d.ts +71 -0
- package/dist/lib/session/remote-list.js +410 -2
- package/dist/lib/session/shell-programs.d.ts +15 -0
- package/dist/lib/session/shell-programs.js +359 -0
- package/dist/lib/session/tool-calls.d.ts +88 -0
- package/dist/lib/session/tool-calls.js +612 -0
- package/dist/lib/session/tool-index.d.ts +100 -0
- package/dist/lib/session/tool-index.js +773 -0
- package/dist/lib/session/tool-store.d.ts +15 -0
- package/dist/lib/session/tool-store.js +198 -0
- package/dist/lib/session/types.d.ts +7 -0
- package/dist/lib/state.d.ts +10 -1
- package/dist/lib/state.js +11 -2
- package/dist/lib/teams/remoteWorktree.d.ts +3 -4
- package/dist/lib/teams/remoteWorktree.js +3 -4
- package/dist/lib/teams/worktree.d.ts +11 -1
- package/dist/lib/teams/worktree.js +42 -4
- package/dist/lib/types.d.ts +17 -0
- package/dist/lib/types.js +17 -0
- package/dist/lib/versions.js +69 -22
- package/package.json +3 -1
|
@@ -34,6 +34,8 @@ import { mapBounded } from '../concurrency.js';
|
|
|
34
34
|
import { getDB, getScanStampByPath, getScanStampsForPaths, getParserStatesForPaths, getDirLedgerForPaths, recordDirScans, recordScans, syncLabels, seedLabelsFromNames, syncTopics, upsertSessionsBatch, querySessions, countSessions, ftsSearch, tryClaimScan, releaseScan, cacheLinearProject, } from './db.js';
|
|
35
35
|
import { buildRunNameMap } from './run-names.js';
|
|
36
36
|
import { resolveLinearApiKey } from '../auto-dispatch-linear.js';
|
|
37
|
+
import { ToolCallCollector, collectClaudeToolCalls, collectCodexToolCalls, } from './tool-calls.js';
|
|
38
|
+
import { purgeMissingToolCallsInDirectory } from './tool-store.js';
|
|
37
39
|
const HOME = os.homedir();
|
|
38
40
|
// Versions can live under either repo: the user repo (current canonical
|
|
39
41
|
// location, ~/.agents/.history/versions/) or the system repo (legacy / npm-shipped,
|
|
@@ -47,6 +49,73 @@ const HERMES_SESSIONS_DIR = path.join(HOME, '.hermes', 'sessions');
|
|
|
47
49
|
/** How long OpenClaw channel/cron snapshots stay valid before we re-shell-out. */
|
|
48
50
|
const OPENCLAW_TTL_MS = 60_000;
|
|
49
51
|
const ACTIVE_APPEND_RESCAN_DEBOUNCE_MS = 5_000;
|
|
52
|
+
/** One JSONL record may not force an unbounded string allocation. */
|
|
53
|
+
export const SESSION_JSONL_LINE_MAX_BYTES = 1024 * 1024;
|
|
54
|
+
/**
|
|
55
|
+
* Stream one appended JSONL range, applying only newline-terminated records.
|
|
56
|
+
* Memory is bounded to one record. An ordinary unterminated tail remains for the
|
|
57
|
+
* next scan. Once a record exceeds the cap, its consumed offset and a one-bit
|
|
58
|
+
* dropping state advance together so later scans never reread the growing tail.
|
|
59
|
+
*/
|
|
60
|
+
async function applyJsonlAppend(filePath, fromOffset, wasDroppingOversizedLine, apply) {
|
|
61
|
+
let pending = Buffer.alloc(0);
|
|
62
|
+
let droppingOversizedLine = wasDroppingOversizedLine;
|
|
63
|
+
let bytesBeforeChunk = 0;
|
|
64
|
+
let consumedBytes = 0;
|
|
65
|
+
let skippedOversizedLine = false;
|
|
66
|
+
const stream = fs.createReadStream(filePath, { start: fromOffset });
|
|
67
|
+
try {
|
|
68
|
+
for await (const rawChunk of stream) {
|
|
69
|
+
const chunk = typeof rawChunk === 'string' ? Buffer.from(rawChunk, 'utf-8') : rawChunk;
|
|
70
|
+
let cursor = 0;
|
|
71
|
+
for (;;) {
|
|
72
|
+
const newline = chunk.indexOf(0x0a, cursor);
|
|
73
|
+
if (newline === -1) {
|
|
74
|
+
if (!droppingOversizedLine) {
|
|
75
|
+
const tail = chunk.subarray(cursor);
|
|
76
|
+
if (pending.length + tail.length > SESSION_JSONL_LINE_MAX_BYTES) {
|
|
77
|
+
pending = Buffer.alloc(0);
|
|
78
|
+
droppingOversizedLine = true;
|
|
79
|
+
skippedOversizedLine = true;
|
|
80
|
+
}
|
|
81
|
+
else if (tail.length > 0) {
|
|
82
|
+
pending = pending.length > 0 ? Buffer.concat([pending, tail]) : Buffer.from(tail);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (droppingOversizedLine)
|
|
86
|
+
consumedBytes = bytesBeforeChunk + chunk.length;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
if (!droppingOversizedLine) {
|
|
90
|
+
const segment = chunk.subarray(cursor, newline);
|
|
91
|
+
if (pending.length + segment.length <= SESSION_JSONL_LINE_MAX_BYTES) {
|
|
92
|
+
const line = (pending.length > 0 ? Buffer.concat([pending, segment]) : segment).toString('utf-8');
|
|
93
|
+
if (line.trim()) {
|
|
94
|
+
try {
|
|
95
|
+
apply(JSON.parse(line));
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// One malformed line never aborts the session scan.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
skippedOversizedLine = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
pending = Buffer.alloc(0);
|
|
107
|
+
droppingOversizedLine = false;
|
|
108
|
+
consumedBytes = bytesBeforeChunk + newline + 1;
|
|
109
|
+
cursor = newline + 1;
|
|
110
|
+
}
|
|
111
|
+
bytesBeforeChunk += chunk.length;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
stream.destroy();
|
|
116
|
+
}
|
|
117
|
+
return { consumedBytes, droppingOversizedLine, skippedOversizedLine };
|
|
118
|
+
}
|
|
50
119
|
/**
|
|
51
120
|
* How recently a file must have been scanned to be treated as "hot" — a
|
|
52
121
|
* candidate for an in-place append even when its parent dir's mtime hasn't
|
|
@@ -102,8 +171,17 @@ export async function discoverSessions(options) {
|
|
|
102
171
|
releaseScan(process.pid);
|
|
103
172
|
}
|
|
104
173
|
}
|
|
105
|
-
|
|
106
|
-
|
|
174
|
+
return queryIndexedSessions(options, {
|
|
175
|
+
skipExistenceCheck: options?.skipExistenceCheck ?? false,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/** Read the current SQLite snapshot without scanning or parsing transcript files. */
|
|
179
|
+
export async function queryIndexedSessions(options, indexedOptions = {}) {
|
|
180
|
+
getDB();
|
|
181
|
+
const agents = options?.agent ? [options.agent] : SESSION_AGENTS;
|
|
182
|
+
const sessions = querySessions(buildQueryOptions({ ...options, skipExistenceCheck: indexedOptions.skipExistenceCheck ?? true }, agents, { includeLimit: true }));
|
|
183
|
+
if (indexedOptions.resolveLinear !== false)
|
|
184
|
+
await resolveLinearProjects(sessions);
|
|
107
185
|
for (const s of sessions)
|
|
108
186
|
s.machine = machineForSessionFile(s.filePath, s.agent);
|
|
109
187
|
return scopeToManaged(sessions, agents, options);
|
|
@@ -156,7 +234,7 @@ async function resolveLinearProjects(sessions) {
|
|
|
156
234
|
* An agent with no managed versions is left alone entirely — someone who has never
|
|
157
235
|
* run `agents add` sees exactly what they saw before.
|
|
158
236
|
*/
|
|
159
|
-
function scopeToManaged(sessions, agents, options) {
|
|
237
|
+
export function scopeToManaged(sessions, agents, options) {
|
|
160
238
|
if (options?.includeUnmanaged)
|
|
161
239
|
return sessions;
|
|
162
240
|
if (!anyManagedVersions())
|
|
@@ -306,11 +384,12 @@ function buildQueryOptions(options, agents, opts) {
|
|
|
306
384
|
project: projectQuery,
|
|
307
385
|
sinceMs,
|
|
308
386
|
untilMs: Number.isFinite(untilMs) ? untilMs : undefined,
|
|
309
|
-
limit: opts.includeLimit ? (options?.limit ?? 50) : undefined,
|
|
387
|
+
limit: opts.includeLimit && !options?.unbounded ? (options?.limit ?? 50) : undefined,
|
|
310
388
|
excludeTeamOrigin: options?.excludeTeamOrigin,
|
|
311
389
|
onlyTeamOrigin: options?.onlyTeamOrigin,
|
|
312
390
|
origin: options?.origin,
|
|
313
391
|
sortBy: options?.sortBy,
|
|
392
|
+
skipExistenceCheck: options?.skipExistenceCheck,
|
|
314
393
|
skill: options?.skill,
|
|
315
394
|
plugin: options?.plugin,
|
|
316
395
|
};
|
|
@@ -582,6 +661,7 @@ export function collectChangedFilesInLeafDirs(leafDirs, ext) {
|
|
|
582
661
|
continue;
|
|
583
662
|
toCompare.push({ filePath, fileMtimeMs: stat.mtimeMs, fileSize: stat.size });
|
|
584
663
|
}
|
|
664
|
+
purgeMissingToolCallsInDirectory(getDB(), dirPath, files);
|
|
585
665
|
if (!disabled)
|
|
586
666
|
dirScansToRecord.push({ dirPath, dirMtimeMs, entryCount });
|
|
587
667
|
}
|
|
@@ -789,7 +869,13 @@ async function scanRoutineArchivesIncremental(agent, onProgress) {
|
|
|
789
869
|
try {
|
|
790
870
|
const result = await readRoutineArchiveMeta(agent, filePath);
|
|
791
871
|
if (result)
|
|
792
|
-
entries.push({
|
|
872
|
+
entries.push({
|
|
873
|
+
meta: result.meta,
|
|
874
|
+
content: result.content,
|
|
875
|
+
scan,
|
|
876
|
+
toolCalls: result.toolCalls,
|
|
877
|
+
toolIndexMode: result.toolIndexMode,
|
|
878
|
+
});
|
|
793
879
|
else
|
|
794
880
|
touched.push({ filePath, scan });
|
|
795
881
|
}
|
|
@@ -964,6 +1050,8 @@ async function scanClaudeIncremental(onProgress) {
|
|
|
964
1050
|
scan,
|
|
965
1051
|
parserState: result.parserState,
|
|
966
1052
|
contentText: result.contentText,
|
|
1053
|
+
toolCalls: result.toolCalls,
|
|
1054
|
+
toolIndexMode: result.toolIndexMode,
|
|
967
1055
|
});
|
|
968
1056
|
}
|
|
969
1057
|
else {
|
|
@@ -993,7 +1081,7 @@ async function scanClaudeIncremental(onProgress) {
|
|
|
993
1081
|
*/
|
|
994
1082
|
async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, account, label) {
|
|
995
1083
|
const prior = parsePriorClaudeState(priorRow);
|
|
996
|
-
const { scan, newState, mode } = await scanClaudeSessionResumable(filePath, prior, scanStamp.fileMtimeMs, scanStamp.fileSize, priorRow?.fileMtimeMs);
|
|
1084
|
+
const { scan, newState, toolCalls, mode } = await scanClaudeSessionResumable(filePath, prior, scanStamp.fileMtimeMs, scanStamp.fileSize, priorRow?.fileMtimeMs);
|
|
997
1085
|
if (mode === 'incremental')
|
|
998
1086
|
claudeIncrementalScanCount++;
|
|
999
1087
|
else
|
|
@@ -1078,6 +1166,8 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, account,
|
|
|
1078
1166
|
// doc, cached so the resume can hydrate userTexts without re-reading the file.
|
|
1079
1167
|
parserState: JSON.stringify(newState),
|
|
1080
1168
|
contentText: newState.contentText,
|
|
1169
|
+
toolCalls,
|
|
1170
|
+
toolIndexMode: mode === 'full' ? 'replace' : 'append',
|
|
1081
1171
|
};
|
|
1082
1172
|
}
|
|
1083
1173
|
// ---------------------------------------------------------------------------
|
|
@@ -1215,6 +1305,8 @@ async function scanCodexIncremental(onProgress) {
|
|
|
1215
1305
|
scan,
|
|
1216
1306
|
parserState: result.parserState,
|
|
1217
1307
|
contentText: result.contentText,
|
|
1308
|
+
toolCalls: result.toolCalls,
|
|
1309
|
+
toolIndexMode: result.toolIndexMode,
|
|
1218
1310
|
});
|
|
1219
1311
|
}
|
|
1220
1312
|
else {
|
|
@@ -1317,6 +1409,8 @@ export async function readCodexMeta(filePath, resolveAccount, currentVersion, sc
|
|
|
1317
1409
|
let scan;
|
|
1318
1410
|
let newState;
|
|
1319
1411
|
let newOffset = 0;
|
|
1412
|
+
let toolCalls;
|
|
1413
|
+
let toolIndexMode = 'replace';
|
|
1320
1414
|
if (scanStamp) {
|
|
1321
1415
|
const prior = parsePriorCodexState(priorRow);
|
|
1322
1416
|
const result = await scanCodexSessionResumable(filePath, prior, scanStamp.fileMtimeMs, scanStamp.fileSize, priorRow?.fileMtimeMs);
|
|
@@ -1327,6 +1421,8 @@ export async function readCodexMeta(filePath, resolveAccount, currentVersion, sc
|
|
|
1327
1421
|
scan = result.scan;
|
|
1328
1422
|
newState = result.newState;
|
|
1329
1423
|
newOffset = result.newOffset;
|
|
1424
|
+
toolCalls = result.toolCalls;
|
|
1425
|
+
toolIndexMode = result.mode === 'full' ? 'replace' : 'append';
|
|
1330
1426
|
}
|
|
1331
1427
|
else {
|
|
1332
1428
|
scan = await scanCodexSession(filePath);
|
|
@@ -1373,6 +1469,8 @@ export async function readCodexMeta(filePath, resolveAccount, currentVersion, sc
|
|
|
1373
1469
|
// doc for the resume's hydrate. Absent when no stamp was supplied.
|
|
1374
1470
|
parserState: newState ? JSON.stringify(newState) : undefined,
|
|
1375
1471
|
contentText: newState?.contentText,
|
|
1472
|
+
toolCalls,
|
|
1473
|
+
toolIndexMode,
|
|
1376
1474
|
};
|
|
1377
1475
|
}
|
|
1378
1476
|
/**
|
|
@@ -2453,6 +2551,7 @@ export function initClaudeParseState() {
|
|
|
2453
2551
|
plan: undefined,
|
|
2454
2552
|
checklistEvents: [],
|
|
2455
2553
|
recentDirectoriesTouched: [],
|
|
2554
|
+
toolCollector: new ToolCallCollector(),
|
|
2456
2555
|
skillEvents: [],
|
|
2457
2556
|
slashCommandEvents: [],
|
|
2458
2557
|
};
|
|
@@ -2491,6 +2590,7 @@ function foldDerivedToolState(state, event) {
|
|
|
2491
2590
|
* malformed-line skip happens in the caller, as before).
|
|
2492
2591
|
*/
|
|
2493
2592
|
export function applyClaudeLine(state, parsed) {
|
|
2593
|
+
collectClaudeToolCalls(state.toolCollector, parsed);
|
|
2494
2594
|
// entrypoint ships on the first envelope event (attachment/user/assistant)
|
|
2495
2595
|
// and is the clean structural signal for "was this a team spawn?"
|
|
2496
2596
|
if (!state.entrypoint && typeof parsed.entrypoint === 'string') {
|
|
@@ -2726,15 +2826,16 @@ const SEEN_IDS_RECENT_CAP = 256;
|
|
|
2726
2826
|
* `offset` bytes consumed. Round-trips through {@link hydrateClaudeParseState}
|
|
2727
2827
|
* so incremental replay equals a full parse.
|
|
2728
2828
|
*/
|
|
2729
|
-
export function serializeClaudeParserState(state, offset) {
|
|
2829
|
+
export function serializeClaudeParserState(state, offset, jsonlDroppingOversizedLine = false) {
|
|
2730
2830
|
const allIds = [...state.seenAssistantIds];
|
|
2731
2831
|
const seenIdsRecent = allIds.length > SEEN_IDS_RECENT_CAP
|
|
2732
2832
|
? allIds.slice(allIds.length - SEEN_IDS_RECENT_CAP)
|
|
2733
2833
|
: allIds;
|
|
2734
2834
|
const ticket = detectTicket(state.userTexts.join('\n') || undefined, state.gitBranch);
|
|
2735
2835
|
return {
|
|
2736
|
-
v:
|
|
2836
|
+
v: 2,
|
|
2737
2837
|
offset,
|
|
2838
|
+
jsonlDroppingOversizedLine: jsonlDroppingOversizedLine || undefined,
|
|
2738
2839
|
timestamp: state.timestamp,
|
|
2739
2840
|
cwd: state.cwd,
|
|
2740
2841
|
gitBranch: state.gitBranch,
|
|
@@ -2770,6 +2871,7 @@ export function serializeClaudeParserState(state, offset) {
|
|
|
2770
2871
|
contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
|
|
2771
2872
|
checklistEvents: state.checklistEvents,
|
|
2772
2873
|
recentDirectoriesTouched: state.recentDirectoriesTouched,
|
|
2874
|
+
toolCalls: state.toolCollector.snapshot(),
|
|
2773
2875
|
skillEvents: state.skillEvents,
|
|
2774
2876
|
slashCommandEvents: state.slashCommandEvents,
|
|
2775
2877
|
};
|
|
@@ -2828,6 +2930,7 @@ export function hydrateClaudeParseState(prior) {
|
|
|
2828
2930
|
plan: prior.plan,
|
|
2829
2931
|
checklistEvents: prior.checklistEvents ?? [],
|
|
2830
2932
|
recentDirectoriesTouched: prior.recentDirectoriesTouched ?? [],
|
|
2933
|
+
toolCollector: new ToolCallCollector(prior.toolCalls),
|
|
2831
2934
|
skillEvents: prior.skillEvents ?? [],
|
|
2832
2935
|
slashCommandEvents: prior.slashCommandEvents ?? [],
|
|
2833
2936
|
};
|
|
@@ -2853,45 +2956,20 @@ export async function scanClaudeSessionIncremental(filePath, fromOffset, prior)
|
|
|
2853
2956
|
// have no dedup, unlike assistant `seenAssistantIds`). A record written
|
|
2854
2957
|
// non-atomically — bytes first, then '\n' in a second write — is exactly this
|
|
2855
2958
|
// case. So we slice at the last '\n' ourselves: everything up to and including
|
|
2856
|
-
// it is a run of complete lines we apply and commit
|
|
2857
|
-
//
|
|
2858
|
-
//
|
|
2859
|
-
const
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
}
|
|
2866
|
-
finally {
|
|
2867
|
-
stream.destroy();
|
|
2868
|
-
}
|
|
2869
|
-
const appended = Buffer.concat(chunks);
|
|
2870
|
-
// Bytes up to AND INCLUDING the last '\n' are the committed, complete-line run.
|
|
2871
|
-
const lastNl = appended.lastIndexOf(0x0a);
|
|
2872
|
-
const consumedBytes = lastNl === -1 ? 0 : lastNl + 1;
|
|
2873
|
-
if (consumedBytes > 0) {
|
|
2874
|
-
// split('\n') on the committed run: the element after the final '\n' is ''
|
|
2875
|
-
// (skipped by the trim guard). A stray '\r' from CRLF is tolerated by the
|
|
2876
|
-
// JSON.parse below exactly as the full parse tolerates it.
|
|
2877
|
-
for (const line of appended.subarray(0, consumedBytes).toString('utf-8').split('\n')) {
|
|
2878
|
-
if (!line.trim())
|
|
2879
|
-
continue;
|
|
2880
|
-
let parsed;
|
|
2881
|
-
try {
|
|
2882
|
-
parsed = JSON.parse(line);
|
|
2883
|
-
}
|
|
2884
|
-
catch {
|
|
2885
|
-
continue;
|
|
2886
|
-
}
|
|
2887
|
-
applyClaudeLine(state, parsed);
|
|
2888
|
-
}
|
|
2889
|
-
}
|
|
2890
|
-
const newOffset = fromOffset + consumedBytes;
|
|
2959
|
+
// it is a run of complete lines we apply and commit. An ordinary tail after
|
|
2960
|
+
// it is deferred until its '\n' lands; after a tail exceeds 1 MiB, its offset
|
|
2961
|
+
// advances with a persisted discard-until-newline bit.
|
|
2962
|
+
const append = await applyJsonlAppend(filePath, fromOffset, prior.jsonlDroppingOversizedLine === true, (parsed) => applyClaudeLine(state, parsed));
|
|
2963
|
+
if (append.skippedOversizedLine)
|
|
2964
|
+
state.toolCollector.recordIndexLimit();
|
|
2965
|
+
const newOffset = fromOffset + append.consumedBytes;
|
|
2966
|
+
const scan = finalizeClaudeScan(state);
|
|
2967
|
+
const toolCalls = state.toolCollector.drainChanged();
|
|
2891
2968
|
return {
|
|
2892
|
-
scan
|
|
2893
|
-
newState: serializeClaudeParserState(state, newOffset),
|
|
2969
|
+
scan,
|
|
2970
|
+
newState: serializeClaudeParserState(state, newOffset, append.droppingOversizedLine),
|
|
2894
2971
|
newOffset,
|
|
2972
|
+
toolCalls,
|
|
2895
2973
|
};
|
|
2896
2974
|
}
|
|
2897
2975
|
/** Serialized zero-value continuation: a fresh accumulator at offset 0, used to drive a FULL parse from the start through the same resumable path. */
|
|
@@ -2986,7 +3064,7 @@ function parsePriorClaudeState(row) {
|
|
|
2986
3064
|
return null;
|
|
2987
3065
|
try {
|
|
2988
3066
|
const parsed = JSON.parse(row.parserState);
|
|
2989
|
-
if (parsed?.v !==
|
|
3067
|
+
if (parsed?.v !== 2 || typeof parsed.offset !== 'number' || parsed.toolCalls?.v !== 1)
|
|
2990
3068
|
return null;
|
|
2991
3069
|
return parsed;
|
|
2992
3070
|
}
|
|
@@ -3031,6 +3109,7 @@ export function initCodexParseState() {
|
|
|
3031
3109
|
spawnedTeam: undefined,
|
|
3032
3110
|
checklistEvents: [],
|
|
3033
3111
|
recentDirectoriesTouched: [],
|
|
3112
|
+
toolCollector: new ToolCallCollector(),
|
|
3034
3113
|
skillEvents: [],
|
|
3035
3114
|
slashCommandEvents: [],
|
|
3036
3115
|
};
|
|
@@ -3042,6 +3121,7 @@ export function initCodexParseState() {
|
|
|
3042
3121
|
* malformed-line skip happens in the caller, as before).
|
|
3043
3122
|
*/
|
|
3044
3123
|
export function applyCodexLine(state, parsed) {
|
|
3124
|
+
collectCodexToolCalls(state.toolCollector, parsed);
|
|
3045
3125
|
// PR signal, structurally: a Codex `function_call` whose command is
|
|
3046
3126
|
// `gh pr create`, then the pull URL from a `function_call_output`.
|
|
3047
3127
|
if (parsed.type === 'response_item') {
|
|
@@ -3214,11 +3294,12 @@ async function scanCodexSession(filePath) {
|
|
|
3214
3294
|
* bytes consumed. Round-trips through {@link hydrateCodexParseState} so
|
|
3215
3295
|
* incremental replay equals a full parse.
|
|
3216
3296
|
*/
|
|
3217
|
-
export function serializeCodexParserState(state, offset) {
|
|
3297
|
+
export function serializeCodexParserState(state, offset, jsonlDroppingOversizedLine = false) {
|
|
3218
3298
|
const ticket = detectTicket(state.userTexts.join('\n') || undefined, state.gitBranch);
|
|
3219
3299
|
return {
|
|
3220
|
-
v:
|
|
3300
|
+
v: 2,
|
|
3221
3301
|
offset,
|
|
3302
|
+
jsonlDroppingOversizedLine: jsonlDroppingOversizedLine || undefined,
|
|
3222
3303
|
sessionId: state.sessionId,
|
|
3223
3304
|
timestamp: state.timestamp,
|
|
3224
3305
|
cwd: state.cwd,
|
|
@@ -3245,6 +3326,7 @@ export function serializeCodexParserState(state, offset) {
|
|
|
3245
3326
|
contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
|
|
3246
3327
|
checklistEvents: state.checklistEvents,
|
|
3247
3328
|
recentDirectoriesTouched: state.recentDirectoriesTouched,
|
|
3329
|
+
toolCalls: state.toolCollector.snapshot(),
|
|
3248
3330
|
};
|
|
3249
3331
|
}
|
|
3250
3332
|
/**
|
|
@@ -3280,6 +3362,7 @@ export function hydrateCodexParseState(prior) {
|
|
|
3280
3362
|
spawnedTeam: prior.spawnedTeam,
|
|
3281
3363
|
checklistEvents: prior.checklistEvents ?? [],
|
|
3282
3364
|
recentDirectoriesTouched: prior.recentDirectoriesTouched ?? [],
|
|
3365
|
+
toolCollector: new ToolCallCollector(prior.toolCalls),
|
|
3283
3366
|
// Not persisted in CodexParserState (always empty for Codex today — see
|
|
3284
3367
|
// CodexParseState.skillEvents) — a resume starts fresh rather than
|
|
3285
3368
|
// round-tripping an always-empty array through the continuation blob.
|
|
@@ -3294,47 +3377,25 @@ export function hydrateCodexParseState(prior) {
|
|
|
3294
3377
|
*
|
|
3295
3378
|
* Same trailing-line discipline as {@link scanClaudeSessionIncremental}: apply
|
|
3296
3379
|
* ONLY the run of newline-terminated lines (slice at the last `'\n'`), and set
|
|
3297
|
-
* `newOffset = fromOffset + consumedBytes`.
|
|
3298
|
-
*
|
|
3299
|
-
*
|
|
3380
|
+
* `newOffset = fromOffset + consumedBytes`. An ordinary tail after the last
|
|
3381
|
+
* `'\n'` is deferred; an oversized one advances under a persisted discard bit.
|
|
3382
|
+
* Codex `messageCount` is additive with
|
|
3300
3383
|
* NO dedup, so re-reading a still-unterminated complete line would double-count
|
|
3301
3384
|
* it; deferring prevents that (the bug class prix-cloud caught for Claude).
|
|
3302
3385
|
*/
|
|
3303
3386
|
export async function scanCodexSessionIncremental(filePath, fromOffset, prior) {
|
|
3304
3387
|
const state = hydrateCodexParseState(prior);
|
|
3305
|
-
const
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
}
|
|
3312
|
-
finally {
|
|
3313
|
-
stream.destroy();
|
|
3314
|
-
}
|
|
3315
|
-
const appended = Buffer.concat(chunks);
|
|
3316
|
-
// Bytes up to AND INCLUDING the last '\n' are the committed, complete-line run.
|
|
3317
|
-
const lastNl = appended.lastIndexOf(0x0a);
|
|
3318
|
-
const consumedBytes = lastNl === -1 ? 0 : lastNl + 1;
|
|
3319
|
-
if (consumedBytes > 0) {
|
|
3320
|
-
for (const line of appended.subarray(0, consumedBytes).toString('utf-8').split('\n')) {
|
|
3321
|
-
if (!line.trim())
|
|
3322
|
-
continue;
|
|
3323
|
-
let parsed;
|
|
3324
|
-
try {
|
|
3325
|
-
parsed = JSON.parse(line);
|
|
3326
|
-
}
|
|
3327
|
-
catch {
|
|
3328
|
-
continue;
|
|
3329
|
-
}
|
|
3330
|
-
applyCodexLine(state, parsed);
|
|
3331
|
-
}
|
|
3332
|
-
}
|
|
3333
|
-
const newOffset = fromOffset + consumedBytes;
|
|
3388
|
+
const append = await applyJsonlAppend(filePath, fromOffset, prior.jsonlDroppingOversizedLine === true, (parsed) => applyCodexLine(state, parsed));
|
|
3389
|
+
if (append.skippedOversizedLine)
|
|
3390
|
+
state.toolCollector.recordIndexLimit();
|
|
3391
|
+
const newOffset = fromOffset + append.consumedBytes;
|
|
3392
|
+
const scan = finalizeCodexScan(state);
|
|
3393
|
+
const toolCalls = state.toolCollector.drainChanged();
|
|
3334
3394
|
return {
|
|
3335
|
-
scan
|
|
3336
|
-
newState: serializeCodexParserState(state, newOffset),
|
|
3395
|
+
scan,
|
|
3396
|
+
newState: serializeCodexParserState(state, newOffset, append.droppingOversizedLine),
|
|
3337
3397
|
newOffset,
|
|
3398
|
+
toolCalls,
|
|
3338
3399
|
};
|
|
3339
3400
|
}
|
|
3340
3401
|
/** Serialized zero-value continuation: a fresh accumulator at offset 0, used to drive a FULL parse from the start through the same resumable path. */
|
|
@@ -3423,7 +3484,7 @@ function parsePriorCodexState(row) {
|
|
|
3423
3484
|
return null;
|
|
3424
3485
|
try {
|
|
3425
3486
|
const parsed = JSON.parse(row.parserState);
|
|
3426
|
-
if (parsed?.v !==
|
|
3487
|
+
if (parsed?.v !== 2 || typeof parsed.offset !== 'number' || parsed.toolCalls?.v !== 1)
|
|
3427
3488
|
return null;
|
|
3428
3489
|
return parsed;
|
|
3429
3490
|
}
|
|
@@ -12,7 +12,14 @@ import type { SessionAgentId, SessionEvent } from './types.js';
|
|
|
12
12
|
* Node's ~512MB string ceiling with a healthy margin.
|
|
13
13
|
*/
|
|
14
14
|
export declare const SESSION_FILE_MAX_BYTES = 200000000;
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Strip terminal control sequences that a malicious session file could use to
|
|
17
|
+
* hijack the user's terminal (clipboard via OSC 52, scrollback wipe, alt-screen
|
|
18
|
+
* takeover, cursor moves, etc.). Allowed through: tab (0x09), newline (0x0a),
|
|
19
|
+
* carriage return (0x0d). Everything else in the C0/C1 range and every CSI/OSC
|
|
20
|
+
* escape is dropped.
|
|
21
|
+
*/
|
|
22
|
+
export { sanitizeForTerminal } from '../redact.js';
|
|
16
23
|
/** In-place sanitize every user-visible string field on a list of events. */
|
|
17
24
|
export declare function sanitizeEvents(events: SessionEvent[]): void;
|
|
18
25
|
/**
|