@phnx-labs/agents-cli 1.22.46 → 1.22.47
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 +279 -0
- package/LICENSE +102 -182
- package/README.md +31 -18
- package/dist/cli/command-registry.d.ts +1 -1
- package/dist/cli/command-registry.js +4 -2
- package/dist/commands/accounts.js +56 -12
- package/dist/commands/auth.js +7 -5
- package/dist/commands/browser-sessions-picker.js +9 -7
- package/dist/commands/browser.js +266 -15
- package/dist/commands/config.js +26 -4
- package/dist/commands/cost.d.ts +1 -1
- package/dist/commands/detach.d.ts +13 -0
- package/dist/commands/detach.js +11 -8
- package/dist/commands/exec.js +55 -38
- package/dist/commands/feed-watch.d.ts +2 -0
- package/dist/commands/feed-watch.js +28 -0
- package/dist/commands/feed.js +27 -0
- package/dist/commands/feedback.js +3 -19
- package/dist/commands/insights.d.ts +1 -1
- package/dist/commands/insights.js +2 -2
- package/dist/commands/lease.js +3 -3
- package/dist/commands/routines.js +1 -1
- package/dist/commands/sessions-picker-factory.d.ts +2 -1
- package/dist/commands/sessions-picker-factory.js +1 -1
- package/dist/commands/sessions-stop.d.ts +24 -0
- package/dist/commands/sessions-stop.js +66 -0
- package/dist/commands/sessions-trace.d.ts +82 -0
- package/dist/commands/sessions-trace.js +387 -0
- package/dist/commands/sessions.js +4 -0
- package/dist/commands/ssh.d.ts +15 -0
- package/dist/commands/ssh.js +194 -16
- package/dist/commands/teams.js +34 -2
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +18 -31
- package/dist/commands/watchdog.js +9 -14
- package/dist/lib/account-capabilities.d.ts +4 -0
- package/dist/lib/account-capabilities.js +13 -9
- package/dist/lib/account-registry.d.ts +2 -0
- package/dist/lib/account-registry.js +30 -2
- package/dist/lib/accounting/rotate.d.ts +9 -0
- package/dist/lib/accounting/rotate.js +12 -2
- package/dist/lib/accounting/usage.d.ts +70 -3
- package/dist/lib/accounting/usage.js +235 -63
- package/dist/lib/agent-modes.js +29 -1
- package/dist/lib/agent-spec/agents.d.ts +4 -3
- package/dist/lib/agent-spec/agents.js +14 -6
- package/dist/lib/analytics/mix-commands.d.ts +1 -1
- package/dist/lib/analytics/mix-commands.js +2 -2
- package/dist/lib/auth-health.d.ts +13 -3
- package/dist/lib/auth-health.js +50 -8
- package/dist/lib/browser/devices.d.ts +19 -0
- package/dist/lib/browser/devices.js +23 -0
- package/dist/lib/browser/ipc.d.ts +1 -1
- package/dist/lib/browser/ipc.js +33 -7
- package/dist/lib/browser/profiles.d.ts +106 -1
- package/dist/lib/browser/profiles.js +300 -17
- package/dist/lib/browser/remote-control.d.ts +26 -2
- package/dist/lib/browser/remote-control.js +33 -3
- package/dist/lib/browser/runtime-state.d.ts +9 -0
- package/dist/lib/browser/runtime-state.js +24 -8
- package/dist/lib/browser/service.d.ts +37 -0
- package/dist/lib/browser/service.js +78 -20
- package/dist/lib/browser/sessions-list.d.ts +1 -3
- package/dist/lib/browser/sessions-list.js +3 -16
- package/dist/lib/browser/types.d.ts +12 -1
- package/dist/lib/codex-policy.d.ts +11 -1
- package/dist/lib/codex-policy.js +20 -5
- package/dist/lib/config-keys.d.ts +4 -3
- package/dist/lib/config-keys.js +17 -7
- package/dist/lib/daemon/runner.js +15 -2
- package/dist/lib/daemon-ticks.js +2 -2
- package/dist/lib/device-config.d.ts +25 -8
- package/dist/lib/device-config.js +90 -17
- package/dist/lib/devices/config-migration.d.ts +7 -0
- package/dist/lib/devices/config-migration.js +67 -3
- package/dist/lib/devices/health.d.ts +27 -2
- package/dist/lib/devices/health.js +39 -9
- package/dist/lib/devices/interactive-host.d.ts +15 -0
- package/dist/lib/devices/interactive-host.js +53 -0
- package/dist/lib/devices/registry.d.ts +61 -4
- package/dist/lib/devices/registry.js +94 -38
- package/dist/lib/devices/stats-cache.d.ts +6 -0
- package/dist/lib/devices/stats-cache.js +10 -2
- package/dist/lib/exec.d.ts +5 -3
- package/dist/lib/exec.js +32 -16
- package/dist/lib/feed/answer.d.ts +29 -0
- package/dist/lib/feed/answer.js +145 -0
- package/dist/lib/feed/attention.d.ts +83 -0
- package/dist/lib/feed/attention.js +0 -0
- package/dist/lib/feed/feed.d.ts +109 -0
- package/dist/lib/feed/feed.js +154 -3
- package/dist/lib/feed/pr-status.d.ts +14 -0
- package/dist/lib/feed/pr-status.js +47 -0
- package/dist/lib/feed/watch.d.ts +59 -0
- package/dist/lib/feed/watch.js +180 -0
- package/dist/lib/fleet/capture.js +6 -0
- package/dist/lib/fleet/remote-login.js +8 -3
- package/dist/lib/fleet/types.d.ts +17 -0
- package/dist/lib/harness/adapter.d.ts +11 -0
- package/dist/lib/harness/adapters/claude.js +38 -24
- package/dist/lib/harness/adapters/codex.js +2 -4
- package/dist/lib/hooks/cache.js +8 -0
- package/dist/lib/hooks/match.d.ts +3 -0
- package/dist/lib/hooks/match.js +25 -0
- package/dist/lib/hosts/passthrough.js +16 -0
- package/dist/lib/hosts/registry.js +7 -0
- 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.d.ts +13 -0
- package/dist/lib/menubar/install-menubar.js +26 -2
- package/dist/lib/open-url.d.ts +60 -2
- package/dist/lib/open-url.js +167 -12
- package/dist/lib/refresh-coordinator.d.ts +1 -1
- package/dist/lib/refresh-coordinator.js +1 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/session/active.d.ts +67 -0
- package/dist/lib/session/active.js +69 -1
- package/dist/lib/session/db.js +4 -2
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +12 -6
- package/dist/lib/session/prompt.d.ts +35 -0
- package/dist/lib/session/prompt.js +56 -0
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +6 -1
- package/dist/lib/session/render.js +7 -3
- package/dist/lib/session/session-cache.js +5 -1
- package/dist/lib/session/trajectory-compare.d.ts +47 -0
- package/dist/lib/session/trajectory-compare.js +116 -0
- package/dist/lib/session/trajectory-html.d.ts +15 -0
- package/dist/lib/session/trajectory-html.js +750 -0
- package/dist/lib/session/trajectory-lineage.d.ts +92 -0
- package/dist/lib/session/trajectory-lineage.js +195 -0
- package/dist/lib/session/trajectory-text.d.ts +37 -0
- package/dist/lib/session/trajectory-text.js +277 -0
- package/dist/lib/session/trajectory.d.ts +102 -0
- package/dist/lib/session/trajectory.js +335 -0
- package/dist/lib/startup/command-registry.d.ts +3 -1
- package/dist/lib/startup/command-registry.js +5 -2
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +4 -2
- package/dist/lib/teams/agents.js +9 -0
- package/dist/lib/terminal/inject.d.ts +19 -0
- package/dist/lib/terminal/inject.js +19 -9
- package/dist/lib/tmux/session.d.ts +46 -13
- package/dist/lib/tmux/session.js +173 -27
- package/dist/lib/types.d.ts +16 -0
- package/dist/lib/usage-backoff.d.ts +2 -1
- package/dist/lib/usage-backoff.js +60 -19
- package/dist/lib/usage-refresh.d.ts +17 -3
- package/dist/lib/usage-refresh.js +57 -11
- package/dist/lib/watchdog/log.d.ts +1 -1
- package/dist/lib/watchdog/log.js +1 -1
- package/dist/lib/watchdog/runner.d.ts +36 -45
- package/dist/lib/watchdog/runner.js +150 -119
- package/dist/lib/watchdog/service.d.ts +0 -1
- package/dist/lib/watchdog/service.js +0 -1
- package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
- package/dist/lib/watchdog/watchdog-agent.js +59 -0
- package/dist/lib/watchdog/watchdog.d.ts +11 -2
- package/dist/lib/watchdog/watchdog.js +37 -104
- package/package.json +2 -2
- package/dist/commands/usage.d.ts +0 -27
- package/dist/commands/usage.js +0 -96
|
Binary file
|
|
Binary file
|
|
@@ -3,9 +3,21 @@
|
|
|
3
3
|
<plist version="1.0">
|
|
4
4
|
<dict>
|
|
5
5
|
<key>files</key>
|
|
6
|
-
<dict
|
|
6
|
+
<dict>
|
|
7
|
+
<key>Resources/AppIcon.icns</key>
|
|
8
|
+
<data>
|
|
9
|
+
DFq5H08EkhgWIC3UvGMR9B58BZw=
|
|
10
|
+
</data>
|
|
11
|
+
</dict>
|
|
7
12
|
<key>files2</key>
|
|
8
13
|
<dict>
|
|
14
|
+
<key>Resources/AppIcon.icns</key>
|
|
15
|
+
<dict>
|
|
16
|
+
<key>hash2</key>
|
|
17
|
+
<data>
|
|
18
|
+
mBSjM6jlvN7J1jQowsvqTl7CHM0pur0e6qsHMuzk5k0=
|
|
19
|
+
</data>
|
|
20
|
+
</dict>
|
|
9
21
|
<key>embedded.provisionprofile</key>
|
|
10
22
|
<dict>
|
|
11
23
|
<key>hash2</key>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CloudTaskStatus } from '../cloud/types.js';
|
|
2
2
|
import { type PidSessionEntry } from './pid-registry.js';
|
|
3
3
|
import { type HookSessionIndex } from './hook-sessions.js';
|
|
4
|
+
import { type UserPromptKind } from './prompt.js';
|
|
4
5
|
import { type SessionState, type SessionActivity, type AwaitingReason, type StructuredQuestion, type TodoProgress, type DetectedPr, type DetectedWorktree, type DetectedTicket } from './state.js';
|
|
5
6
|
import { type SessionAgentId, type SessionAttachment, type SessionMeta } from './types.js';
|
|
6
7
|
import { type SessionProvenance } from './provenance.js';
|
|
@@ -99,6 +100,15 @@ export type ActiveStatus = 'running' | 'idle' | 'queued' | 'input_required' | 'c
|
|
|
99
100
|
| 'orphaned'
|
|
100
101
|
/** The host window died and took the agent with it — an unclean exit, not a normal close. */
|
|
101
102
|
| 'crashed' | 'unknown';
|
|
103
|
+
/**
|
|
104
|
+
* Which rung of the recap ladder produced a row's shown {@link ActiveSession.title}
|
|
105
|
+
* (RUSH-3011), best-first:
|
|
106
|
+
* - `label` — a `/rename` or harness-set label (incl. an agent-generated
|
|
107
|
+
* title, which lands in `label`); always wins.
|
|
108
|
+
* - `last` — the last assistant line from the transcript tail; agent-derived.
|
|
109
|
+
* - `prompt` — the first-user-prompt topic; the last-resort fallback.
|
|
110
|
+
*/
|
|
111
|
+
export type RecapSource = 'label' | 'last' | 'prompt';
|
|
102
112
|
export interface ActiveSession {
|
|
103
113
|
context: ActiveContext;
|
|
104
114
|
kind: string;
|
|
@@ -115,6 +125,30 @@ export interface ActiveSession {
|
|
|
115
125
|
name?: string;
|
|
116
126
|
/** First meaningful line of the initial prompt (extracted topic). */
|
|
117
127
|
topic?: string;
|
|
128
|
+
/**
|
|
129
|
+
* The row's shown title — WHAT the session is, best-source-wins (RUSH-3011).
|
|
130
|
+
* The ladder ({@link deriveSessionRecap}): a `/rename` or harness `label` →
|
|
131
|
+
* the last agent line (`lastAgentLine`) → the first-prompt `topic`. A session
|
|
132
|
+
* whose agent did work shows an agent-derived line, not the stale first
|
|
133
|
+
* prompt. Folded on at the end of {@link getActiveSessions}; `recapSource`
|
|
134
|
+
* names which rung produced it.
|
|
135
|
+
*/
|
|
136
|
+
title?: string;
|
|
137
|
+
/** Which ladder rung produced {@link title}. */
|
|
138
|
+
recapSource?: RecapSource;
|
|
139
|
+
/**
|
|
140
|
+
* The first user turn cleaned for a "You" line — a screenshot path folds to
|
|
141
|
+
* `[image]`, a pasted `$ cmd` to the command, a `/skill` install path to
|
|
142
|
+
* `/<name>`, so path noise never shows. See {@link classifyUserPrompt}.
|
|
143
|
+
*/
|
|
144
|
+
userPromptClean?: string;
|
|
145
|
+
/** What kind of first turn {@link userPromptClean} was. */
|
|
146
|
+
userPromptKind?: UserPromptKind;
|
|
147
|
+
/**
|
|
148
|
+
* The most recent assistant line (from the transcript tail) — the free,
|
|
149
|
+
* always-current signal of what the agent last said/did. Ladder rung 3.
|
|
150
|
+
*/
|
|
151
|
+
lastAgentLine?: string;
|
|
118
152
|
/** Live preview: the latest turn (agent message or tool action), from the state engine. */
|
|
119
153
|
preview?: string;
|
|
120
154
|
/** Inferred activity: working / waiting_input / idle (from the transcript tail). */
|
|
@@ -787,6 +821,39 @@ export declare function sessionProcessIsLocal(s: Pick<ActiveSession, 'machine' |
|
|
|
787
821
|
*/
|
|
788
822
|
export declare function sessionProcessHost(s: Pick<ActiveSession, 'machine' | 'offloadedFrom'>, self: string): string | undefined;
|
|
789
823
|
export declare function foldHostLink(rows: ActiveSession[]): void;
|
|
824
|
+
/**
|
|
825
|
+
* The recap ladder (RUSH-3011): compute a row's shown {@link ActiveSession.title}
|
|
826
|
+
* + {@link RecapSource} from the best available source, plus the cleaned first
|
|
827
|
+
* prompt (`userPromptClean`/`userPromptKind`) and the `lastAgentLine`. Pure over
|
|
828
|
+
* one row; exported for tests and folded in by {@link foldRecap}.
|
|
829
|
+
*
|
|
830
|
+
* Ladder, best-first: a `/rename`/harness `label` → the last assistant line →
|
|
831
|
+
* the first-prompt topic. The `last` rung is agent-derived, so a session that
|
|
832
|
+
* produced work stops showing its stale first prompt as the title. (`topic` is
|
|
833
|
+
* the row's already-extracted first line, so image detection here is
|
|
834
|
+
* path-based; a pure-attachment turn with no first-line text stays on `prompt`.)
|
|
835
|
+
*/
|
|
836
|
+
export declare function deriveSessionRecap(row: Pick<ActiveSession, 'label' | 'topic' | 'tail'>): {
|
|
837
|
+
title?: string;
|
|
838
|
+
recapSource?: RecapSource;
|
|
839
|
+
userPromptClean?: string;
|
|
840
|
+
userPromptKind?: UserPromptKind;
|
|
841
|
+
lastAgentLine?: string;
|
|
842
|
+
};
|
|
843
|
+
/** Fold the recap ladder onto every row (see {@link deriveSessionRecap}). */
|
|
844
|
+
export declare function foldRecap(rows: ActiveSession[]): void;
|
|
845
|
+
/**
|
|
846
|
+
* True when a crash-leaked orphan is genuinely DEAD and should be reaped from the
|
|
847
|
+
* reconnectable set rather than shown as resumable forever (RUSH-3011 / issue #3b).
|
|
848
|
+
*
|
|
849
|
+
* The gate is `abandoned` (no transcript write in {@link ABANDONED_STALE_MS}) AND
|
|
850
|
+
* a dead pid — exactly "past the stale threshold whose pid is gone". A live pid
|
|
851
|
+
* (an idle-but-unfinished session, the highest-risk state) is NEVER reaped, and
|
|
852
|
+
* neither is a recently-`closed`/`crashed` session that just exited (still
|
|
853
|
+
* resumable). `pidAlive` absent (a cloud row or an older peer that can't prove
|
|
854
|
+
* death) also stays un-reaped — reaping is fail-safe, never a guess.
|
|
855
|
+
*/
|
|
856
|
+
export declare function isReapableOrphan(row: Pick<ActiveSession, 'status' | 'pidAlive'>): boolean;
|
|
790
857
|
/**
|
|
791
858
|
* Resolve each teams row's `orchestratorLabel` from the orchestrator's own row,
|
|
792
859
|
* when that orchestrator session is itself in the active set (it usually is — the
|
|
@@ -30,7 +30,7 @@ import { loadHookSessionIndex, resolveHookSessionRecord, readStateSessionRecord
|
|
|
30
30
|
import { buildClaudeLabelMap, getAgentSessionDirs } from './discover.js';
|
|
31
31
|
import { buildRunNameMap } from './run-names.js';
|
|
32
32
|
import { latestSessionFileForCwd, findSessionsByShortIds, findSessionMachinesByIds, getSessionById } from './db.js';
|
|
33
|
-
import { extractSessionTopic } from './prompt.js';
|
|
33
|
+
import { extractSessionTopic, classifyUserPrompt } from './prompt.js';
|
|
34
34
|
import { readSessionTailWithRaw } from './tail.js';
|
|
35
35
|
import { parseSession } from './parse.js';
|
|
36
36
|
import { computeTokPerSec } from './throughput.js';
|
|
@@ -1831,6 +1831,9 @@ export async function getActiveSessions(opts = {}) {
|
|
|
1831
1831
|
// the EXECUTION host rather than the dispatcher (RUSH-2479).
|
|
1832
1832
|
foldExecutionMachine(merged, recordedMachineLookup(merged), machineId());
|
|
1833
1833
|
annotateOrchestratorLabels(merged);
|
|
1834
|
+
// Last: derive the shown title (recap ladder) from label/tail/topic now that
|
|
1835
|
+
// every row's live tail + label are resolved (RUSH-3011).
|
|
1836
|
+
foldRecap(merged);
|
|
1834
1837
|
return merged;
|
|
1835
1838
|
}
|
|
1836
1839
|
/**
|
|
@@ -2074,6 +2077,71 @@ export function foldHostLink(rows) {
|
|
|
2074
2077
|
}
|
|
2075
2078
|
}
|
|
2076
2079
|
}
|
|
2080
|
+
/** One display line, trimmed and capped, or undefined when empty. */
|
|
2081
|
+
function recapLine(s, max = 120) {
|
|
2082
|
+
const t = s?.replace(/\s+/g, ' ').trim();
|
|
2083
|
+
if (!t)
|
|
2084
|
+
return undefined;
|
|
2085
|
+
return t.length > max ? t.slice(0, max - 1).trimEnd() + '…' : t;
|
|
2086
|
+
}
|
|
2087
|
+
/**
|
|
2088
|
+
* The recap ladder (RUSH-3011): compute a row's shown {@link ActiveSession.title}
|
|
2089
|
+
* + {@link RecapSource} from the best available source, plus the cleaned first
|
|
2090
|
+
* prompt (`userPromptClean`/`userPromptKind`) and the `lastAgentLine`. Pure over
|
|
2091
|
+
* one row; exported for tests and folded in by {@link foldRecap}.
|
|
2092
|
+
*
|
|
2093
|
+
* Ladder, best-first: a `/rename`/harness `label` → the last assistant line →
|
|
2094
|
+
* the first-prompt topic. The `last` rung is agent-derived, so a session that
|
|
2095
|
+
* produced work stops showing its stale first prompt as the title. (`topic` is
|
|
2096
|
+
* the row's already-extracted first line, so image detection here is
|
|
2097
|
+
* path-based; a pure-attachment turn with no first-line text stays on `prompt`.)
|
|
2098
|
+
*/
|
|
2099
|
+
export function deriveSessionRecap(row) {
|
|
2100
|
+
const lastAgentLine = recapLine(row.tail?.length ? row.tail[row.tail.length - 1] : undefined);
|
|
2101
|
+
const { clean: userPromptClean, kind: userPromptKind } = classifyUserPrompt(row.topic ?? '');
|
|
2102
|
+
const label = recapLine(row.label);
|
|
2103
|
+
const prompt = recapLine(userPromptClean || row.topic);
|
|
2104
|
+
let title;
|
|
2105
|
+
let recapSource;
|
|
2106
|
+
if (label) {
|
|
2107
|
+
title = label;
|
|
2108
|
+
recapSource = 'label';
|
|
2109
|
+
}
|
|
2110
|
+
else if (lastAgentLine) {
|
|
2111
|
+
title = lastAgentLine;
|
|
2112
|
+
recapSource = 'last';
|
|
2113
|
+
}
|
|
2114
|
+
else if (prompt) {
|
|
2115
|
+
title = prompt;
|
|
2116
|
+
recapSource = 'prompt';
|
|
2117
|
+
}
|
|
2118
|
+
return { title, recapSource, userPromptClean: recapLine(userPromptClean), userPromptKind, lastAgentLine };
|
|
2119
|
+
}
|
|
2120
|
+
/** Fold the recap ladder onto every row (see {@link deriveSessionRecap}). */
|
|
2121
|
+
export function foldRecap(rows) {
|
|
2122
|
+
for (const s of rows) {
|
|
2123
|
+
const recap = deriveSessionRecap(s);
|
|
2124
|
+
s.title = recap.title;
|
|
2125
|
+
s.recapSource = recap.recapSource;
|
|
2126
|
+
s.userPromptClean = recap.userPromptClean;
|
|
2127
|
+
s.userPromptKind = recap.userPromptKind;
|
|
2128
|
+
s.lastAgentLine = recap.lastAgentLine;
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
2132
|
+
* True when a crash-leaked orphan is genuinely DEAD and should be reaped from the
|
|
2133
|
+
* reconnectable set rather than shown as resumable forever (RUSH-3011 / issue #3b).
|
|
2134
|
+
*
|
|
2135
|
+
* The gate is `abandoned` (no transcript write in {@link ABANDONED_STALE_MS}) AND
|
|
2136
|
+
* a dead pid — exactly "past the stale threshold whose pid is gone". A live pid
|
|
2137
|
+
* (an idle-but-unfinished session, the highest-risk state) is NEVER reaped, and
|
|
2138
|
+
* neither is a recently-`closed`/`crashed` session that just exited (still
|
|
2139
|
+
* resumable). `pidAlive` absent (a cloud row or an older peer that can't prove
|
|
2140
|
+
* death) also stays un-reaped — reaping is fail-safe, never a guess.
|
|
2141
|
+
*/
|
|
2142
|
+
export function isReapableOrphan(row) {
|
|
2143
|
+
return row.status === 'abandoned' && row.pidAlive === false;
|
|
2144
|
+
}
|
|
2077
2145
|
/**
|
|
2078
2146
|
* Resolve each teams row's `orchestratorLabel` from the orchestrator's own row,
|
|
2079
2147
|
* when that orchestrator session is itself in the active set (it usually is — the
|
package/dist/lib/session/db.js
CHANGED
|
@@ -2112,8 +2112,10 @@ export function syncLabels(labelMap) {
|
|
|
2112
2112
|
.prepare(`SELECT id, label FROM sessions WHERE id IN (${placeholders})`)
|
|
2113
2113
|
.all(...chunk);
|
|
2114
2114
|
for (const row of rows) {
|
|
2115
|
-
const live = labelMap.get(row.id)
|
|
2116
|
-
|
|
2115
|
+
const live = labelMap.get(row.id)?.trim() || null;
|
|
2116
|
+
// A missing/empty live label means "no refinement yet", not "erase the
|
|
2117
|
+
// generated title or launch handle already stored for this session".
|
|
2118
|
+
if (live && live !== (row.label ?? '')) {
|
|
2117
2119
|
updates.push({ id: row.id, label: live });
|
|
2118
2120
|
}
|
|
2119
2121
|
}
|
|
@@ -78,6 +78,8 @@ interface ClaudeSessionScan {
|
|
|
78
78
|
version?: string;
|
|
79
79
|
model?: string;
|
|
80
80
|
topic?: string;
|
|
81
|
+
/** Harness-owned session name (`/rename` or Claude's generated `ai-title`). */
|
|
82
|
+
label?: string;
|
|
81
83
|
messageCount: number;
|
|
82
84
|
tokenCount?: number;
|
|
83
85
|
/** Real generated (output) tokens, excluding cache-read/-write context. */
|
|
@@ -1215,7 +1215,9 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, label) {
|
|
|
1215
1215
|
accountKey: acct.key,
|
|
1216
1216
|
accountOrg: acct.orgName ?? undefined,
|
|
1217
1217
|
topic: scan.topic,
|
|
1218
|
-
|
|
1218
|
+
// The live sessions metadata file contains only user `/rename` values;
|
|
1219
|
+
// prefer it when present, then fall back to the title parsed from JSONL.
|
|
1220
|
+
label: label || scan.label,
|
|
1219
1221
|
messageCount: scan.messageCount,
|
|
1220
1222
|
toolCallCount: scan.toolCallCount,
|
|
1221
1223
|
tokenCount: scan.tokenCount,
|
|
@@ -1253,7 +1255,9 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, label) {
|
|
|
1253
1255
|
accountKey: acct.key,
|
|
1254
1256
|
accountOrg: acct.orgName ?? undefined,
|
|
1255
1257
|
model: scan.model,
|
|
1256
|
-
|
|
1258
|
+
// Keep parsed ai-title/custom-title values even when the transcript has
|
|
1259
|
+
// no timestamp and therefore takes this stat-backed fallback path.
|
|
1260
|
+
label: label || scan.label,
|
|
1257
1261
|
messageCount: scan.messageCount,
|
|
1258
1262
|
toolCallCount: scan.toolCallCount,
|
|
1259
1263
|
tokenCount: scan.tokenCount,
|
|
@@ -3247,9 +3251,10 @@ export function finalizeClaudeScan(state) {
|
|
|
3247
3251
|
const durationMs = state.firstTsMs !== undefined && state.lastTsMs !== undefined && state.lastTsMs > state.firstTsMs
|
|
3248
3252
|
? state.lastTsMs - state.firstTsMs
|
|
3249
3253
|
: undefined;
|
|
3250
|
-
//
|
|
3251
|
-
//
|
|
3252
|
-
|
|
3254
|
+
// A topic is the first meaningful prompt. Harness-owned names travel in the
|
|
3255
|
+
// separate label field so consumers can replace an early topic once Claude's
|
|
3256
|
+
// generated title (or a later `/rename`) arrives.
|
|
3257
|
+
const label = state.customTitle || state.aiTitle;
|
|
3253
3258
|
const worktree = detectWorktree(state.cwd, state.gitBranch);
|
|
3254
3259
|
const ticket = detectTicket(state.userTexts.join('\n') || undefined, state.gitBranch);
|
|
3255
3260
|
return {
|
|
@@ -3258,7 +3263,8 @@ export function finalizeClaudeScan(state) {
|
|
|
3258
3263
|
gitBranch: state.gitBranch,
|
|
3259
3264
|
version: state.version,
|
|
3260
3265
|
model: state.model,
|
|
3261
|
-
topic:
|
|
3266
|
+
topic: state.topic,
|
|
3267
|
+
label,
|
|
3262
3268
|
entrypoint: state.entrypoint,
|
|
3263
3269
|
messageCount: state.messageCount,
|
|
3264
3270
|
toolCallCount: state.toolCallCount,
|
|
@@ -34,5 +34,40 @@ export declare function extractSlashCommandFromToolInput(toolInput: Record<strin
|
|
|
34
34
|
export declare const HEADLESS_PLAN_MODE_PREFIX = "You are running in HEADLESS PLAN MODE.";
|
|
35
35
|
/** Strip framework noise from a raw session prompt, returning only meaningful user text. */
|
|
36
36
|
export declare function cleanSessionPrompt(raw: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* How a session's first user turn reads once its framework noise is stripped —
|
|
39
|
+
* so a UI row can show `[image]` / `$ cmd` / `/skill` instead of a screenshot
|
|
40
|
+
* path, a pasted shell line, or a skill's install directory.
|
|
41
|
+
*/
|
|
42
|
+
export type UserPromptKind = 'text' | 'image' | 'command' | 'skill';
|
|
43
|
+
export interface ClassifiedPrompt {
|
|
44
|
+
/**
|
|
45
|
+
* The one-line, display-ready form of the prompt — never a bare path. NOT
|
|
46
|
+
* length-capped: the recap card shows it in full; row consumers cap it
|
|
47
|
+
* themselves (`deriveSessionRecap`).
|
|
48
|
+
*/
|
|
49
|
+
clean: string;
|
|
50
|
+
kind: UserPromptKind;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Classify a raw first user turn into a display-ready `{ clean, kind }`, so a
|
|
54
|
+
* Fleet/session row shows the user's actual intent rather than path noise:
|
|
55
|
+
*
|
|
56
|
+
* - `skill` — a `/skill` invocation's injected "Base directory for this
|
|
57
|
+
* skill: …" line collapses to `/<name>`.
|
|
58
|
+
* - `command` — a `$ `-prefixed shell paste keeps only the first command.
|
|
59
|
+
* - `image` — a screenshot/image path standing alone, or an image
|
|
60
|
+
* attachment with no real accompanying text, becomes `[image]`.
|
|
61
|
+
* - `text` — everything else: wrapper tags stripped (via
|
|
62
|
+
* {@link cleanSessionPrompt}) and any inline image path replaced
|
|
63
|
+
* with `[image]` so a mixed prompt still drops the path.
|
|
64
|
+
*
|
|
65
|
+
* Pure and cross-harness (text markers, no per-agent branch). `hasImageAttachment`
|
|
66
|
+
* lets a caller that only has an already-extracted topic line still detect an
|
|
67
|
+
* image-only turn from the row's attachment list.
|
|
68
|
+
*/
|
|
69
|
+
export declare function classifyUserPrompt(raw: string | undefined, opts?: {
|
|
70
|
+
hasImageAttachment?: boolean;
|
|
71
|
+
}): ClassifiedPrompt;
|
|
37
72
|
/** Extract a one-line topic from a raw user message, or undefined if the message is pure noise. */
|
|
38
73
|
export declare function extractSessionTopic(raw: string): string | undefined;
|
|
@@ -145,6 +145,62 @@ export function cleanSessionPrompt(raw) {
|
|
|
145
145
|
.filter(line => !NOISE_LINE_PATTERNS.some(pattern => pattern.test(line)));
|
|
146
146
|
return meaningful.join('\n').trim();
|
|
147
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* A skill invocation injects a system line naming the skill's install directory:
|
|
150
|
+
* "Base directory for this skill: /home/.../skills/<name>"
|
|
151
|
+
* which collapses to "/<name>" so a row shows the skill, not the path. Anchored
|
|
152
|
+
* to that exact injected line — a bare `.../skills/<name>` mention in ordinary
|
|
153
|
+
* prose is NOT a skill invocation and must keep its real text.
|
|
154
|
+
*/
|
|
155
|
+
const SKILL_BASEDIR_RE = /Base directory for this skill:\s*\S*[\/\\]skills[\/\\]([\w.-]+)/i;
|
|
156
|
+
/**
|
|
157
|
+
* An explicit shell-prompt paste (`$ cmd`). Only the command survives. Deliberately
|
|
158
|
+
* `$`-only: a leading `>` is a markdown blockquote far more often than a shell
|
|
159
|
+
* prompt, so matching it misread quoted prose as a command.
|
|
160
|
+
*/
|
|
161
|
+
const COMMAND_PREFIX_RE = /^\$\s+(\S.*)$/;
|
|
162
|
+
/**
|
|
163
|
+
* A filesystem path (absolute or `~`-rooted) ending in an image extension —
|
|
164
|
+
* matched even across the spaces a CleanShot filename carries. Used both to
|
|
165
|
+
* detect an image-only prompt and to drop the path from a mixed one.
|
|
166
|
+
*/
|
|
167
|
+
const IMAGE_PATH_RE = /[\/~][^\n]*?\.(?:png|jpe?g|gif|webp|heic|bmp|svg)\b/i;
|
|
168
|
+
/**
|
|
169
|
+
* Classify a raw first user turn into a display-ready `{ clean, kind }`, so a
|
|
170
|
+
* Fleet/session row shows the user's actual intent rather than path noise:
|
|
171
|
+
*
|
|
172
|
+
* - `skill` — a `/skill` invocation's injected "Base directory for this
|
|
173
|
+
* skill: …" line collapses to `/<name>`.
|
|
174
|
+
* - `command` — a `$ `-prefixed shell paste keeps only the first command.
|
|
175
|
+
* - `image` — a screenshot/image path standing alone, or an image
|
|
176
|
+
* attachment with no real accompanying text, becomes `[image]`.
|
|
177
|
+
* - `text` — everything else: wrapper tags stripped (via
|
|
178
|
+
* {@link cleanSessionPrompt}) and any inline image path replaced
|
|
179
|
+
* with `[image]` so a mixed prompt still drops the path.
|
|
180
|
+
*
|
|
181
|
+
* Pure and cross-harness (text markers, no per-agent branch). `hasImageAttachment`
|
|
182
|
+
* lets a caller that only has an already-extracted topic line still detect an
|
|
183
|
+
* image-only turn from the row's attachment list.
|
|
184
|
+
*/
|
|
185
|
+
export function classifyUserPrompt(raw, opts = {}) {
|
|
186
|
+
const text = (raw ?? '').replace(/\r/g, '').trim();
|
|
187
|
+
const skill = text.match(SKILL_BASEDIR_RE);
|
|
188
|
+
if (skill)
|
|
189
|
+
return { clean: `/${skill[1]}`, kind: 'skill' };
|
|
190
|
+
const firstLine = text.split('\n').map(line => line.trim()).find(Boolean) ?? '';
|
|
191
|
+
const cmd = firstLine.match(COMMAND_PREFIX_RE);
|
|
192
|
+
if (cmd)
|
|
193
|
+
return { clean: `$ ${cmd[1]}`.trim(), kind: 'command' };
|
|
194
|
+
const cleaned = cleanSessionPrompt(text);
|
|
195
|
+
const cleanedFirst = cleaned.split('\n').map(line => line.trim()).find(Boolean) ?? '';
|
|
196
|
+
const imgMatch = firstLine.match(IMAGE_PATH_RE);
|
|
197
|
+
const imageOnly = !!imgMatch && firstLine.replace(imgMatch[0], '').replace(/[^\w]/g, '').length <= 8;
|
|
198
|
+
if (imageOnly || (opts.hasImageAttachment && !cleanedFirst)) {
|
|
199
|
+
return { clean: '[image]', kind: 'image' };
|
|
200
|
+
}
|
|
201
|
+
const display = (cleanedFirst || firstLine).replace(IMAGE_PATH_RE, '[image]').trim();
|
|
202
|
+
return { clean: display, kind: 'text' };
|
|
203
|
+
}
|
|
148
204
|
/** Extract a one-line topic from a raw user message, or undefined if the message is pure noise. */
|
|
149
205
|
export function extractSessionTopic(raw) {
|
|
150
206
|
if (!raw.trim())
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ActiveSession } from '../active.js';
|
|
2
2
|
import { readActiveSessionsCache } from '../session-cache.js';
|
|
3
3
|
export declare const SESSION_WATCH_VERSION: 1;
|
|
4
4
|
export declare const SESSION_WATCH_HEARTBEAT_MS = 15000;
|
|
@@ -8,6 +8,7 @@ import { deviceIdentityArgs, sshTargetFor } from '../../devices/connect.js';
|
|
|
8
8
|
import { machineId, normalizeHost } from '../../machine-id.js';
|
|
9
9
|
import { SSH_OPTS, controlOpts, shellQuote } from '../../ssh-exec.js';
|
|
10
10
|
import { buildWindowsAgentsCommand, remoteShellFor } from '../../hosts/remote-cmd.js';
|
|
11
|
+
import { isReapableOrphan } from '../active.js';
|
|
11
12
|
import { activeSessionsJournalPath, activeSessionJournalIdentity, readActiveSessionsCache, } from '../session-cache.js';
|
|
12
13
|
export const SESSION_WATCH_VERSION = 1;
|
|
13
14
|
export const SESSION_WATCH_HEARTBEAT_MS = 15_000;
|
|
@@ -18,7 +19,11 @@ export function sessionWatchRowKey(scope, row) {
|
|
|
18
19
|
}
|
|
19
20
|
export function toSessionWatchRow(scope, row) {
|
|
20
21
|
const rowKey = sessionWatchRowKey(scope, row);
|
|
21
|
-
|
|
22
|
+
// A dead, days-stale crash-orphan is folded OUT of the reconnectable set — it
|
|
23
|
+
// is not resumable, so the "Needs reconnecting" list stops ballooning with
|
|
24
|
+
// leaked --device tunnel sessions (RUSH-3011 / issue #3b). A live or
|
|
25
|
+
// recently-exited session stays resumable.
|
|
26
|
+
const resumable = Boolean(row.sessionId) && !isReapableOrphan(row);
|
|
22
27
|
const viewingIn = row.viewingIn
|
|
23
28
|
? [row.viewingIn.app, row.viewingIn.tab ? `tab ${row.viewingIn.tab}` : undefined].filter(Boolean).join(' ')
|
|
24
29
|
: null;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
import { truncate } from '../format.js';
|
|
11
11
|
import { summarizeToolUse } from './parse.js';
|
|
12
|
-
import { cleanSessionPrompt, extractSessionTopic } from './prompt.js';
|
|
12
|
+
import { cleanSessionPrompt, classifyUserPrompt, extractSessionTopic } from './prompt.js';
|
|
13
13
|
import { renderMarkdown } from '../markdown.js';
|
|
14
14
|
import { redactSecrets } from '../redact.js';
|
|
15
15
|
import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } from './digest.js';
|
|
@@ -635,8 +635,12 @@ export function renderSummary(events, cwd) {
|
|
|
635
635
|
// 1. Prompt
|
|
636
636
|
if (firstUserMessage) {
|
|
637
637
|
const cleaned = cleanSessionPrompt(firstUserMessage);
|
|
638
|
-
|
|
639
|
-
|
|
638
|
+
// Show the classified prompt (a screenshot path folds to `[image]`, a
|
|
639
|
+
// pasted `$ cmd` to the command, a skill install path to `/<name>`) so the
|
|
640
|
+
// recap's first line is intent, never path noise (RUSH-3011).
|
|
641
|
+
const { clean } = classifyUserPrompt(firstUserMessage, { hasImageAttachment: attachments.length > 0 });
|
|
642
|
+
if (clean || cleaned) {
|
|
643
|
+
lines.push(chalk.bold('Prompt:') + ' ' + (clean || cleaned.split('\n')[0]));
|
|
640
644
|
const secondLine = cleaned.split('\n')[1]?.trim();
|
|
641
645
|
if (secondLine)
|
|
642
646
|
lines.push(' ' + secondLine);
|
|
@@ -27,7 +27,7 @@ import * as fs from 'fs';
|
|
|
27
27
|
import * as path from 'path';
|
|
28
28
|
import { createMemoryCache } from '../memory-cache.js';
|
|
29
29
|
import { getCacheDir } from '../state.js';
|
|
30
|
-
import { sessionProcessIsLocal } from './active.js';
|
|
30
|
+
import { backfillActiveRowsFromIndex, sessionProcessIsLocal } from './active.js';
|
|
31
31
|
/** Snapshot file under `getCacheDir()` (regenerable, gitignored). */
|
|
32
32
|
const SNAPSHOT_FILE = '.active-sessions.json';
|
|
33
33
|
/** Immutable-field memo file (keyed by sessionId + transcript mtime). */
|
|
@@ -376,6 +376,10 @@ export async function loadLocalActiveSessions(opts = {}) {
|
|
|
376
376
|
return getActiveSessions({ localOnly: true });
|
|
377
377
|
});
|
|
378
378
|
const sessions = await gather();
|
|
379
|
+
// The index owns transcript-derived labels (Claude custom-title / ai-title).
|
|
380
|
+
// Enrich before publishing so the canonical watch snapshot carries the same
|
|
381
|
+
// label as one-shot `sessions --active` output.
|
|
382
|
+
backfillActiveRowsFromIndex(sessions);
|
|
379
383
|
for (const s of sessions)
|
|
380
384
|
applyImmutableMemo(s);
|
|
381
385
|
updateImmutableMemos(sessions, now);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { SessionMeta } from './types.js';
|
|
2
|
+
import type { SessionTrajectory, TrajectoryStep } from './trajectory.js';
|
|
3
|
+
/** Where two sessions' tool sequences first stop lining up. */
|
|
4
|
+
export interface TrajectoryDivergence {
|
|
5
|
+
/** Ordinal of the last step both sessions still agree on (0 if none). */
|
|
6
|
+
afterOrdinalA: number;
|
|
7
|
+
afterOrdinalB: number;
|
|
8
|
+
/** ms into each session's own timeline where the divergence begins. */
|
|
9
|
+
startMsA: number;
|
|
10
|
+
startMsB: number;
|
|
11
|
+
/** One-line description of what differs — the next tool step on each side. */
|
|
12
|
+
detail: string;
|
|
13
|
+
}
|
|
14
|
+
/** Aggregate numbers for one side of a compare — the summary table row. */
|
|
15
|
+
export interface TrajectorySummary {
|
|
16
|
+
session: SessionMeta;
|
|
17
|
+
toolCount: number;
|
|
18
|
+
errorCount: number;
|
|
19
|
+
spanMs: number;
|
|
20
|
+
outputTokens: number;
|
|
21
|
+
}
|
|
22
|
+
/** The result of diffing two trajectories. */
|
|
23
|
+
export interface TrajectoryComparison {
|
|
24
|
+
a: SessionTrajectory;
|
|
25
|
+
b: SessionTrajectory;
|
|
26
|
+
/** Undefined only when the tool sequences are identical (or one/both are empty and equal). */
|
|
27
|
+
divergence?: TrajectoryDivergence;
|
|
28
|
+
/** Tool steps present in `b` with no counterpart in `a`, in `b`'s order. */
|
|
29
|
+
added: TrajectoryStep[];
|
|
30
|
+
/** Tool steps present in `a` with no counterpart in `b`, in `a`'s order. */
|
|
31
|
+
removed: TrajectoryStep[];
|
|
32
|
+
summaryA: TrajectorySummary;
|
|
33
|
+
summaryB: TrajectorySummary;
|
|
34
|
+
/** Tool steps dropped from the diff computation by the `maxDiffSteps` cap, per side. */
|
|
35
|
+
truncatedA: number;
|
|
36
|
+
truncatedB: number;
|
|
37
|
+
}
|
|
38
|
+
export interface DiffTrajectoriesOptions {
|
|
39
|
+
/** Cap on tool steps considered per side (the LCS table is O(n*m)). Default 1000. */
|
|
40
|
+
maxDiffSteps?: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Diff two trajectories: align their tool-step sequences, find the first
|
|
44
|
+
* divergence, and collect the steps each session ran that the other never
|
|
45
|
+
* did. Never mutates either input.
|
|
46
|
+
*/
|
|
47
|
+
export declare function diffTrajectories(a: SessionTrajectory, b: SessionTrajectory, options?: DiffTrajectoriesOptions): TrajectoryComparison;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff two {@link SessionTrajectory} models for `agents sessions trace <a> <b>` —
|
|
3
|
+
* the second selector turns the single-session trajectory into a **compare**.
|
|
4
|
+
*
|
|
5
|
+
* Aligns the two sessions' TOOL steps (thinking steps carry no comparable
|
|
6
|
+
* identity) with the `diff` package's `diffArrays` (already a dependency,
|
|
7
|
+
* `lib/diff-text.ts`), comparator-keyed on tool name, in order — cheap,
|
|
8
|
+
* deterministic, and matches how a human reads "where did these two runs
|
|
9
|
+
* diverge": the first point their tool sequence stops lining up.
|
|
10
|
+
*
|
|
11
|
+
* Pure and framework-free, exactly like `trajectory.ts`: takes two already-built
|
|
12
|
+
* `SessionTrajectory` models (never re-parses a transcript) and returns a plain
|
|
13
|
+
* diff object the HTML/text/JSON renderers project.
|
|
14
|
+
*/
|
|
15
|
+
import { diffArrays } from 'diff';
|
|
16
|
+
const DEFAULT_MAX_DIFF_STEPS = 1000;
|
|
17
|
+
/**
|
|
18
|
+
* Turn `diffArrays`' run-length change objects into a per-step edit script.
|
|
19
|
+
*
|
|
20
|
+
* `diffArrays(a, b, { comparator })` (Myers' diff under the hood) returns
|
|
21
|
+
* consecutive-run chunks, not individual elements, and — per its own docs —
|
|
22
|
+
* an unchanged chunk's `value` is drawn from `b`, not `a`. Neither shape is
|
|
23
|
+
* what a divergence marker needs (a stepA/stepB PAIR per matched step, with
|
|
24
|
+
* both sides' own ordinals/timestamps), so this walks two cursors over the
|
|
25
|
+
* original arrays in lockstep with the run lengths `diffArrays` reports,
|
|
26
|
+
* recovering the exact `a[i]`/`b[j]` pairing for every `same` step.
|
|
27
|
+
*/
|
|
28
|
+
function computeEditScript(a, b) {
|
|
29
|
+
const changes = diffArrays(a, b, { comparator: (x, y) => x.tool === y.tool });
|
|
30
|
+
const ops = [];
|
|
31
|
+
let i = 0;
|
|
32
|
+
let j = 0;
|
|
33
|
+
for (const change of changes) {
|
|
34
|
+
const count = change.count ?? change.value.length;
|
|
35
|
+
if (change.removed) {
|
|
36
|
+
for (let k = 0; k < count; k++)
|
|
37
|
+
ops.push({ type: 'remove', stepA: a[i++] });
|
|
38
|
+
}
|
|
39
|
+
else if (change.added) {
|
|
40
|
+
for (let k = 0; k < count; k++)
|
|
41
|
+
ops.push({ type: 'add', stepB: b[j++] });
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
for (let k = 0; k < count; k++)
|
|
45
|
+
ops.push({ type: 'same', stepA: a[i++], stepB: b[j++] });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return ops;
|
|
49
|
+
}
|
|
50
|
+
function describeDivergence(op) {
|
|
51
|
+
if (op.type === 'remove')
|
|
52
|
+
return `${op.stepA.tool ?? op.stepA.kind} — only in the first session`;
|
|
53
|
+
if (op.type === 'add')
|
|
54
|
+
return `${op.stepB.tool ?? op.stepB.kind} — only in the second session`;
|
|
55
|
+
return '';
|
|
56
|
+
}
|
|
57
|
+
function summarize(t) {
|
|
58
|
+
return {
|
|
59
|
+
session: t.session,
|
|
60
|
+
toolCount: t.stats.toolCount,
|
|
61
|
+
errorCount: t.errorCount,
|
|
62
|
+
spanMs: t.spanMs,
|
|
63
|
+
outputTokens: t.stats.outputTokens,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Diff two trajectories: align their tool-step sequences, find the first
|
|
68
|
+
* divergence, and collect the steps each session ran that the other never
|
|
69
|
+
* did. Never mutates either input.
|
|
70
|
+
*/
|
|
71
|
+
export function diffTrajectories(a, b, options = {}) {
|
|
72
|
+
const maxDiffSteps = options.maxDiffSteps ?? DEFAULT_MAX_DIFF_STEPS;
|
|
73
|
+
const allToolStepsA = a.steps.filter((s) => s.kind === 'tool');
|
|
74
|
+
const allToolStepsB = b.steps.filter((s) => s.kind === 'tool');
|
|
75
|
+
const toolStepsA = allToolStepsA.slice(0, maxDiffSteps);
|
|
76
|
+
const toolStepsB = allToolStepsB.slice(0, maxDiffSteps);
|
|
77
|
+
const truncatedA = allToolStepsA.length - toolStepsA.length;
|
|
78
|
+
const truncatedB = allToolStepsB.length - toolStepsB.length;
|
|
79
|
+
const ops = computeEditScript(toolStepsA, toolStepsB);
|
|
80
|
+
const added = [];
|
|
81
|
+
const removed = [];
|
|
82
|
+
let divergence;
|
|
83
|
+
let lastSameA;
|
|
84
|
+
let lastSameB;
|
|
85
|
+
for (const op of ops) {
|
|
86
|
+
if (op.type === 'same') {
|
|
87
|
+
lastSameA = op.stepA;
|
|
88
|
+
lastSameB = op.stepB;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (op.type === 'remove')
|
|
92
|
+
removed.push(op.stepA);
|
|
93
|
+
else
|
|
94
|
+
added.push(op.stepB);
|
|
95
|
+
if (!divergence) {
|
|
96
|
+
divergence = {
|
|
97
|
+
afterOrdinalA: lastSameA?.ordinal ?? 0,
|
|
98
|
+
afterOrdinalB: lastSameB?.ordinal ?? 0,
|
|
99
|
+
startMsA: op.type === 'remove' ? op.stepA.startMs : (lastSameA?.startMs ?? 0),
|
|
100
|
+
startMsB: op.type === 'add' ? op.stepB.startMs : (lastSameB?.startMs ?? 0),
|
|
101
|
+
detail: describeDivergence(op),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
a,
|
|
107
|
+
b,
|
|
108
|
+
divergence,
|
|
109
|
+
added,
|
|
110
|
+
removed,
|
|
111
|
+
summaryA: summarize(a),
|
|
112
|
+
summaryB: summarize(b),
|
|
113
|
+
truncatedA,
|
|
114
|
+
truncatedB,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SessionTrajectory } from './trajectory.js';
|
|
2
|
+
import type { TrajectoryComparison } from './trajectory-compare.js';
|
|
3
|
+
import type { SessionLineage } from './trajectory-lineage.js';
|
|
4
|
+
/** Render one session's trajectory as a self-contained HTML page. */
|
|
5
|
+
export declare function renderTrajectoryHtml(model: SessionTrajectory): string;
|
|
6
|
+
/** Render a two-session {@link TrajectoryComparison} as a self-contained HTML page. */
|
|
7
|
+
export declare function renderTrajectoryCompareHtml(cmp: TrajectoryComparison): string;
|
|
8
|
+
/**
|
|
9
|
+
* Render a {@link SessionLineage} as ONE self-contained HTML page: the inline-SVG
|
|
10
|
+
* delegation graph (orchestrator on top, the sessions it spawned below, edges
|
|
11
|
+
* auto-discovered from the team records) plus a per-node summary card revealed
|
|
12
|
+
* by clicking a node. Same shell, redaction, and no-CDN rule as the other two
|
|
13
|
+
* layouts; local paths (`filePath`/`cwd`) are deliberately never rendered.
|
|
14
|
+
*/
|
|
15
|
+
export declare function renderLineageHtml(lineage: SessionLineage): string;
|