@phnx-labs/agents-cli 1.22.45 → 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 +303 -0
- package/LICENSE +102 -182
- package/README.md +54 -17
- package/dist/cli/command-registry.d.ts +2 -1
- package/dist/cli/command-registry.js +6 -2
- package/dist/commands/accounts.js +56 -12
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +229 -0
- 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 +19 -32
- 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 +92 -6
- package/dist/lib/accounting/usage.js +329 -75
- 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 +20 -7
- 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/cli-resources.js +17 -15
- 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/harness-inventory.js +20 -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 +25 -3
- package/dist/lib/exec.js +75 -22
- 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/identity/client.d.ts +53 -0
- package/dist/lib/identity/client.js +106 -0
- package/dist/lib/identity/index.d.ts +115 -0
- package/dist/lib/identity/index.js +82 -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/probe.d.ts +8 -0
- package/dist/lib/probe.js +105 -0
- 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 +5 -2
- package/dist/lib/startup/command-registry.js +8 -5
- 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/view-types.d.ts +2 -2
- 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
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawn helpers for CAPABILITY PROBES — short-lived invocations of third-party
|
|
3
|
+
* binaries (`copilot --version`, a manifest's `check:` command) whose only job
|
|
4
|
+
* is an exit status or a line of stdout.
|
|
5
|
+
*
|
|
6
|
+
* A probed binary may fork children of its own: the copilot npm wrapper forks
|
|
7
|
+
* a platform-binary downloader into `~/Library/Caches/copilot` on first run
|
|
8
|
+
* under a fresh HOME. Node's `timeout:` option kills only the DIRECT child, so
|
|
9
|
+
* such grandchildren outlive the probe and keep writing — under a test's temp
|
|
10
|
+
* HOME that race is the ENOTEMPTY teardown class (RUSH-3028; residual after
|
|
11
|
+
* RUSH-3021 gated the daemon autostart). Every probe here therefore runs in
|
|
12
|
+
* its OWN process group (`detached`), and the whole group is reaped once the
|
|
13
|
+
* probe settles, so nothing a probe spawned can outlive it.
|
|
14
|
+
*
|
|
15
|
+
* Group semantics are POSIX only: on win32 `detached` means a new console and
|
|
16
|
+
* negative-pid group kills are unsupported, so there the DIRECT child is
|
|
17
|
+
* killed on settle instead — the same guarantee `execFileAsync`'s `timeout:`
|
|
18
|
+
* gave (the grandchild leak class is a darwin/linux temp-HOME teardown race).
|
|
19
|
+
*
|
|
20
|
+
* The parent dying mid-probe is covered too: a detached probe leaves the
|
|
21
|
+
* terminal's foreground group, so a Ctrl-C that hard-exits the CLI
|
|
22
|
+
* (`process.exit(130)` in index.ts) would strand it. Live probe groups are
|
|
23
|
+
* tracked in LIVE_GROUPS and a `process.on('exit')` hook — which runs on
|
|
24
|
+
* every `process.exit` path, including that SIGINT handler — reaps them
|
|
25
|
+
* synchronously.
|
|
26
|
+
*/
|
|
27
|
+
import { spawn } from 'child_process';
|
|
28
|
+
const GROUP_REAP = process.platform !== 'win32';
|
|
29
|
+
const LIVE_GROUPS = new Set();
|
|
30
|
+
let exitHookInstalled = false;
|
|
31
|
+
function ensureExitHook() {
|
|
32
|
+
if (exitHookInstalled)
|
|
33
|
+
return;
|
|
34
|
+
exitHookInstalled = true;
|
|
35
|
+
process.on('exit', () => {
|
|
36
|
+
for (const pid of LIVE_GROUPS) {
|
|
37
|
+
try {
|
|
38
|
+
process.kill(-pid, 'SIGKILL');
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
/* group already fully exited */
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function reapGroup(pid) {
|
|
47
|
+
if (!GROUP_REAP || !pid)
|
|
48
|
+
return;
|
|
49
|
+
LIVE_GROUPS.delete(pid);
|
|
50
|
+
try {
|
|
51
|
+
process.kill(-pid, 'SIGKILL');
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
/* group already fully exited */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Async probe capturing stdout. Rejects on spawn error, non-zero exit, or
|
|
59
|
+
* timeout — matching the `execFileAsync` contract the version probe had — and
|
|
60
|
+
* reaps the probe's whole process group on every settle path.
|
|
61
|
+
*/
|
|
62
|
+
export function probeCapture(cmd, args, timeoutMs) {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const child = spawn(cmd, args, {
|
|
65
|
+
detached: GROUP_REAP,
|
|
66
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
67
|
+
windowsHide: true,
|
|
68
|
+
});
|
|
69
|
+
if (GROUP_REAP && child.pid) {
|
|
70
|
+
LIVE_GROUPS.add(child.pid);
|
|
71
|
+
ensureExitHook();
|
|
72
|
+
}
|
|
73
|
+
let out = '';
|
|
74
|
+
let settled = false;
|
|
75
|
+
const settle = (err) => {
|
|
76
|
+
if (settled)
|
|
77
|
+
return;
|
|
78
|
+
settled = true;
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
reapGroup(child.pid);
|
|
81
|
+
// win32 has no group to reap: kill the direct child so a timed-out
|
|
82
|
+
// probe still dies, matching execFile's `timeout:` behavior. No-op
|
|
83
|
+
// after a clean exit.
|
|
84
|
+
if (!GROUP_REAP)
|
|
85
|
+
child.kill('SIGKILL');
|
|
86
|
+
if (err)
|
|
87
|
+
reject(err);
|
|
88
|
+
else
|
|
89
|
+
resolve({ stdout: out });
|
|
90
|
+
};
|
|
91
|
+
const timer = setTimeout(() => settle(new Error(`probe timed out after ${timeoutMs}ms: ${cmd} ${args.join(' ')}`)), timeoutMs);
|
|
92
|
+
child.stdout?.setEncoding('utf8');
|
|
93
|
+
child.stdout?.on('data', (d) => {
|
|
94
|
+
out += d;
|
|
95
|
+
});
|
|
96
|
+
child.on('error', (e) => settle(e));
|
|
97
|
+
// 'exit', not 'close': a forked grandchild inherits the stdout pipe, and
|
|
98
|
+
// 'close' waits for EVERY holder of that pipe to exit — exactly the
|
|
99
|
+
// process this helper exists to reap. Settle when the probed binary
|
|
100
|
+
// itself exits; one tick's grace lets its final stdout chunks land.
|
|
101
|
+
child.on('exit', (code) => {
|
|
102
|
+
setImmediate(() => settle(code === 0 ? null : new Error(`probe exited ${code}: ${cmd} ${args.join(' ')}`)));
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -15,7 +15,7 @@ export interface RefreshLeaseOptions<T> {
|
|
|
15
15
|
* Serialize refresh work across every agents-cli process on this device.
|
|
16
16
|
*
|
|
17
17
|
* In-process promise maps only protect one Node process. Factory, the daemon,
|
|
18
|
-
*
|
|
18
|
+
* and `agents view` are separate processes, so they need an
|
|
19
19
|
* OS-visible lease. The result is re-read after lock acquisition: a waiter
|
|
20
20
|
* consumes the winner's publication and never calls the provider a second
|
|
21
21
|
* time. `proper-lockfile` supplies heartbeat + stale-owner recovery while an
|
|
@@ -18,7 +18,7 @@ function lockTarget(scope, key) {
|
|
|
18
18
|
* Serialize refresh work across every agents-cli process on this device.
|
|
19
19
|
*
|
|
20
20
|
* In-process promise maps only protect one Node process. Factory, the daemon,
|
|
21
|
-
*
|
|
21
|
+
* and `agents view` are separate processes, so they need an
|
|
22
22
|
* OS-visible lease. The result is re-read after lock acquisition: a waiter
|
|
23
23
|
* consumes the winner's publication and never calls the provider a second
|
|
24
24
|
* time. `proper-lockfile` supplies heartbeat + stale-owner recovery while an
|
|
Binary file
|
|
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;
|