@phnx-labs/agents-cli 1.22.56 → 1.22.58
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 +70 -0
- package/README.md +4 -4
- package/dist/bootstrap.js +11 -2
- package/dist/cli/command-registry.d.ts +0 -1
- package/dist/cli/command-registry.js +0 -3
- package/dist/commands/accounts.js +7 -3
- package/dist/commands/apply.js +10 -2
- package/dist/commands/exec.js +1 -1
- package/dist/commands/fork.d.ts +23 -10
- package/dist/commands/fork.js +115 -58
- package/dist/commands/hooks.js +4 -4
- package/dist/commands/insights.d.ts +7 -5
- package/dist/commands/insights.js +16 -9
- package/dist/commands/monitors.js +11 -0
- package/dist/commands/perf.d.ts +16 -7
- package/dist/commands/perf.js +29 -20
- package/dist/commands/prune.js +5 -3
- package/dist/commands/routines.d.ts +8 -0
- package/dist/commands/routines.js +57 -3
- package/dist/commands/rules.js +1 -1
- package/dist/commands/sessions-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +16 -0
- package/dist/commands/sessions.js +1 -0
- package/dist/commands/share.d.ts +14 -0
- package/dist/commands/share.js +43 -2
- package/dist/commands/ssh.js +24 -14
- package/dist/commands/status.js +1 -1
- package/dist/commands/sync.js +83 -7
- package/dist/commands/traces.js +7 -0
- package/dist/commands/trash.d.ts +2 -2
- package/dist/commands/trash.js +2 -6
- package/dist/commands/versions.d.ts +2 -2
- package/dist/commands/versions.js +1 -10
- package/dist/commands/view.d.ts +2 -2
- package/dist/commands/view.js +7 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +14 -0
- package/dist/lib/account-registry.d.ts +5 -1
- package/dist/lib/account-registry.js +47 -14
- package/dist/lib/accounting/capacity.d.ts +18 -7
- package/dist/lib/accounting/capacity.js +19 -8
- package/dist/lib/accounting/usage-ingest.d.ts +1 -0
- package/dist/lib/accounting/usage-ingest.js +75 -0
- package/dist/lib/accounting/usage-sync.d.ts +97 -0
- package/dist/lib/accounting/usage-sync.js +203 -0
- package/dist/lib/accounting/usage.d.ts +48 -2
- package/dist/lib/accounting/usage.js +79 -2
- package/dist/lib/agent-spec/agents.js +1 -1
- package/dist/lib/analytics/mix-commands.d.ts +8 -7
- package/dist/lib/analytics/mix-commands.js +50 -73
- package/dist/lib/auth-mint.d.ts +11 -1
- package/dist/lib/auth-mint.js +21 -6
- package/dist/lib/browser/ipc.d.ts +8 -0
- package/dist/lib/browser/ipc.js +87 -0
- package/dist/lib/browser/service.d.ts +19 -0
- package/dist/lib/browser/service.js +96 -11
- package/dist/lib/browser/sessions-list.js +10 -1
- package/dist/lib/daemon/daemon.js +5 -0
- package/dist/lib/daemon/runner.d.ts +3 -0
- package/dist/lib/daemon/runner.js +95 -53
- package/dist/lib/daemon/usage-sync-service.d.ts +21 -0
- package/dist/lib/daemon/usage-sync-service.js +42 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/device-config.d.ts +17 -6
- package/dist/lib/device-config.js +25 -11
- package/dist/lib/devices/connect.d.ts +17 -8
- package/dist/lib/devices/connect.js +31 -14
- package/dist/lib/devices/pool.d.ts +4 -3
- package/dist/lib/devices/pool.js +13 -5
- package/dist/lib/doctor-diff.js +77 -7
- package/dist/lib/exec.d.ts +6 -41
- package/dist/lib/exec.js +6 -41
- package/dist/lib/fleet/manifest.d.ts +17 -0
- package/dist/lib/fleet/manifest.js +26 -0
- package/dist/lib/git.d.ts +13 -1
- package/dist/lib/git.js +36 -7
- package/dist/lib/harness/adapter.d.ts +7 -7
- package/dist/lib/harness/adapters/claude.js +3 -2
- package/dist/lib/hooks/install.d.ts +27 -11
- package/dist/lib/hooks/install.js +42 -17
- package/dist/lib/hosts/reconnect.d.ts +52 -203
- package/dist/lib/hosts/reconnect.js +64 -284
- package/dist/lib/hosts/remote-cmd.d.ts +9 -0
- package/dist/lib/hosts/remote-cmd.js +22 -0
- package/dist/lib/installations/migrate.d.ts +6 -120
- package/dist/lib/installations/migrate.js +27 -259
- package/dist/lib/installations/shims.d.ts +13 -95
- package/dist/lib/installations/shims.js +22 -139
- package/dist/lib/installations/store.js +1 -1
- package/dist/lib/installations/versions.d.ts +26 -133
- package/dist/lib/installations/versions.js +41 -204
- package/dist/lib/perf/db.d.ts +1 -1
- package/dist/lib/perf/db.js +1 -1
- package/dist/lib/plugins/skills.d.ts +8 -1
- package/dist/lib/plugins/skills.js +18 -2
- package/dist/lib/refresh.d.ts +9 -0
- package/dist/lib/refresh.js +3 -1
- package/dist/lib/routine-readiness.d.ts +15 -1
- package/dist/lib/routine-readiness.js +41 -0
- package/dist/lib/sandbox.d.ts +4 -1
- package/dist/lib/sandbox.js +30 -1
- package/dist/lib/secrets/agent.d.ts +80 -225
- package/dist/lib/secrets/agent.js +139 -401
- package/dist/lib/secrets/bundles.d.ts +73 -222
- package/dist/lib/secrets/bundles.js +168 -467
- package/dist/lib/secrets/reaper.d.ts +28 -70
- package/dist/lib/secrets/reaper.js +30 -85
- package/dist/lib/secrets/remote.d.ts +42 -129
- package/dist/lib/secrets/remote.js +55 -173
- package/dist/lib/self-heal/checks/install-staging.d.ts +4 -0
- package/dist/lib/self-heal/checks/install-staging.js +96 -0
- package/dist/lib/self-heal/registry.js +2 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/self-update.d.ts +23 -0
- package/dist/lib/self-update.js +50 -0
- package/dist/lib/session/active.d.ts +16 -32
- package/dist/lib/session/active.js +10 -68
- package/dist/lib/session/db.d.ts +24 -36
- package/dist/lib/session/db.js +143 -44
- package/dist/lib/session/discover.d.ts +6 -58
- package/dist/lib/session/discover.js +5 -43
- package/dist/lib/session/fork.d.ts +45 -26
- package/dist/lib/session/fork.js +32 -95
- package/dist/lib/session/parse.d.ts +1 -19
- package/dist/lib/session/parse.js +2 -15
- package/dist/lib/session/tool-calls.d.ts +43 -1
- package/dist/lib/session/tool-calls.js +74 -44
- package/dist/lib/session/tool-store.d.ts +33 -2
- package/dist/lib/session/tool-store.js +56 -3
- package/dist/lib/staleness/writers/sources.d.ts +5 -0
- package/dist/lib/staleness/writers/sources.js +2 -1
- package/dist/lib/startup/command-registry.d.ts +8 -2
- package/dist/lib/startup/command-registry.js +12 -4
- package/dist/lib/sync-status.d.ts +22 -0
- package/dist/lib/sync-status.js +27 -0
- package/dist/lib/sync-umbrella.d.ts +9 -0
- package/dist/lib/sync-umbrella.js +21 -2
- package/dist/lib/traces/insights.d.ts +47 -14
- package/dist/lib/traces/insights.js +92 -21
- package/dist/lib/traces/phenotype.d.ts +23 -3
- package/dist/lib/traces/phenotype.js +72 -24
- package/dist/lib/traces/sync.d.ts +15 -0
- package/dist/lib/traces/sync.js +104 -19
- package/dist/lib/traces/worker-template.js +154 -1
- package/package.json +1 -1
|
@@ -54,14 +54,7 @@ export interface DiscoverOptions {
|
|
|
54
54
|
idExact?: string;
|
|
55
55
|
/** Session id prefix — a targeted indexed lookup with no scan (RUSH-2477). */
|
|
56
56
|
idPrefix?: string;
|
|
57
|
-
/**
|
|
58
|
-
* Cold-miss repair: when another live process already holds the scan claim,
|
|
59
|
-
* wait (bounded) for that in-flight scan to finish before reading the index,
|
|
60
|
-
* instead of returning the pre-scan snapshot (RUSH-2682). A caller repairing a
|
|
61
|
-
* "not indexed yet" miss wants the fresh result the concurrent scan is about to
|
|
62
|
-
* write, not the stale read that just missed. Ignored when THIS process wins
|
|
63
|
-
* the claim (it scans itself) or no scan is in progress.
|
|
64
|
-
*/
|
|
57
|
+
/** On a cold miss, briefly await the scan already holding the single-flight claim. */
|
|
65
58
|
waitForScan?: boolean;
|
|
66
59
|
}
|
|
67
60
|
/** Progress report emitted during incremental scanning. */
|
|
@@ -181,41 +174,15 @@ export declare function discoverSessions(options?: DiscoverOptions): Promise<Ses
|
|
|
181
174
|
interface IncrementalScanResult {
|
|
182
175
|
/** True when this process won the single-flight claim and ran the scan. */
|
|
183
176
|
claimed: boolean;
|
|
184
|
-
/**
|
|
185
|
-
* Transcripts parsed this scan — i.e. those whose (mtime, size) changed. Zero
|
|
186
|
-
* is the steady state on an idle box and does NOT mean the scan was skipped;
|
|
187
|
-
* read `claimed` for that.
|
|
188
|
-
*
|
|
189
|
-
* Twelve of the 13 SESSION_AGENTS contribute, including OpenCode, whose
|
|
190
|
-
* scanner filters to sessions whose own per-session stamp changed and reports
|
|
191
|
-
* that batch — so a tick whose only changed sessions live there no longer
|
|
192
|
-
* reports 0 (RUSH-2691). OpenClaw is the exception and contributes nothing:
|
|
193
|
-
* its scanner has no change detection to report (a TTL gate, a fresh stamp
|
|
194
|
-
* every run, and an entry list rebuilt as the current inventory), so counting
|
|
195
|
-
* it would overstate rather than measure. See `scanOpenClawIncremental`.
|
|
196
|
-
*/
|
|
177
|
+
/** Changed transcripts parsed; zero with `claimed: true` is a successful no-op scan. */
|
|
197
178
|
scanned: number;
|
|
198
179
|
}
|
|
199
|
-
/**
|
|
200
|
-
* The write half of {@link discoverSessions}: claim the single-flight scan slot,
|
|
201
|
-
* incrementally index this host's transcript dirs, and report what was parsed.
|
|
202
|
-
*
|
|
203
|
-
* Split out so a caller that only wants the index refreshed — the daemon's warm
|
|
204
|
-
* tick — can run it WITHOUT the listing query `discoverSessions` ends with. That
|
|
205
|
-
* query is not free: it applies a cwd filter, runs the `archived_at`-writing
|
|
206
|
-
* existence check, and can issue a Linear fetch, none of which index anything
|
|
207
|
-
* (RUSH-2691). Keeping one implementation here is also what stops the tick and
|
|
208
|
-
* the foreground path from drifting apart.
|
|
209
|
-
*/
|
|
180
|
+
/** Separate write half so daemon warming does not pay for listing or external enrichment. */
|
|
210
181
|
export declare function scanSessionsIncremental(options?: {
|
|
211
182
|
agent?: SessionAgentId;
|
|
212
183
|
onProgress?: (p: ScanProgress) => void;
|
|
213
184
|
}): Promise<IncrementalScanResult>;
|
|
214
|
-
/**
|
|
215
|
-
* Poll until no live process holds the scan claim, or the bound elapses
|
|
216
|
-
* (RUSH-2682). Bounded so a wedged/slow scan can never hang a foreground preview.
|
|
217
|
-
* Exported for the cold-miss repair test.
|
|
218
|
-
*/
|
|
185
|
+
/** Bounded wait so a wedged scan cannot hang a foreground cold-miss repair. */
|
|
219
186
|
export declare function waitForScanToSettle(timeoutMs?: number, pollMs?: number): Promise<boolean>;
|
|
220
187
|
/** Read the current SQLite snapshot without scanning or parsing transcript files. */
|
|
221
188
|
export declare function queryIndexedSessions(options?: DiscoverOptions, indexedOptions?: {
|
|
@@ -223,27 +190,8 @@ export declare function queryIndexedSessions(options?: DiscoverOptions, indexedO
|
|
|
223
190
|
skipExistenceCheck?: boolean;
|
|
224
191
|
}): Promise<SessionMeta[]>;
|
|
225
192
|
/**
|
|
226
|
-
* Resolve
|
|
227
|
-
*
|
|
228
|
-
* A plain WAL read through `queryIndexedSessions` — same origin-machine
|
|
229
|
-
* attribution and managed scoping every indexed read gets — with NO incremental
|
|
230
|
-
* discovery scan (so none of `tryClaimScan`/`releaseScan`'s `BEGIN IMMEDIATE`
|
|
231
|
-
* writer lock) and NO fleet SSH fan-out. This is the crash-restart storm path
|
|
232
|
-
* (RUSH-2477): dozens of `sessions resume <id>` at once for a known local id must
|
|
233
|
-
* each be a cheap read, never a writer-lock contender or a dial into the
|
|
234
|
-
* not-yet-up tailnet. Exact id first, then prefix (matching `findSessionsById`),
|
|
235
|
-
* so a complete id never also drags in its prefix siblings. Returns `[]` on a
|
|
236
|
-
* genuine local miss, leaving the caller to fall back to the fleet resolver.
|
|
237
|
-
*
|
|
238
|
-
* The existence check is left ON (`skipExistenceCheck: false`), exactly as the old
|
|
239
|
-
* `discoverSessions` path and `findSessionsById` do (RUSH-2436): it KEEPS a
|
|
240
|
-
* file-gone session whose user turns still live in `session_text` (flagged
|
|
241
|
-
* archived) and SUPPRESSES a contentless phantom — so a phantom id misses here and
|
|
242
|
-
* falls through to the fleet resolver, instead of resolving to a row with no real
|
|
243
|
-
* transcript to resume. For a present transcript — the storm's actual case, since
|
|
244
|
-
* the crashed tabs' files are on disk — the check does no writes, so the lock-free
|
|
245
|
-
* guarantee holds; it only writes to (un)archive a genuinely missing or resurrected
|
|
246
|
-
* file, which is not the 20-at-once resume path.
|
|
193
|
+
* Resolve locally without scanning or fleet I/O, keeping concurrent crash recovery lock-light.
|
|
194
|
+
* The existence check preserves archived content while rejecting transcriptless phantoms.
|
|
247
195
|
*/
|
|
248
196
|
export declare function resolveIndexedSessionById(idQuery: string): Promise<SessionMeta[]>;
|
|
249
197
|
/**
|
|
@@ -131,12 +131,7 @@ async function applyJsonlAppend(filePath, fromOffset, wasDroppingOversizedLine,
|
|
|
131
131
|
* between `agents sessions` calls but is still being written to.
|
|
132
132
|
*/
|
|
133
133
|
const HOT_FILE_WINDOW_MS = 600_000;
|
|
134
|
-
/**
|
|
135
|
-
* Kill-switch: set `AGENTS_SESSIONS_NO_DIR_LEDGER=1` to force the old full-walk
|
|
136
|
-
* path (readdir + per-file stat every dir, every run — the pre-A-2 behavior),
|
|
137
|
-
* skipping the dir_ledger short-circuit entirely. One env var reverts a field
|
|
138
|
-
* regression to today's behavior.
|
|
139
|
-
*/
|
|
134
|
+
/** Emergency kill-switch for the directory-ledger optimization. */
|
|
140
135
|
function dirLedgerDisabled() {
|
|
141
136
|
const v = process.env.AGENTS_SESSIONS_NO_DIR_LEDGER;
|
|
142
137
|
return v === '1' || v === 'true';
|
|
@@ -175,17 +170,7 @@ export async function discoverSessions(options) {
|
|
|
175
170
|
skipExistenceCheck: options?.skipExistenceCheck ?? false,
|
|
176
171
|
});
|
|
177
172
|
}
|
|
178
|
-
/**
|
|
179
|
-
* The write half of {@link discoverSessions}: claim the single-flight scan slot,
|
|
180
|
-
* incrementally index this host's transcript dirs, and report what was parsed.
|
|
181
|
-
*
|
|
182
|
-
* Split out so a caller that only wants the index refreshed — the daemon's warm
|
|
183
|
-
* tick — can run it WITHOUT the listing query `discoverSessions` ends with. That
|
|
184
|
-
* query is not free: it applies a cwd filter, runs the `archived_at`-writing
|
|
185
|
-
* existence check, and can issue a Linear fetch, none of which index anything
|
|
186
|
-
* (RUSH-2691). Keeping one implementation here is also what stops the tick and
|
|
187
|
-
* the foreground path from drifting apart.
|
|
188
|
-
*/
|
|
173
|
+
/** Separate write half so daemon warming does not pay for listing or external enrichment. */
|
|
189
174
|
export async function scanSessionsIncremental(options) {
|
|
190
175
|
// Touch the DB so the schema is ready and connection is cached for this run.
|
|
191
176
|
getDB();
|
|
@@ -222,11 +207,7 @@ export async function scanSessionsIncremental(options) {
|
|
|
222
207
|
scanned += n;
|
|
223
208
|
return { claimed: true, scanned };
|
|
224
209
|
}
|
|
225
|
-
/**
|
|
226
|
-
* Poll until no live process holds the scan claim, or the bound elapses
|
|
227
|
-
* (RUSH-2682). Bounded so a wedged/slow scan can never hang a foreground preview.
|
|
228
|
-
* Exported for the cold-miss repair test.
|
|
229
|
-
*/
|
|
210
|
+
/** Bounded wait so a wedged scan cannot hang a foreground cold-miss repair. */
|
|
230
211
|
export async function waitForScanToSettle(timeoutMs = WAIT_FOR_SCAN_TIMEOUT_MS, pollMs = WAIT_FOR_SCAN_POLL_MS) {
|
|
231
212
|
const deadline = Date.now() + timeoutMs;
|
|
232
213
|
while (scanInProgressByLivePid()) {
|
|
@@ -259,27 +240,8 @@ export async function queryIndexedSessions(options, indexedOptions = {}) {
|
|
|
259
240
|
return scopeToManaged(sessions, agents, options);
|
|
260
241
|
}
|
|
261
242
|
/**
|
|
262
|
-
* Resolve
|
|
263
|
-
*
|
|
264
|
-
* A plain WAL read through `queryIndexedSessions` — same origin-machine
|
|
265
|
-
* attribution and managed scoping every indexed read gets — with NO incremental
|
|
266
|
-
* discovery scan (so none of `tryClaimScan`/`releaseScan`'s `BEGIN IMMEDIATE`
|
|
267
|
-
* writer lock) and NO fleet SSH fan-out. This is the crash-restart storm path
|
|
268
|
-
* (RUSH-2477): dozens of `sessions resume <id>` at once for a known local id must
|
|
269
|
-
* each be a cheap read, never a writer-lock contender or a dial into the
|
|
270
|
-
* not-yet-up tailnet. Exact id first, then prefix (matching `findSessionsById`),
|
|
271
|
-
* so a complete id never also drags in its prefix siblings. Returns `[]` on a
|
|
272
|
-
* genuine local miss, leaving the caller to fall back to the fleet resolver.
|
|
273
|
-
*
|
|
274
|
-
* The existence check is left ON (`skipExistenceCheck: false`), exactly as the old
|
|
275
|
-
* `discoverSessions` path and `findSessionsById` do (RUSH-2436): it KEEPS a
|
|
276
|
-
* file-gone session whose user turns still live in `session_text` (flagged
|
|
277
|
-
* archived) and SUPPRESSES a contentless phantom — so a phantom id misses here and
|
|
278
|
-
* falls through to the fleet resolver, instead of resolving to a row with no real
|
|
279
|
-
* transcript to resume. For a present transcript — the storm's actual case, since
|
|
280
|
-
* the crashed tabs' files are on disk — the check does no writes, so the lock-free
|
|
281
|
-
* guarantee holds; it only writes to (un)archive a genuinely missing or resurrected
|
|
282
|
-
* file, which is not the 20-at-once resume path.
|
|
243
|
+
* Resolve locally without scanning or fleet I/O, keeping concurrent crash recovery lock-light.
|
|
244
|
+
* The existence check preserves archived content while rejecting transcriptless phantoms.
|
|
283
245
|
*/
|
|
284
246
|
export async function resolveIndexedSessionById(idQuery) {
|
|
285
247
|
const q = idQuery.trim();
|
|
@@ -1,32 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session forking — branch an existing conversation into a new, independent
|
|
3
|
+
* sibling that continues the work, leaving the original untouched.
|
|
4
|
+
*
|
|
5
|
+
* `resume` continues the SAME conversation (same id, same file — it appends).
|
|
6
|
+
* `fork` launches a NEW same-harness session, load-balanced, seeded with a
|
|
7
|
+
* recap of the source so it picks up where the original left off. This is the
|
|
8
|
+
* "git branch" of conversations.
|
|
9
|
+
*
|
|
10
|
+
* The recap — not a transcript copy — is what makes fork work across every
|
|
11
|
+
* device and every REPL harness: the sibling is handed plain text as its opening
|
|
12
|
+
* input, so it never has to reach a transcript that may live on another box. The
|
|
13
|
+
* source is resolved cross-fleet by the same resolver `preview` uses; this module
|
|
14
|
+
* owns only the pure recap text the resolved data folds into.
|
|
15
|
+
*/
|
|
1
16
|
import type { SessionMeta } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
filePath: string;
|
|
14
|
-
/** The label applied to the fork. */
|
|
17
|
+
/** File-change tally as `sessions preview --json` serializes it (digest.changes). */
|
|
18
|
+
export interface ForkRecapChanges {
|
|
19
|
+
created: number;
|
|
20
|
+
modified: number;
|
|
21
|
+
deleted: number;
|
|
22
|
+
}
|
|
23
|
+
/** Everything the recap seed is built from — resolved cross-fleet before launch. */
|
|
24
|
+
export interface ForkRecapInput {
|
|
25
|
+
/** Source harness id — the sibling launches the same one. */
|
|
26
|
+
agent: string;
|
|
27
|
+
/** Display label for the source (label → topic → short id, resolved by the caller). */
|
|
15
28
|
label: string;
|
|
29
|
+
/** Source working directory, so the sibling re-roots itself. */
|
|
30
|
+
cwd?: string;
|
|
31
|
+
/** Linear/GitHub ticket the source was bound to, if any. */
|
|
32
|
+
ticketId?: string;
|
|
33
|
+
/** Device that owns the source transcript, for the `/continue` escape hatch. */
|
|
34
|
+
machine?: string;
|
|
35
|
+
/** Short + full id, so the sibling can pull full history with `/continue <id>`. */
|
|
36
|
+
shortId: string;
|
|
37
|
+
id: string;
|
|
38
|
+
/** The source's last assistant line — the single best "where it left off" signal. */
|
|
39
|
+
lastAssistant?: string;
|
|
40
|
+
/** Changed-files tally so far. */
|
|
41
|
+
changes?: ForkRecapChanges;
|
|
16
42
|
}
|
|
43
|
+
/** Resolve the human display label the caller passes in from a raw SessionMeta. */
|
|
44
|
+
export declare function forkLabelFor(session: Pick<SessionMeta, 'label' | 'topic' | 'shortId'>): string;
|
|
17
45
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* Copies `source.filePath` to a new `<uuid>.jsonl` beside it, rewrites the
|
|
21
|
-
* embedded session id, registers the new session in the index, and records a
|
|
22
|
-
* `--name`-style label. Returns the new ids/path. Throws if the source
|
|
23
|
-
* transcript is missing.
|
|
46
|
+
* Build the recap-seed prompt handed to the forked sibling as its opening input.
|
|
24
47
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* @param now ISO timestamp to stamp the fork with (injectable for tests).
|
|
48
|
+
* Pure and deterministic (unit-tested) — no filesystem, no spawn — so the launch
|
|
49
|
+
* orchestration in `commands/fork.ts` stays the only side-effecting layer.
|
|
28
50
|
*/
|
|
29
|
-
export declare function
|
|
30
|
-
name?: string;
|
|
31
|
-
now?: string;
|
|
32
|
-
}): ForkResult;
|
|
51
|
+
export declare function buildForkRecap(input: ForkRecapInput): string;
|
package/dist/lib/session/fork.js
CHANGED
|
@@ -1,102 +1,39 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* resumes natively via `--resume`. A fork is therefore: copy the transcript to
|
|
12
|
-
* a new-uuid filename in the same directory, rewrite the embedded `sessionId`
|
|
13
|
-
* on each line, register the new session in the index, and label it. Other
|
|
14
|
-
* agents (codex single-file; grok/kimi multi-file; opencode DB-only) are a
|
|
15
|
-
* natural follow-up and are refused up front for now.
|
|
16
|
-
*/
|
|
17
|
-
import { randomUUID } from 'crypto';
|
|
18
|
-
import * as fs from 'fs';
|
|
19
|
-
import * as path from 'path';
|
|
20
|
-
import { upsertSession } from './db.js';
|
|
21
|
-
import { recordRunName } from './run-names.js';
|
|
22
|
-
import { deriveShortId } from '../text/short-id.js';
|
|
23
|
-
/** Agents that `fork` can branch today (see the module doc for why). */
|
|
24
|
-
export const FORKABLE_AGENTS = ['claude'];
|
|
25
|
-
/** Whether a session's agent can be forked by {@link forkSession}. */
|
|
26
|
-
export function isForkableAgent(agent) {
|
|
27
|
-
return FORKABLE_AGENTS.includes(agent);
|
|
1
|
+
/** Longest last-assistant excerpt carried into the seed — enough to convey intent
|
|
2
|
+
* without pasting a wall of text (decision: Recap, not Full digest). */
|
|
3
|
+
const LAST_LINE_CAP = 400;
|
|
4
|
+
/** Collapse whitespace and cap length so a multi-paragraph final message becomes
|
|
5
|
+
* one scannable recap line. */
|
|
6
|
+
function trimLastLine(text) {
|
|
7
|
+
const collapsed = text.replace(/\s+/g, ' ').trim();
|
|
8
|
+
if (collapsed.length <= LAST_LINE_CAP)
|
|
9
|
+
return collapsed;
|
|
10
|
+
return `${collapsed.slice(0, LAST_LINE_CAP).trimEnd()}…`;
|
|
28
11
|
}
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
* (keeps the in-file id consistent with the new filename); malformed lines are
|
|
33
|
-
* passed through untouched.
|
|
34
|
-
*/
|
|
35
|
-
function rewriteSessionId(transcript, newId) {
|
|
36
|
-
return transcript
|
|
37
|
-
.split('\n')
|
|
38
|
-
.map((line) => {
|
|
39
|
-
if (!line.trim())
|
|
40
|
-
return line;
|
|
41
|
-
try {
|
|
42
|
-
const obj = JSON.parse(line);
|
|
43
|
-
if (typeof obj.sessionId === 'string') {
|
|
44
|
-
obj.sessionId = newId;
|
|
45
|
-
return JSON.stringify(obj);
|
|
46
|
-
}
|
|
47
|
-
return line;
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
return line;
|
|
51
|
-
}
|
|
52
|
-
})
|
|
53
|
-
.join('\n');
|
|
12
|
+
/** Resolve the human display label the caller passes in from a raw SessionMeta. */
|
|
13
|
+
export function forkLabelFor(session) {
|
|
14
|
+
return session.label || session.topic || session.shortId;
|
|
54
15
|
}
|
|
55
16
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* Copies `source.filePath` to a new `<uuid>.jsonl` beside it, rewrites the
|
|
59
|
-
* embedded session id, registers the new session in the index, and records a
|
|
60
|
-
* `--name`-style label. Returns the new ids/path. Throws if the source
|
|
61
|
-
* transcript is missing.
|
|
17
|
+
* Build the recap-seed prompt handed to the forked sibling as its opening input.
|
|
62
18
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* @param now ISO timestamp to stamp the fork with (injectable for tests).
|
|
19
|
+
* Pure and deterministic (unit-tested) — no filesystem, no spawn — so the launch
|
|
20
|
+
* orchestration in `commands/fork.ts` stays the only side-effecting layer.
|
|
66
21
|
*/
|
|
67
|
-
export function
|
|
68
|
-
|
|
69
|
-
|
|
22
|
+
export function buildForkRecap(input) {
|
|
23
|
+
const lines = [];
|
|
24
|
+
lines.push(`Continue a prior ${input.agent} session ("${input.label}"). Pick up where it left off — do not restart it.`);
|
|
25
|
+
if (input.cwd)
|
|
26
|
+
lines.push(`Working directory: ${input.cwd}`);
|
|
27
|
+
if (input.ticketId)
|
|
28
|
+
lines.push(`Ticket: ${input.ticketId}`);
|
|
29
|
+
const last = input.lastAssistant ? trimLastLine(input.lastAssistant) : '';
|
|
30
|
+
if (last)
|
|
31
|
+
lines.push(`It last said: "${last}"`);
|
|
32
|
+
const chg = input.changes;
|
|
33
|
+
if (chg && (chg.created || chg.modified || chg.deleted)) {
|
|
34
|
+
lines.push(`Changes so far: +${chg.created} ~${chg.modified} -${chg.deleted}.`);
|
|
70
35
|
}
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const filePath = path.join(dir, `${newId}.jsonl`);
|
|
75
|
-
const transcript = fs.readFileSync(source.filePath, 'utf-8');
|
|
76
|
-
const rewritten = rewriteSessionId(transcript, newId);
|
|
77
|
-
fs.writeFileSync(filePath, rewritten);
|
|
78
|
-
const original = source.label || source.topic || source.shortId;
|
|
79
|
-
const label = opts.name || `fork of ${original}`;
|
|
80
|
-
// Label sidecar (seeds the DB label; survives rescans until an agent title
|
|
81
|
-
// supersedes it), mirroring `agents run --name`.
|
|
82
|
-
recordRunName({ sessionId: newId, name: label, agent: source.agent, cwd: source.cwd });
|
|
83
|
-
// Register the new session so it resolves immediately (by `agents sessions resume`,
|
|
84
|
-
// `agents sessions`, etc.) without waiting for the next scan.
|
|
85
|
-
const stamp = opts.now ?? new Date().toISOString();
|
|
86
|
-
const meta = {
|
|
87
|
-
...source,
|
|
88
|
-
id: newId,
|
|
89
|
-
shortId,
|
|
90
|
-
filePath,
|
|
91
|
-
label,
|
|
92
|
-
timestamp: stamp,
|
|
93
|
-
lastActivity: stamp,
|
|
94
|
-
// The fork has not opened its own PR / team; drop origin-specific refs.
|
|
95
|
-
prUrl: undefined,
|
|
96
|
-
prNumber: undefined,
|
|
97
|
-
teamOrigin: undefined,
|
|
98
|
-
spawnedTeam: undefined,
|
|
99
|
-
};
|
|
100
|
-
upsertSession(meta, rewritten);
|
|
101
|
-
return { newId, shortId, filePath, label };
|
|
36
|
+
const origin = input.machine ? ` on ${input.machine}` : '';
|
|
37
|
+
lines.push(`Source session ${input.shortId}${origin} — run \`/continue ${input.id}\` if you need the full transcript.`);
|
|
38
|
+
return lines.join('\n');
|
|
102
39
|
}
|
|
@@ -28,28 +28,10 @@ export declare function sanitizeEvents(events: SessionEvent[]): void;
|
|
|
28
28
|
* ERR_STRING_TOO_LONG ceiling.
|
|
29
29
|
*/
|
|
30
30
|
export declare function safeReadSessionFile(filePath: string, maxBytes?: number): string;
|
|
31
|
-
/**
|
|
32
|
-
* Auto-detect agent type from file path and parse the session.
|
|
33
|
-
*/
|
|
34
31
|
export interface ParseSessionOptions {
|
|
35
32
|
/** Keep normalized tool results compact by default; renderers can request full output. */
|
|
36
33
|
maxToolOutputChars?: number;
|
|
37
|
-
/**
|
|
38
|
-
* Emit an `interrupt` event where the transcript records `[Request interrupted`.
|
|
39
|
-
*
|
|
40
|
-
* OFF by default, deliberately. That marker is not a user message, and the default
|
|
41
|
-
* event array is a versioned consumer contract: `agents sessions <id> --json`
|
|
42
|
-
* serializes it verbatim (see render.ts, issue #743), `computeSummaryStats` folds
|
|
43
|
-
* every event's timestamp into the session duration, and the live-state reader and
|
|
44
|
-
* tail renderer inspect fixed-size windows of the last N events. Emitting it
|
|
45
|
-
* unconditionally changed all four — a measured 12x duration swing on one real
|
|
46
|
-
* transcript, a new object in a published payload, and an eviction from the
|
|
47
|
-
* 12-event rate-limit window whose trigger shape (a trailing interrupt) is exactly
|
|
48
|
-
* a session the user just cancelled.
|
|
49
|
-
*
|
|
50
|
-
* `agents insights` opts in: an interruption is a real friction signal, and dropping
|
|
51
|
-
* it outright is what made it unrecoverable.
|
|
52
|
-
*/
|
|
34
|
+
/** Opt-in because interrupts are not user messages and would change the published event stream. */
|
|
53
35
|
includeInterrupts?: boolean;
|
|
54
36
|
}
|
|
55
37
|
export declare function parseSession(filePath: string, agent?: SessionAgentId, opts?: ParseSessionOptions): SessionEvent[];
|
|
@@ -117,14 +117,7 @@ function truncateNormalizedToolOutput(output, maxChars) {
|
|
|
117
117
|
return output;
|
|
118
118
|
return `${output.slice(0, maxChars)}\n\n[Output truncated: ${output.length - maxChars} characters omitted.]`;
|
|
119
119
|
}
|
|
120
|
-
/**
|
|
121
|
-
* Registry-dispatch table: each `SessionAgentId` to its offline transcript
|
|
122
|
-
* parser. Replaces the per-harness `switch` — the harness axis of Move 3. Kept
|
|
123
|
-
* as its own table (not on the HarnessAdapter registry) because its id domain is
|
|
124
|
-
* `SessionAgentId`: `rush` is a session agent with no `AgentId`, and the offline
|
|
125
|
-
* transcript reader is a deliberately separate concern from live team events.
|
|
126
|
-
* The `Record` is total, so a new session harness must add an entry here.
|
|
127
|
-
*/
|
|
120
|
+
/** Separate from HarnessAdapter because offline transcripts include session-only agents such as Rush. */
|
|
128
121
|
const TRANSCRIPT_PARSERS = {
|
|
129
122
|
claude: (filePath, opts) => parseClaude(filePath, opts),
|
|
130
123
|
codex: (filePath) => parseCodex(filePath),
|
|
@@ -146,13 +139,7 @@ export function parseSession(filePath, agent, opts = {}) {
|
|
|
146
139
|
throw new Error(`Cannot detect agent type from path: ${filePath}`);
|
|
147
140
|
}
|
|
148
141
|
const events = TRANSCRIPT_PARSERS[detected](filePath, opts);
|
|
149
|
-
//
|
|
150
|
-
// file gets stripped of terminal escapes here, so renderers downstream can
|
|
151
|
-
// safely splat values into chalk/console output. Same pass flags
|
|
152
|
-
// harness-injected `role=user` scaffolding (Claude `<bash-input>`/`<bash-stdout>`
|
|
153
|
-
// from `!`-prefix runs, `<system-reminder>`, etc.) as `_synthetic` so turn
|
|
154
|
-
// slicing and `--include user` count only genuine user intent — one place,
|
|
155
|
-
// every harness, instead of per-consumer regex.
|
|
142
|
+
// Sanitize untrusted strings and identify synthetic user scaffolding once for every consumer.
|
|
156
143
|
const maxToolOutputChars = opts.maxToolOutputChars ?? 500;
|
|
157
144
|
for (const e of events) {
|
|
158
145
|
if (e.type === 'tool_result' && e.output) {
|
|
@@ -10,12 +10,21 @@ export declare const TOOL_CHANGED_MAX_CALLS = 10000;
|
|
|
10
10
|
export declare const TOOL_INDEX_LIMIT_ORDINAL: number;
|
|
11
11
|
export declare const TOOL_TEXT_PROCESSING_MAX_BYTES: number;
|
|
12
12
|
export declare const TOOL_SHELL_PARSE_MAX_BYTES: number;
|
|
13
|
-
export declare const TOOL_INDEX_VERSION =
|
|
13
|
+
export declare const TOOL_INDEX_VERSION = 8;
|
|
14
14
|
export type ToolCallOutcome = 'ok' | 'error' | 'unknown';
|
|
15
15
|
export interface IndexedToolCall {
|
|
16
16
|
ordinal: number;
|
|
17
17
|
sourceCallId?: string;
|
|
18
18
|
timestamp: string;
|
|
19
|
+
/**
|
|
20
|
+
* When the call's RESULT record arrived — the call's own end time, taken from
|
|
21
|
+
* the tool_result transcript record at `finish()` (PHNX-3437). `timestamp` is
|
|
22
|
+
* the start; `endTimestamp - timestamp` is the call's own blocking duration,
|
|
23
|
+
* which the traces insight engine attributes as a failed call's wasted time.
|
|
24
|
+
* Undefined for a call that never produced a result (still pending at scan end)
|
|
25
|
+
* and for rows produced by an older extractor.
|
|
26
|
+
*/
|
|
27
|
+
endTimestamp?: string;
|
|
19
28
|
tool: string;
|
|
20
29
|
programs: string[];
|
|
21
30
|
programOccurrences: ShellProgramOccurrence[];
|
|
@@ -81,6 +90,39 @@ export declare class ToolCallCollector {
|
|
|
81
90
|
private removePending;
|
|
82
91
|
private markChanged;
|
|
83
92
|
}
|
|
93
|
+
/** The prior scan's resume point for an append-only event stream. */
|
|
94
|
+
export interface EventToolScanResumePoint {
|
|
95
|
+
/** ToolCallCollector snapshot after folding `eventCount` events. */
|
|
96
|
+
snapshot: ToolCallCollectorSnapshot;
|
|
97
|
+
/** How many events had been folded when the snapshot was taken. */
|
|
98
|
+
eventCount: number;
|
|
99
|
+
}
|
|
100
|
+
export interface EventToolScanResult {
|
|
101
|
+
/** The CHANGED calls — an append-safe upsert set, not the whole history. */
|
|
102
|
+
calls: IndexedToolCall[];
|
|
103
|
+
/** Serialized-ready snapshot to persist for the next incremental scan. */
|
|
104
|
+
snapshot: ToolCallCollectorSnapshot;
|
|
105
|
+
/** Events folded so far — the next scan's resume offset into `events`. */
|
|
106
|
+
eventCount: number;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Derive tool calls from a full-file harness's normalized events, optionally
|
|
110
|
+
* RESUMING from a prior scan of the same append-only stream.
|
|
111
|
+
*
|
|
112
|
+
* Without `prior` this is a full parse from event 0 (identical to the legacy
|
|
113
|
+
* {@link toolCallsFromEvents}). With `prior`, the collector is seeded from the
|
|
114
|
+
* prior snapshot and only events at or after `prior.eventCount` are folded — so
|
|
115
|
+
* an active session that grew by a few turns re-derives (and re-redacts) only
|
|
116
|
+
* those new tool calls instead of re-sanitizing its entire history on every
|
|
117
|
+
* daemon warm tick (PHNX-3411). The ordinals continue deterministically from the
|
|
118
|
+
* snapshot, so folding [0..k) then [k..n) yields the same index as folding
|
|
119
|
+
* [0..n) once, and the CHANGED set is safe to persist with `mode: 'append'`.
|
|
120
|
+
*
|
|
121
|
+
* The caller is responsible for only supplying `prior` when the stream is still
|
|
122
|
+
* an append of what was scanned before (same source, un-truncated,
|
|
123
|
+
* `prior.eventCount <= events.length`); anything else must full-scan.
|
|
124
|
+
*/
|
|
125
|
+
export declare function scanEventToolCalls(events: SessionEvent[], prior?: EventToolScanResumePoint): EventToolScanResult;
|
|
84
126
|
/** Build indexed calls from the normalized parser contract used by full-file harnesses. */
|
|
85
127
|
export declare function toolCallsFromEvents(events: SessionEvent[]): IndexedToolCall[];
|
|
86
128
|
export declare function toolCallKey(sessionId: string, ordinal: number): string;
|
|
@@ -12,7 +12,10 @@ export const TOOL_CHANGED_MAX_CALLS = 10_000;
|
|
|
12
12
|
export const TOOL_INDEX_LIMIT_ORDINAL = Number.MAX_SAFE_INTEGER;
|
|
13
13
|
export const TOOL_TEXT_PROCESSING_MAX_BYTES = 64 * 1024;
|
|
14
14
|
export const TOOL_SHELL_PARSE_MAX_BYTES = 64 * 1024;
|
|
15
|
-
|
|
15
|
+
// Bumped to 8 for the per-call end timestamp (PHNX-3437): the extractor now
|
|
16
|
+
// records when a call's result arrived, so a re-index re-derives it for rows
|
|
17
|
+
// stored by an older extractor.
|
|
18
|
+
export const TOOL_INDEX_VERSION = 8;
|
|
16
19
|
const BASE64_BLOCK = /(?:[A-Za-z0-9+/]{256,}={0,2})/g;
|
|
17
20
|
const SECRET_FIELD = /(?:token|secret|password|authorization|cookie|api[_-]?key|private[_-]?key)$/i;
|
|
18
21
|
const KNOWN_SECRET_VALUES = knownSecretValuesFromEnv();
|
|
@@ -310,7 +313,7 @@ function buildCall(ordinal, timestamp, tool, args, command, sourceCallId) {
|
|
|
310
313
|
}
|
|
311
314
|
export function toolCallEvidenceBytes(call) {
|
|
312
315
|
return Buffer.byteLength([
|
|
313
|
-
call.sourceCallId, call.timestamp, call.tool, call.input, call.errorCode,
|
|
316
|
+
call.sourceCallId, call.timestamp, call.endTimestamp, call.tool, call.input, call.errorCode,
|
|
314
317
|
call.output, call.error, call.parseError, ...call.programs,
|
|
315
318
|
...call.programOccurrences.map((occurrence) => `${occurrence.role}:${occurrence.program}`),
|
|
316
319
|
].filter((value) => typeof value === 'string').join('\0'));
|
|
@@ -363,6 +366,9 @@ export class ToolCallCollector {
|
|
|
363
366
|
const call = this.takePending(args.sourceCallId, tool);
|
|
364
367
|
if (!call)
|
|
365
368
|
return undefined;
|
|
369
|
+
if (typeof args.timestamp === 'string' && args.timestamp.length > 0) {
|
|
370
|
+
call.endTimestamp = sanitizeToolEvidenceText(args.timestamp, 128);
|
|
371
|
+
}
|
|
366
372
|
const outcome = args.outcome === 'ok' || args.outcome === 'error' || args.outcome === 'unknown'
|
|
367
373
|
? args.outcome
|
|
368
374
|
: undefined;
|
|
@@ -471,50 +477,74 @@ export class ToolCallCollector {
|
|
|
471
477
|
return true;
|
|
472
478
|
}
|
|
473
479
|
}
|
|
480
|
+
/** Fold one normalized event into a collector (the full-file harness contract). */
|
|
481
|
+
function foldEventIntoCollector(collector, event) {
|
|
482
|
+
if (event.type === 'tool_use') {
|
|
483
|
+
collector.start({
|
|
484
|
+
timestamp: event.timestamp,
|
|
485
|
+
tool: event.tool || 'unknown',
|
|
486
|
+
input: event.args,
|
|
487
|
+
command: event.command,
|
|
488
|
+
sourceCallId: event.callId,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
else if (event.type === 'tool_result') {
|
|
492
|
+
collector.finish({
|
|
493
|
+
timestamp: event.timestamp,
|
|
494
|
+
sourceCallId: event.callId,
|
|
495
|
+
tool: event.tool,
|
|
496
|
+
success: event.success,
|
|
497
|
+
outcome: event.outcome,
|
|
498
|
+
exitCode: event.exitCode,
|
|
499
|
+
statusCode: event.statusCode,
|
|
500
|
+
errorCode: event.errorCode,
|
|
501
|
+
output: event.output,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
else if (event.type === 'error' && event.tool) {
|
|
505
|
+
const structuredError = event.outcome === 'error' || event.success === false;
|
|
506
|
+
const evidence = event.content || event.output || 'Tool execution failed';
|
|
507
|
+
collector.finish({
|
|
508
|
+
timestamp: event.timestamp,
|
|
509
|
+
sourceCallId: event.callId,
|
|
510
|
+
tool: event.tool,
|
|
511
|
+
success: structuredError ? false : undefined,
|
|
512
|
+
outcome: event.outcome,
|
|
513
|
+
exitCode: event.exitCode,
|
|
514
|
+
statusCode: event.statusCode,
|
|
515
|
+
errorCode: event.errorCode,
|
|
516
|
+
error: structuredError ? evidence : undefined,
|
|
517
|
+
output: structuredError ? undefined : evidence,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Derive tool calls from a full-file harness's normalized events, optionally
|
|
523
|
+
* RESUMING from a prior scan of the same append-only stream.
|
|
524
|
+
*
|
|
525
|
+
* Without `prior` this is a full parse from event 0 (identical to the legacy
|
|
526
|
+
* {@link toolCallsFromEvents}). With `prior`, the collector is seeded from the
|
|
527
|
+
* prior snapshot and only events at or after `prior.eventCount` are folded — so
|
|
528
|
+
* an active session that grew by a few turns re-derives (and re-redacts) only
|
|
529
|
+
* those new tool calls instead of re-sanitizing its entire history on every
|
|
530
|
+
* daemon warm tick (PHNX-3411). The ordinals continue deterministically from the
|
|
531
|
+
* snapshot, so folding [0..k) then [k..n) yields the same index as folding
|
|
532
|
+
* [0..n) once, and the CHANGED set is safe to persist with `mode: 'append'`.
|
|
533
|
+
*
|
|
534
|
+
* The caller is responsible for only supplying `prior` when the stream is still
|
|
535
|
+
* an append of what was scanned before (same source, un-truncated,
|
|
536
|
+
* `prior.eventCount <= events.length`); anything else must full-scan.
|
|
537
|
+
*/
|
|
538
|
+
export function scanEventToolCalls(events, prior) {
|
|
539
|
+
const startIndex = prior ? Math.min(prior.eventCount, events.length) : 0;
|
|
540
|
+
const collector = new ToolCallCollector(prior?.snapshot);
|
|
541
|
+
for (let i = startIndex; i < events.length; i++)
|
|
542
|
+
foldEventIntoCollector(collector, events[i]);
|
|
543
|
+
return { calls: collector.drainChanged(), snapshot: collector.snapshot(), eventCount: events.length };
|
|
544
|
+
}
|
|
474
545
|
/** Build indexed calls from the normalized parser contract used by full-file harnesses. */
|
|
475
546
|
export function toolCallsFromEvents(events) {
|
|
476
|
-
|
|
477
|
-
for (const event of events) {
|
|
478
|
-
if (event.type === 'tool_use') {
|
|
479
|
-
collector.start({
|
|
480
|
-
timestamp: event.timestamp,
|
|
481
|
-
tool: event.tool || 'unknown',
|
|
482
|
-
input: event.args,
|
|
483
|
-
command: event.command,
|
|
484
|
-
sourceCallId: event.callId,
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
else if (event.type === 'tool_result') {
|
|
488
|
-
collector.finish({
|
|
489
|
-
timestamp: event.timestamp,
|
|
490
|
-
sourceCallId: event.callId,
|
|
491
|
-
tool: event.tool,
|
|
492
|
-
success: event.success,
|
|
493
|
-
outcome: event.outcome,
|
|
494
|
-
exitCode: event.exitCode,
|
|
495
|
-
statusCode: event.statusCode,
|
|
496
|
-
errorCode: event.errorCode,
|
|
497
|
-
output: event.output,
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
else if (event.type === 'error' && event.tool) {
|
|
501
|
-
const structuredError = event.outcome === 'error' || event.success === false;
|
|
502
|
-
const evidence = event.content || event.output || 'Tool execution failed';
|
|
503
|
-
collector.finish({
|
|
504
|
-
timestamp: event.timestamp,
|
|
505
|
-
sourceCallId: event.callId,
|
|
506
|
-
tool: event.tool,
|
|
507
|
-
success: structuredError ? false : undefined,
|
|
508
|
-
outcome: event.outcome,
|
|
509
|
-
exitCode: event.exitCode,
|
|
510
|
-
statusCode: event.statusCode,
|
|
511
|
-
errorCode: event.errorCode,
|
|
512
|
-
error: structuredError ? evidence : undefined,
|
|
513
|
-
output: structuredError ? undefined : evidence,
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
return collector.drainChanged();
|
|
547
|
+
return scanEventToolCalls(events).calls;
|
|
518
548
|
}
|
|
519
549
|
export function toolCallKey(sessionId, ordinal) {
|
|
520
550
|
return createHash('sha256').update(`${sessionId}\0${ordinal}`).digest('hex').slice(0, 20);
|