@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
|
@@ -8,14 +8,7 @@ import { type SessionProvenance } from './provenance.js';
|
|
|
8
8
|
import { type DeviceRegistry } from '../devices/registry.js';
|
|
9
9
|
import { type Presence } from './detached.js';
|
|
10
10
|
import { type HostLink } from './host-link.js';
|
|
11
|
-
/**
|
|
12
|
-
* The owner (actor id) to show for a session in `--active`. Prefers the actor
|
|
13
|
-
* recorded on the live-attribution source (the pid registry / teammate record),
|
|
14
|
-
* but falls back to the durable per-session actor sidecar — written at spawn and,
|
|
15
|
-
* unlike the pid entry, NOT overwritten by the SessionStart hook's own by-pid
|
|
16
|
-
* write. Without this fallback a real `agents run` shows no owner whenever the
|
|
17
|
-
* hook's actor-less entry wins the by-pid file (RUSH-2018 fix).
|
|
18
|
-
*/
|
|
11
|
+
/** Prefer live actor attribution; the durable sidecar survives actor-less hook rewrites. */
|
|
19
12
|
export declare function resolveOwner(pidActor: string | null | undefined, sessionId: string | undefined): string | undefined;
|
|
20
13
|
/**
|
|
21
14
|
* Per-PID `lsof` probes run bounded and staggered rather than as one parallel
|
|
@@ -62,7 +55,7 @@ export declare function attributedSetLostPids(prev: Set<number>, next: Set<numbe
|
|
|
62
55
|
export declare function filterCachedUnattributed(sessions: ActiveSession[], attributed: Set<number>, alive: (pid: number, startedAtMs?: number) => boolean): ActiveSession[];
|
|
63
56
|
type ActiveContext = 'terminal' | 'teams' | 'cloud' | 'headless';
|
|
64
57
|
/** The SessionMeta fields the live-row backfill reads — the enrichment a running process cannot report. */
|
|
65
|
-
export type BackfillMeta = Pick<SessionMeta, 'version' | 'timestamp' | 'label' | 'ticketId' | 'prUrl' | 'prNumber' | 'origin' | 'routineName' | 'harness'>;
|
|
58
|
+
export type BackfillMeta = Pick<SessionMeta, 'version' | 'account' | 'timestamp' | 'label' | 'ticketId' | 'prUrl' | 'prNumber' | 'origin' | 'routineName' | 'harness'>;
|
|
66
59
|
export declare function backfillActiveRowsFromMeta(sessions: ActiveSession[], metaById: Map<string, BackfillMeta>): void;
|
|
67
60
|
export declare function backfillActiveRowsFromIndex(sessions: ActiveSession[]): void;
|
|
68
61
|
export declare function isRunningLiveSession(s: ActiveSession): boolean;
|
|
@@ -208,6 +201,18 @@ export interface ActiveSession {
|
|
|
208
201
|
* id (RUSH-2205), never asserted by a source.
|
|
209
202
|
*/
|
|
210
203
|
version?: string;
|
|
204
|
+
/**
|
|
205
|
+
* Email of the account that produced the session (display-only). Like
|
|
206
|
+
* {@link version}, a running process does not report which account a
|
|
207
|
+
* `--strategy balanced` launch selected, so it is backfilled at render time
|
|
208
|
+
* from the indexed {@link SessionMeta} by session id (PHNX-3184). This is what
|
|
209
|
+
* the AGI EXT status bar renders as the session's account — it reads it off the
|
|
210
|
+
* `sessions watch --json` row instead of spawning a per-tab `agents sessions
|
|
211
|
+
* <id> --device <host> --json` (the 2026-08-25 CPU incident, agi-cli#3019).
|
|
212
|
+
* Never group on this — two orgs can share one email; group on the index's
|
|
213
|
+
* `accountKey`.
|
|
214
|
+
*/
|
|
215
|
+
account?: string;
|
|
211
216
|
/**
|
|
212
217
|
* Last-activity epoch — the transcript's last write (mtime). Distinct from
|
|
213
218
|
* {@link startedAtMs} (session START): a session begun 3h ago but last touched
|
|
@@ -830,18 +835,7 @@ export declare function sessionProcessIsLocal(s: Pick<ActiveSession, 'machine' |
|
|
|
830
835
|
*/
|
|
831
836
|
export declare function sessionProcessHost(s: Pick<ActiveSession, 'machine' | 'offloadedFrom'>, self: string): string | undefined;
|
|
832
837
|
export declare function foldHostLink(rows: ActiveSession[]): void;
|
|
833
|
-
/**
|
|
834
|
-
* The recap ladder (RUSH-3011): compute a row's shown {@link ActiveSession.title}
|
|
835
|
-
* + {@link RecapSource} from the best available source, plus the cleaned first
|
|
836
|
-
* prompt (`userPromptClean`/`userPromptKind`) and the `lastAgentLine`. Pure over
|
|
837
|
-
* one row; exported for tests and folded in by {@link foldRecap}.
|
|
838
|
-
*
|
|
839
|
-
* Ladder, best-first: a `/rename`/harness `label` → the last assistant line →
|
|
840
|
-
* the first-prompt topic. The `last` rung is agent-derived, so a session that
|
|
841
|
-
* produced work stops showing its stale first prompt as the title. (`topic` is
|
|
842
|
-
* the row's already-extracted first line, so image detection here is
|
|
843
|
-
* path-based; a pure-attachment turn with no first-line text stays on `prompt`.)
|
|
844
|
-
*/
|
|
838
|
+
/** Labels win, then the last assistant line, then the first-prompt topic. */
|
|
845
839
|
export declare function deriveSessionRecap(row: Pick<ActiveSession, 'label' | 'topic' | 'tail'>): {
|
|
846
840
|
title?: string;
|
|
847
841
|
recapSource?: RecapSource;
|
|
@@ -851,17 +845,7 @@ export declare function deriveSessionRecap(row: Pick<ActiveSession, 'label' | 't
|
|
|
851
845
|
};
|
|
852
846
|
/** Fold the recap ladder onto every row (see {@link deriveSessionRecap}). */
|
|
853
847
|
export declare function foldRecap(rows: ActiveSession[]): void;
|
|
854
|
-
/**
|
|
855
|
-
* True when a crash-leaked orphan is genuinely DEAD and should be reaped from the
|
|
856
|
-
* reconnectable set rather than shown as resumable forever (RUSH-3011 / issue #3b).
|
|
857
|
-
*
|
|
858
|
-
* The gate is `abandoned` (no transcript write in {@link ABANDONED_STALE_MS}) AND
|
|
859
|
-
* a dead pid — exactly "past the stale threshold whose pid is gone". A live pid
|
|
860
|
-
* (an idle-but-unfinished session, the highest-risk state) is NEVER reaped, and
|
|
861
|
-
* neither is a recently-`closed`/`crashed` session that just exited (still
|
|
862
|
-
* resumable). `pidAlive` absent (a cloud row or an older peer that can't prove
|
|
863
|
-
* death) also stays un-reaped — reaping is fail-safe, never a guess.
|
|
864
|
-
*/
|
|
848
|
+
/** Reap only stale sessions with proven-dead pids; unknown or live processes remain recoverable. */
|
|
865
849
|
export declare function isReapableOrphan(row: Pick<ActiveSession, 'status' | 'pidAlive'>): boolean;
|
|
866
850
|
/**
|
|
867
851
|
* Resolve each teams row's `orchestratorLabel` from the orchestrator's own row,
|
|
@@ -47,14 +47,7 @@ import { linearIssueUrl } from './linear.js';
|
|
|
47
47
|
import { viewingInLabel } from './viewing-in.js';
|
|
48
48
|
import { claudeProjectDirName } from '../project-key.js';
|
|
49
49
|
const execFileAsync = promisify(execFile);
|
|
50
|
-
/**
|
|
51
|
-
* The owner (actor id) to show for a session in `--active`. Prefers the actor
|
|
52
|
-
* recorded on the live-attribution source (the pid registry / teammate record),
|
|
53
|
-
* but falls back to the durable per-session actor sidecar — written at spawn and,
|
|
54
|
-
* unlike the pid entry, NOT overwritten by the SessionStart hook's own by-pid
|
|
55
|
-
* write. Without this fallback a real `agents run` shows no owner whenever the
|
|
56
|
-
* hook's actor-less entry wins the by-pid file (RUSH-2018 fix).
|
|
57
|
-
*/
|
|
50
|
+
/** Prefer live actor attribution; the durable sidecar survives actor-less hook rewrites. */
|
|
58
51
|
export function resolveOwner(pidActor, sessionId) {
|
|
59
52
|
return pidActor ?? (sessionId ? readSessionActorRecord(sessionId)?.actor : undefined) ?? undefined;
|
|
60
53
|
}
|
|
@@ -152,6 +145,8 @@ export function backfillActiveRowsFromMeta(sessions, metaById) {
|
|
|
152
145
|
continue;
|
|
153
146
|
if (!s.version && m.version)
|
|
154
147
|
s.version = m.version;
|
|
148
|
+
if (!s.account && m.account)
|
|
149
|
+
s.account = m.account;
|
|
155
150
|
if (!s.label && m.label)
|
|
156
151
|
s.label = m.label;
|
|
157
152
|
if (!s.ticket && m.ticketId)
|
|
@@ -476,18 +471,8 @@ export function isPidAlive(pid, startedAtMs) {
|
|
|
476
471
|
return true;
|
|
477
472
|
}
|
|
478
473
|
/**
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
* A pid-alive entry is a live session. A pid-DEAD entry is normally noise — a
|
|
482
|
-
* terminal that closed a moment ago, before its window republished — and is
|
|
483
|
-
* dropped. But a dead pid whose owning window ALSO stopped republishing is the
|
|
484
|
-
* signature of a crash: the window went down hard and never ran the teardown that
|
|
485
|
-
* would have removed this entry. Those are KEPT, so the session reaches the
|
|
486
|
-
* listing at all — it used to vanish outright, a VS Code crash simply erasing its
|
|
487
|
-
* agents from `--active`. Such a row arrives as `closed` (dead pid) carrying the
|
|
488
|
-
* stale `windowHeartbeatMs`, which is what {@link foldHostLink} promotes to
|
|
489
|
-
* `crashed`. `pidDead` is local to the dedupe below: a live entry must win a dead
|
|
490
|
-
* one for the same session.
|
|
474
|
+
* Keep dead entries only when their window heartbeat also stopped, proving a crash;
|
|
475
|
+
* a live duplicate always wins.
|
|
491
476
|
*/
|
|
492
477
|
function readLiveTerminals() {
|
|
493
478
|
let raw;
|
|
@@ -541,20 +526,8 @@ function readLiveTerminals() {
|
|
|
541
526
|
const CLAUDE_SESSION_FILE_CACHE_MAX = 256;
|
|
542
527
|
const claudeSessionFileCache = new Map();
|
|
543
528
|
/**
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
* fall back to the most-recent-mtime .jsonl in the project's folder.
|
|
547
|
-
*
|
|
548
|
-
* Searches EVERY version-home project root, not just the live `~/.claude`
|
|
549
|
-
* symlink. `~/.claude` points at the currently-installed agent version; a
|
|
550
|
-
* session launched under an EARLIER version keeps its transcript under that
|
|
551
|
-
* version's home (`…/.history/versions/claude/<ver>/home/.claude/projects/`).
|
|
552
|
-
* Resolving only `~/.claude/projects` meant that the instant a newer version
|
|
553
|
-
* was installed, every still-running older-version session lost its transcript
|
|
554
|
-
* here — no `sessionFile`, so no start/activity time, so `agents sessions`
|
|
555
|
-
* rendered it `unknown` and the watchdog skipped it as "no activity timestamp".
|
|
556
|
-
* `getAgentSessionDirs('claude','projects')` is the same version-aware enumerator
|
|
557
|
-
* the rest of the CLI uses, so this stays in lockstep with discovery.
|
|
529
|
+
* Search every version home because the live ~/.claude symlink moves after upgrades
|
|
530
|
+
* while older running sessions keep writing to their original home.
|
|
558
531
|
*/
|
|
559
532
|
function findClaudeSessionFile(cwd, sessionId) {
|
|
560
533
|
// Only memoize when the exact session UUID is known. Without an id the
|
|
@@ -2080,17 +2053,7 @@ export function foldHostLink(rows) {
|
|
|
2080
2053
|
}
|
|
2081
2054
|
if (link === 'host-gone' && s.status === 'closed')
|
|
2082
2055
|
s.status = 'crashed';
|
|
2083
|
-
//
|
|
2084
|
-
// status (SES-18a). Extending this to a running agent with no client was
|
|
2085
|
-
// tried and reverted: since RUSH-3125 wraps every remote interactive run in
|
|
2086
|
-
// a detached tmux pane, "running with zero attached clients" is the NORMAL
|
|
2087
|
-
// steady state between check-ins, and that path writes no detach record, so
|
|
2088
|
-
// `deliberatelyDetached` is false for it. Promoting it would relabel every
|
|
2089
|
-
// remote agent as orphaned whenever nobody is looking — the over-reporting
|
|
2090
|
-
// this file's header calls worthless. Telling a stranded agent from a
|
|
2091
|
-
// healthy unattended one needs to know a client was EXPECTED and LOST, which
|
|
2092
|
-
// no signal available here carries; that belongs with the peer-side pane
|
|
2093
|
-
// ownership work, not this function.
|
|
2056
|
+
// A clientless running remote pane is normal; only stopped work can be called orphaned.
|
|
2094
2057
|
else if (link === 'no-client' && (s.status === 'idle' || s.status === 'input_required')) {
|
|
2095
2058
|
s.status = 'orphaned';
|
|
2096
2059
|
}
|
|
@@ -2103,18 +2066,7 @@ function recapLine(s, max = 120) {
|
|
|
2103
2066
|
return undefined;
|
|
2104
2067
|
return t.length > max ? t.slice(0, max - 1).trimEnd() + '…' : t;
|
|
2105
2068
|
}
|
|
2106
|
-
/**
|
|
2107
|
-
* The recap ladder (RUSH-3011): compute a row's shown {@link ActiveSession.title}
|
|
2108
|
-
* + {@link RecapSource} from the best available source, plus the cleaned first
|
|
2109
|
-
* prompt (`userPromptClean`/`userPromptKind`) and the `lastAgentLine`. Pure over
|
|
2110
|
-
* one row; exported for tests and folded in by {@link foldRecap}.
|
|
2111
|
-
*
|
|
2112
|
-
* Ladder, best-first: a `/rename`/harness `label` → the last assistant line →
|
|
2113
|
-
* the first-prompt topic. The `last` rung is agent-derived, so a session that
|
|
2114
|
-
* produced work stops showing its stale first prompt as the title. (`topic` is
|
|
2115
|
-
* the row's already-extracted first line, so image detection here is
|
|
2116
|
-
* path-based; a pure-attachment turn with no first-line text stays on `prompt`.)
|
|
2117
|
-
*/
|
|
2069
|
+
/** Labels win, then the last assistant line, then the first-prompt topic. */
|
|
2118
2070
|
export function deriveSessionRecap(row) {
|
|
2119
2071
|
const lastAgentLine = recapLine(row.tail?.length ? row.tail[row.tail.length - 1] : undefined);
|
|
2120
2072
|
const { clean: userPromptClean, kind: userPromptKind } = classifyUserPrompt(row.topic ?? '');
|
|
@@ -2147,17 +2099,7 @@ export function foldRecap(rows) {
|
|
|
2147
2099
|
s.lastAgentLine = recap.lastAgentLine;
|
|
2148
2100
|
}
|
|
2149
2101
|
}
|
|
2150
|
-
/**
|
|
2151
|
-
* True when a crash-leaked orphan is genuinely DEAD and should be reaped from the
|
|
2152
|
-
* reconnectable set rather than shown as resumable forever (RUSH-3011 / issue #3b).
|
|
2153
|
-
*
|
|
2154
|
-
* The gate is `abandoned` (no transcript write in {@link ABANDONED_STALE_MS}) AND
|
|
2155
|
-
* a dead pid — exactly "past the stale threshold whose pid is gone". A live pid
|
|
2156
|
-
* (an idle-but-unfinished session, the highest-risk state) is NEVER reaped, and
|
|
2157
|
-
* neither is a recently-`closed`/`crashed` session that just exited (still
|
|
2158
|
-
* resumable). `pidAlive` absent (a cloud row or an older peer that can't prove
|
|
2159
|
-
* death) also stays un-reaped — reaping is fail-safe, never a guess.
|
|
2160
|
-
*/
|
|
2102
|
+
/** Reap only stale sessions with proven-dead pids; unknown or live processes remain recoverable. */
|
|
2161
2103
|
export function isReapableOrphan(row) {
|
|
2162
2104
|
return row.status === 'abandoned' && row.pidAlive === false;
|
|
2163
2105
|
}
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
import Database from '../sqlite.js';
|
|
10
10
|
import type { SessionAgentId, SessionEvent, SessionMeta } from './types.js';
|
|
11
11
|
import { type IndexedToolCall } from './tool-calls.js';
|
|
12
|
+
import { type ToolScanResumePoint } from './tool-store.js';
|
|
12
13
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
13
14
|
* assert against the constant instead of hardcoding a number that every bump
|
|
14
15
|
* then has to chase (docs/sessions.md calls the constant the source of truth). */
|
|
15
|
-
export declare const SCHEMA_VERSION =
|
|
16
|
+
export declare const SCHEMA_VERSION = 43;
|
|
16
17
|
/**
|
|
17
18
|
* Bump to force the content extractor (assistant-answer text, alongside the
|
|
18
19
|
* user-prompt text every harness already accumulates) to re-derive on every
|
|
@@ -35,6 +36,8 @@ export declare const CONTENT_INDEX_VERSION = 1;
|
|
|
35
36
|
export declare const INSIGHTS_EXTRACTOR_VERSION = 7;
|
|
36
37
|
/** Bump when classifyTopic's output changes so cached topics recompute (human task taxonomy v2). */
|
|
37
38
|
export declare const SESSION_TOPIC_EXTRACTOR_VERSION = 2;
|
|
39
|
+
/** Bump when classifyPhenotype's output changes so cached phenotypes recompute (PHNX-3327 v1). */
|
|
40
|
+
export declare const SESSION_PHENOTYPE_EXTRACTOR_VERSION = 1;
|
|
38
41
|
/** File stat snapshot used to detect changes between scan runs. */
|
|
39
42
|
export interface ScanStamp {
|
|
40
43
|
fileMtimeMs: number;
|
|
@@ -249,6 +252,13 @@ export declare function upsertSessionsBatch(entries: Array<{
|
|
|
249
252
|
toolCalls?: IndexedToolCall[];
|
|
250
253
|
toolScan?: ScanStamp;
|
|
251
254
|
toolIndexMode?: 'replace' | 'append';
|
|
255
|
+
/**
|
|
256
|
+
* Where the NEXT scan of this full-file-harness session may resume its tool
|
|
257
|
+
* index (PHNX-3411). Computed internally in the enrichment map below; not
|
|
258
|
+
* supplied by callers. Persisted alongside the tool ledger so an active
|
|
259
|
+
* session re-derives only its newly appended tool calls next tick.
|
|
260
|
+
*/
|
|
261
|
+
toolResume?: ToolScanResumePoint | null;
|
|
252
262
|
}>): void;
|
|
253
263
|
/**
|
|
254
264
|
* Sync labels for a set of sessions. For each id in the map, if the stored
|
|
@@ -377,6 +387,15 @@ export declare function writeSessionTopics<T>(entries: Array<{
|
|
|
377
387
|
fileSize: number | null;
|
|
378
388
|
topic: T;
|
|
379
389
|
}>): void;
|
|
390
|
+
/** Read cached failure phenotypes only when their transcript byte stamps still match. */
|
|
391
|
+
export declare function readSessionPhenotypes<T>(ids: string[]): Map<string, T>;
|
|
392
|
+
/** Persist failure phenotypes against the exact transcript bytes used to classify them. */
|
|
393
|
+
export declare function writeSessionPhenotypes<T>(entries: Array<{
|
|
394
|
+
id: string;
|
|
395
|
+
fileMtimeMs: number | null;
|
|
396
|
+
fileSize: number | null;
|
|
397
|
+
phenotype: T;
|
|
398
|
+
}>): void;
|
|
380
399
|
/** Read one derived preview only when it matches the transcript bytes on disk. */
|
|
381
400
|
export declare function readSessionPreviewCache<T>(id: string, sourceStamp: {
|
|
382
401
|
fileMtimeMs: number | null;
|
|
@@ -567,46 +586,15 @@ interface TopCostSession {
|
|
|
567
586
|
* vanished, mirroring querySessions' liveness filter.
|
|
568
587
|
*/
|
|
569
588
|
export declare function topSessionsByCost(n: number, options?: QueryOptions): TopCostSession[];
|
|
570
|
-
/** Look up a single session by its unique ID. */
|
|
571
589
|
/**
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
* `getSessionById` would re-`prepare` a `SELECT *` and materialize a full
|
|
575
|
-
* `SessionMeta` per id to read it — mirrors {@link findSessionsByShortIds}'s
|
|
576
|
-
* single-round-trip pattern. Ids absent from the index are simply absent from
|
|
577
|
-
* the map. Best-effort: an unavailable DB yields an empty map, so the live view
|
|
578
|
-
* still renders (the caller then leaves rows attributed to this box).
|
|
590
|
+
* Read machine attribution in batches without materializing full sessions.
|
|
591
|
+
* Failure is best-effort so the live view can still render local attribution.
|
|
579
592
|
*/
|
|
580
593
|
export declare function findSessionMachinesByIds(ids: string[]): Map<string, string>;
|
|
581
594
|
export declare function getSessionById(id: string): SessionMeta | null;
|
|
582
|
-
/**
|
|
583
|
-
* Resolve a full-or-partial session id against the index, exact-first then
|
|
584
|
-
* prefix — the DB-backed equivalent of resolveSessionById() that runs over the
|
|
585
|
-
* SQLite table instead of a pre-loaded array. Matches both the full id and the
|
|
586
|
-
* short id. An exact hit short-circuits so a complete id never also drags in its
|
|
587
|
-
* prefix siblings. `scope` narrows by agent / version / project (cwd) so an
|
|
588
|
-
* ambiguous prefix disambiguates against the caller's context.
|
|
589
|
-
*
|
|
590
|
-
* Routes through the full querySessions existence check (NOT skipExistenceCheck)
|
|
591
|
-
* on purpose (RUSH-2436): that check now KEEPS a file-gone session whose user
|
|
592
|
-
* turns still live in session_text (flagged archived) and only suppresses a
|
|
593
|
-
* contentless phantom — so `agents sessions <id>` resolves an archived session
|
|
594
|
-
* instead of failing with "No session found", while a phantom id still misses.
|
|
595
|
-
*/
|
|
595
|
+
/** Exact ids win over prefixes; the normal existence check preserves archived content but excludes phantoms. */
|
|
596
596
|
export declare function findSessionsById(idQuery: string, scope?: Pick<QueryOptions, 'agent' | 'version' | 'cwd' | 'project'>): SessionMeta[];
|
|
597
|
-
/**
|
|
598
|
-
* Batch-resolve many 8-char short ids to their sessions in ONE indexed query.
|
|
599
|
-
* The live-scan path (listTmuxAgentSessions) turns every `ag-<agent>-<shortid>`
|
|
600
|
-
* tmux pane name back into a full session id this way, so it pays a single
|
|
601
|
-
* `short_id IN (…)` round-trip per scan instead of N per-pane lookups.
|
|
602
|
-
*
|
|
603
|
-
* Returns a map keyed by short_id (lowercased). Short ids are the first 8 chars
|
|
604
|
-
* of the lowercase session UUID (deriveShortId), so a lowercased `IN` matches and
|
|
605
|
-
* still uses idx_sessions_short_id. When several sessions share a short id — only
|
|
606
|
-
* time-ordered ids (ULID/UUIDv7) ever collide; random UUIDv4 short ids are unique
|
|
607
|
-
* in practice — the most-recently-active one wins (the caller can further
|
|
608
|
-
* disambiguate by cwd).
|
|
609
|
-
*/
|
|
597
|
+
/** Batch-resolve pane short ids; on collision the most recently active session wins. */
|
|
610
598
|
export declare function findSessionsByShortIds(shortIds: string[]): Map<string, SessionMeta>;
|
|
611
599
|
/** A single full-text search result with ranking score. */
|
|
612
600
|
interface FtsHit {
|
package/dist/lib/session/db.js
CHANGED
|
@@ -15,8 +15,8 @@ import { getSessionsDir, getSessionsDbPath } from '../state.js';
|
|
|
15
15
|
import { query as queryEvents, queryToolUsageForSessions } from '../feed/events.js';
|
|
16
16
|
import { machineForSessionFile } from '../origin-machine.js';
|
|
17
17
|
import { loadSessionActorIndex, readSessionActorRecord } from './actor-sidecar.js';
|
|
18
|
-
import {
|
|
19
|
-
import { persistToolCalls, toolEvidenceSourcePath } from './tool-store.js';
|
|
18
|
+
import { scanEventToolCalls } from './tool-calls.js';
|
|
19
|
+
import { persistToolCalls, planEventToolResume, toolEvidenceSourcePath } from './tool-store.js';
|
|
20
20
|
import { buildClaudeAccountIndex, resolveClaudeAccount } from './claude-accounts.js';
|
|
21
21
|
import { extractBackgroundShells, extractSkills, extractSlashCommands, harnessTracksBackgroundShells, isSubAgentTool, } from './highlights.js';
|
|
22
22
|
import { resolveResource } from '../resources.js';
|
|
@@ -27,7 +27,7 @@ const DB_PATH = getSessionsDbPath();
|
|
|
27
27
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
28
28
|
* assert against the constant instead of hardcoding a number that every bump
|
|
29
29
|
* then has to chase (docs/sessions.md calls the constant the source of truth). */
|
|
30
|
-
export const SCHEMA_VERSION =
|
|
30
|
+
export const SCHEMA_VERSION = 43;
|
|
31
31
|
/**
|
|
32
32
|
* Bump to force the content extractor (assistant-answer text, alongside the
|
|
33
33
|
* user-prompt text every harness already accumulates) to re-derive on every
|
|
@@ -207,6 +207,11 @@ CREATE TABLE IF NOT EXISTS tool_calls (
|
|
|
207
207
|
ordinal INTEGER NOT NULL,
|
|
208
208
|
source_call_id TEXT,
|
|
209
209
|
timestamp TEXT NOT NULL,
|
|
210
|
+
-- When the call's RESULT record arrived (its own end time). end_timestamp
|
|
211
|
+
-- minus timestamp is the call's own blocking duration, which the traces
|
|
212
|
+
-- insight engine attributes as a failed call's wasted time (PHNX-3437). NULL
|
|
213
|
+
-- for a call that never produced a result and for rows an older extractor stored.
|
|
214
|
+
end_timestamp TEXT,
|
|
210
215
|
tool TEXT NOT NULL,
|
|
211
216
|
input TEXT NOT NULL,
|
|
212
217
|
outcome TEXT NOT NULL,
|
|
@@ -352,6 +357,26 @@ CREATE TABLE IF NOT EXISTS session_topics (
|
|
|
352
357
|
topic_json TEXT NOT NULL
|
|
353
358
|
);
|
|
354
359
|
|
|
360
|
+
-- Derived failure phenotype for traces sync (PHNX-3327). Like session_topics /
|
|
361
|
+
-- session_insights, this is a lazy, stamp-validated cache keyed on
|
|
362
|
+
-- (file_mtime_ms, file_size) and intentionally independent of SCHEMA_VERSION.
|
|
363
|
+
-- Classifying a phenotype needs the full derived SessionTrajectory (ordered
|
|
364
|
+
-- steps, gaps), which buildIndexShard does NOT have from flat tool_calls rows —
|
|
365
|
+
-- so it is computed per-session ONCE (parse -> trajectory -> classify) and cached
|
|
366
|
+
-- here, then read for the WHOLE corpus on every sync. That is what lets the
|
|
367
|
+
-- phenotype grouping dimension fold two identically-signatured sessions into one
|
|
368
|
+
-- cluster regardless of which incremental batch each was first synced in, without
|
|
369
|
+
-- re-parsing transcripts at 10k+ session scale. phenotype_json holds
|
|
370
|
+
-- { phenotype: FailurePhenotype | null } (null = no failure phenotype matched).
|
|
371
|
+
CREATE TABLE IF NOT EXISTS session_phenotypes (
|
|
372
|
+
session_id TEXT PRIMARY KEY,
|
|
373
|
+
file_mtime_ms INTEGER,
|
|
374
|
+
file_size INTEGER,
|
|
375
|
+
extractor_version INTEGER NOT NULL,
|
|
376
|
+
computed_at INTEGER NOT NULL,
|
|
377
|
+
phenotype_json TEXT NOT NULL
|
|
378
|
+
);
|
|
379
|
+
|
|
355
380
|
-- Normalized data behind sessions preview. Like session_insights this is a
|
|
356
381
|
-- lazy, stamp-validated cache: opening one session parses only that transcript,
|
|
357
382
|
-- while subsequent processes reuse the derived preview until its bytes change.
|
|
@@ -437,7 +462,12 @@ CREATE INDEX IF NOT EXISTS idx_computer_sessions_started ON computer_sessions(st
|
|
|
437
462
|
export const INSIGHTS_EXTRACTOR_VERSION = 7;
|
|
438
463
|
/** Bump when classifyTopic's output changes so cached topics recompute (human task taxonomy v2). */
|
|
439
464
|
export const SESSION_TOPIC_EXTRACTOR_VERSION = 2;
|
|
440
|
-
|
|
465
|
+
// Bumped to 2 (PHNX-2973): the digest now carries `changedFiles` (per-file
|
|
466
|
+
// paths). Bumping invalidates v1 cache rows so a fresh recompute populates the
|
|
467
|
+
// new field instead of serving a stale digest that predates it.
|
|
468
|
+
const PREVIEW_EXTRACTOR_VERSION = 2;
|
|
469
|
+
/** Bump when classifyPhenotype's output changes so cached phenotypes recompute (PHNX-3327 v1). */
|
|
470
|
+
export const SESSION_PHENOTYPE_EXTRACTOR_VERSION = 1;
|
|
441
471
|
let dbInstance = null;
|
|
442
472
|
/**
|
|
443
473
|
* Apply schema migrations from `fromVersion` → SCHEMA_VERSION. The new
|
|
@@ -1152,6 +1182,29 @@ function migrateSchema(db, fromVersion) {
|
|
|
1152
1182
|
// Claude/Codex resumable continuation) stay intact for rows that don't
|
|
1153
1183
|
// need a full reparse for any OTHER reason.
|
|
1154
1184
|
}
|
|
1185
|
+
if (fromVersion < 43) {
|
|
1186
|
+
// v42 -> v43: persist a per-tool-call END timestamp (PHNX-3437). The traces
|
|
1187
|
+
// insight engine could only book a failed call's wasted time from the
|
|
1188
|
+
// bounded gap to the NEXT call — so a call that BLOCKED for minutes and was
|
|
1189
|
+
// the last in its session (or was followed quickly by an unrelated call)
|
|
1190
|
+
// registered as ~0 waste. The call's result record already carried its own
|
|
1191
|
+
// timestamp at ingestion; this column persists it so `end_timestamp -
|
|
1192
|
+
// timestamp` (the call's own blocking duration) becomes the primary
|
|
1193
|
+
// attribution, with the gap heuristic kept as the fallback for NULL rows.
|
|
1194
|
+
//
|
|
1195
|
+
// Additive, nullable column — no ledger flush (the v33->v34 contract that
|
|
1196
|
+
// adding a column keeps warm session ledgers warm). Pre-upgrade rows stay
|
|
1197
|
+
// NULL until re-indexed, and `insights.ts` degrades a NULL end back to the
|
|
1198
|
+
// bounded-gap behavior, so nothing crashes or yields NaN. The paired
|
|
1199
|
+
// TOOL_INDEX_VERSION bump (7 -> 8) is what re-derives it on a re-index; the
|
|
1200
|
+
// tool index is deliberately independent of SCHEMA_VERSION and is never
|
|
1201
|
+
// force-rescanned by a migration (only by the explicit tool backfill or a
|
|
1202
|
+
// normal incremental append), so a bare ALTER here would otherwise leave
|
|
1203
|
+
// existing rows dark forever.
|
|
1204
|
+
const cols = new Set(db.prepare(`PRAGMA table_info(tool_calls)`).all().map((c) => c.name));
|
|
1205
|
+
if (!cols.has('end_timestamp'))
|
|
1206
|
+
db.exec(`ALTER TABLE tool_calls ADD COLUMN end_timestamp TEXT`);
|
|
1207
|
+
}
|
|
1155
1208
|
}
|
|
1156
1209
|
/**
|
|
1157
1210
|
* Stamp `account_key` / `account_org` / `account` on every Claude row from its
|
|
@@ -2123,6 +2176,21 @@ export function upsertSessionsBatch(entries) {
|
|
|
2123
2176
|
// metadata fall back to exactly one normalized parse here.
|
|
2124
2177
|
const events = entry.events ?? parseSession(entry.meta.filePath, entry.meta.agent);
|
|
2125
2178
|
writeResourceUsage(entry.meta.id, events, entry.meta.cwd);
|
|
2179
|
+
// Resume the tool index from the last scan of this append-only stream when
|
|
2180
|
+
// it is safe to (PHNX-3411). A live session's transcript grows every tick,
|
|
2181
|
+
// so a full re-derive re-sanitizes its ENTIRE tool history each time — the
|
|
2182
|
+
// synchronous O(session) work that wedged the daemon event loop for the
|
|
2183
|
+
// 11 non-streaming harnesses. `planEventToolResume` returns the prior
|
|
2184
|
+
// snapshot only when the file is still an append of what was scanned
|
|
2185
|
+
// before; otherwise `prior` is null and this is a full replace from
|
|
2186
|
+
// event 0 (identical index, just re-derived).
|
|
2187
|
+
// No tool stamp (a scanner that carried no scan record) means the resume
|
|
2188
|
+
// point cannot be size-guarded, so full-scan rather than trust a stale
|
|
2189
|
+
// offset. persistToolCalls below also skips a resume-less write.
|
|
2190
|
+
const prior = toolScan
|
|
2191
|
+
? planEventToolResume(db, entry.meta.id, toolSourcePath, toolScan, events.length)
|
|
2192
|
+
: null;
|
|
2193
|
+
const scanned = scanEventToolCalls(events, prior ?? undefined);
|
|
2126
2194
|
return {
|
|
2127
2195
|
...entry,
|
|
2128
2196
|
meta: {
|
|
@@ -2131,11 +2199,14 @@ export function upsertSessionsBatch(entries) {
|
|
|
2131
2199
|
recentDirectoriesTouched: extractRecentDirectoriesTouched(events, entry.meta.cwd),
|
|
2132
2200
|
...fanOutCounts(events, entry.meta.agent),
|
|
2133
2201
|
},
|
|
2134
|
-
|
|
2202
|
+
// The CHANGED calls only. On a resume these are the newly appended tail
|
|
2203
|
+
// (append-safe upsert); on a full scan they are the whole history.
|
|
2204
|
+
toolCalls: scanned.calls,
|
|
2135
2205
|
toolScan,
|
|
2136
|
-
|
|
2137
|
-
//
|
|
2138
|
-
|
|
2206
|
+
toolIndexMode: (prior ? 'append' : 'replace'),
|
|
2207
|
+
// Persist where the NEXT scan resumes: the collector snapshot + how many
|
|
2208
|
+
// events this scan folded.
|
|
2209
|
+
toolResume: { parserState: JSON.stringify(scanned.snapshot), parsedOffset: scanned.eventCount },
|
|
2139
2210
|
};
|
|
2140
2211
|
}
|
|
2141
2212
|
catch {
|
|
@@ -2284,7 +2355,13 @@ export function upsertSessionsBatch(entries) {
|
|
|
2284
2355
|
if (!toolScan || !entry.toolCalls)
|
|
2285
2356
|
continue;
|
|
2286
2357
|
try {
|
|
2287
|
-
|
|
2358
|
+
// `resume` is set only by the full-file harness path above; claude/codex
|
|
2359
|
+
// pass none, so their tool ledger keeps carrying no event-offset resume
|
|
2360
|
+
// point (their resume rides the content-scan ledger instead) — unchanged.
|
|
2361
|
+
persistToolCalls(db, entry.meta, entry.toolCalls, toolScan, {
|
|
2362
|
+
mode: entry.toolIndexMode ?? 'replace',
|
|
2363
|
+
resume: entry.toolResume,
|
|
2364
|
+
});
|
|
2288
2365
|
}
|
|
2289
2366
|
catch {
|
|
2290
2367
|
// Boundary is intentionally retryable via tool_scan_ledger.
|
|
@@ -2950,6 +3027,59 @@ export function writeSessionTopics(entries) {
|
|
|
2950
3027
|
}
|
|
2951
3028
|
})();
|
|
2952
3029
|
}
|
|
3030
|
+
/** Read cached failure phenotypes only when their transcript byte stamps still match. */
|
|
3031
|
+
export function readSessionPhenotypes(ids) {
|
|
3032
|
+
const db = getDB();
|
|
3033
|
+
const out = new Map();
|
|
3034
|
+
if (ids.length === 0)
|
|
3035
|
+
return out;
|
|
3036
|
+
const CHUNK = 400;
|
|
3037
|
+
for (let i = 0; i < ids.length; i += CHUNK) {
|
|
3038
|
+
const chunk = ids.slice(i, i + CHUNK);
|
|
3039
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
3040
|
+
const rows = db.prepare(`
|
|
3041
|
+
SELECT sp.session_id AS id, sp.phenotype_json AS phenotypeJson
|
|
3042
|
+
FROM session_phenotypes sp
|
|
3043
|
+
JOIN sessions s ON s.id = sp.session_id
|
|
3044
|
+
WHERE sp.session_id IN (${placeholders})
|
|
3045
|
+
AND sp.extractor_version = ?
|
|
3046
|
+
AND sp.file_mtime_ms IS s.file_mtime_ms
|
|
3047
|
+
AND sp.file_size IS s.file_size
|
|
3048
|
+
`).all(...chunk, SESSION_PHENOTYPE_EXTRACTOR_VERSION);
|
|
3049
|
+
for (const row of rows) {
|
|
3050
|
+
try {
|
|
3051
|
+
out.set(row.id, JSON.parse(row.phenotypeJson));
|
|
3052
|
+
}
|
|
3053
|
+
catch {
|
|
3054
|
+
// Invalid derived cache data is a miss and self-heals on the next write.
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
return out;
|
|
3059
|
+
}
|
|
3060
|
+
/** Persist failure phenotypes against the exact transcript bytes used to classify them. */
|
|
3061
|
+
export function writeSessionPhenotypes(entries) {
|
|
3062
|
+
if (entries.length === 0)
|
|
3063
|
+
return;
|
|
3064
|
+
const db = getDB();
|
|
3065
|
+
const stmt = db.prepare(`
|
|
3066
|
+
INSERT INTO session_phenotypes
|
|
3067
|
+
(session_id, file_mtime_ms, file_size, extractor_version, computed_at, phenotype_json)
|
|
3068
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
3069
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
3070
|
+
file_mtime_ms = excluded.file_mtime_ms,
|
|
3071
|
+
file_size = excluded.file_size,
|
|
3072
|
+
extractor_version = excluded.extractor_version,
|
|
3073
|
+
computed_at = excluded.computed_at,
|
|
3074
|
+
phenotype_json = excluded.phenotype_json
|
|
3075
|
+
`);
|
|
3076
|
+
const now = Date.now();
|
|
3077
|
+
db.transaction(() => {
|
|
3078
|
+
for (const entry of entries) {
|
|
3079
|
+
stmt.run(entry.id, entry.fileMtimeMs, entry.fileSize, SESSION_PHENOTYPE_EXTRACTOR_VERSION, now, JSON.stringify(entry.phenotype));
|
|
3080
|
+
}
|
|
3081
|
+
})();
|
|
3082
|
+
}
|
|
2953
3083
|
/** Read one derived preview only when it matches the transcript bytes on disk. */
|
|
2954
3084
|
export function readSessionPreviewCache(id, sourceStamp) {
|
|
2955
3085
|
const row = getDB().prepare(`
|
|
@@ -3390,15 +3520,9 @@ export function topSessionsByCost(n, options = {}) {
|
|
|
3390
3520
|
durationMs: r.duration_ms ?? 0,
|
|
3391
3521
|
}));
|
|
3392
3522
|
}
|
|
3393
|
-
/** Look up a single session by its unique ID. */
|
|
3394
3523
|
/**
|
|
3395
|
-
*
|
|
3396
|
-
*
|
|
3397
|
-
* `getSessionById` would re-`prepare` a `SELECT *` and materialize a full
|
|
3398
|
-
* `SessionMeta` per id to read it — mirrors {@link findSessionsByShortIds}'s
|
|
3399
|
-
* single-round-trip pattern. Ids absent from the index are simply absent from
|
|
3400
|
-
* the map. Best-effort: an unavailable DB yields an empty map, so the live view
|
|
3401
|
-
* still renders (the caller then leaves rows attributed to this box).
|
|
3524
|
+
* Read machine attribution in batches without materializing full sessions.
|
|
3525
|
+
* Failure is best-effort so the live view can still render local attribution.
|
|
3402
3526
|
*/
|
|
3403
3527
|
export function findSessionMachinesByIds(ids) {
|
|
3404
3528
|
const out = new Map();
|
|
@@ -3429,20 +3553,7 @@ export function getSessionById(id) {
|
|
|
3429
3553
|
const row = db.prepare(`SELECT * FROM sessions WHERE id = ?`).get(id);
|
|
3430
3554
|
return row ? rowToMeta(row) : null;
|
|
3431
3555
|
}
|
|
3432
|
-
/**
|
|
3433
|
-
* Resolve a full-or-partial session id against the index, exact-first then
|
|
3434
|
-
* prefix — the DB-backed equivalent of resolveSessionById() that runs over the
|
|
3435
|
-
* SQLite table instead of a pre-loaded array. Matches both the full id and the
|
|
3436
|
-
* short id. An exact hit short-circuits so a complete id never also drags in its
|
|
3437
|
-
* prefix siblings. `scope` narrows by agent / version / project (cwd) so an
|
|
3438
|
-
* ambiguous prefix disambiguates against the caller's context.
|
|
3439
|
-
*
|
|
3440
|
-
* Routes through the full querySessions existence check (NOT skipExistenceCheck)
|
|
3441
|
-
* on purpose (RUSH-2436): that check now KEEPS a file-gone session whose user
|
|
3442
|
-
* turns still live in session_text (flagged archived) and only suppresses a
|
|
3443
|
-
* contentless phantom — so `agents sessions <id>` resolves an archived session
|
|
3444
|
-
* instead of failing with "No session found", while a phantom id still misses.
|
|
3445
|
-
*/
|
|
3556
|
+
/** Exact ids win over prefixes; the normal existence check preserves archived content but excludes phantoms. */
|
|
3446
3557
|
export function findSessionsById(idQuery, scope = {}) {
|
|
3447
3558
|
const q = idQuery.trim();
|
|
3448
3559
|
if (!q)
|
|
@@ -3452,19 +3563,7 @@ export function findSessionsById(idQuery, scope = {}) {
|
|
|
3452
3563
|
return exact;
|
|
3453
3564
|
return querySessions({ ...scope, idPrefix: q });
|
|
3454
3565
|
}
|
|
3455
|
-
/**
|
|
3456
|
-
* Batch-resolve many 8-char short ids to their sessions in ONE indexed query.
|
|
3457
|
-
* The live-scan path (listTmuxAgentSessions) turns every `ag-<agent>-<shortid>`
|
|
3458
|
-
* tmux pane name back into a full session id this way, so it pays a single
|
|
3459
|
-
* `short_id IN (…)` round-trip per scan instead of N per-pane lookups.
|
|
3460
|
-
*
|
|
3461
|
-
* Returns a map keyed by short_id (lowercased). Short ids are the first 8 chars
|
|
3462
|
-
* of the lowercase session UUID (deriveShortId), so a lowercased `IN` matches and
|
|
3463
|
-
* still uses idx_sessions_short_id. When several sessions share a short id — only
|
|
3464
|
-
* time-ordered ids (ULID/UUIDv7) ever collide; random UUIDv4 short ids are unique
|
|
3465
|
-
* in practice — the most-recently-active one wins (the caller can further
|
|
3466
|
-
* disambiguate by cwd).
|
|
3467
|
-
*/
|
|
3566
|
+
/** Batch-resolve pane short ids; on collision the most recently active session wins. */
|
|
3468
3567
|
export function findSessionsByShortIds(shortIds) {
|
|
3469
3568
|
const out = new Map();
|
|
3470
3569
|
const uniq = [...new Set(shortIds.map((s) => s.trim().toLowerCase()).filter(Boolean))];
|