@phnx-labs/agents-cli 1.22.56 → 1.22.57
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 +14 -0
- package/README.md +4 -4
- package/dist/bootstrap.js +3 -1
- package/dist/cli/command-registry.d.ts +0 -1
- package/dist/cli/command-registry.js +0 -3
- package/dist/commands/exec.js +1 -1
- 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/perf.d.ts +16 -7
- package/dist/commands/perf.js +29 -20
- package/dist/commands/rules.js +1 -1
- package/dist/commands/ssh.js +24 -14
- 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 +9 -0
- 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 +69 -0
- package/dist/lib/accounting/usage-sync.js +129 -0
- package/dist/lib/accounting/usage.d.ts +48 -2
- package/dist/lib/accounting/usage.js +72 -1
- 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/daemon/daemon.js +5 -0
- package/dist/lib/daemon/runner.js +9 -8
- package/dist/lib/daemon/usage-sync-service.d.ts +21 -0
- package/dist/lib/daemon/usage-sync-service.js +36 -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/pool.d.ts +4 -3
- package/dist/lib/devices/pool.js +13 -5
- package/dist/lib/exec.d.ts +6 -41
- package/dist/lib/exec.js +6 -41
- 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/hosts/remote-cmd.d.ts +9 -0
- package/dist/lib/hosts/remote-cmd.js +22 -0
- package/dist/lib/perf/db.d.ts +1 -1
- package/dist/lib/perf/db.js +1 -1
- package/dist/lib/session/active.d.ts +3 -31
- package/dist/lib/session/active.js +8 -68
- package/dist/lib/session/db.d.ts +4 -35
- package/dist/lib/session/db.js +4 -35
- package/dist/lib/session/discover.d.ts +6 -58
- package/dist/lib/session/discover.js +5 -43
- package/dist/lib/session/parse.d.ts +1 -19
- package/dist/lib/session/parse.js +2 -15
- package/dist/lib/startup/command-registry.d.ts +8 -2
- package/dist/lib/startup/command-registry.js +12 -4
- package/package.json +1 -1
package/dist/lib/git.js
CHANGED
|
@@ -498,7 +498,7 @@ export async function getCurrentBranch(repoPath) {
|
|
|
498
498
|
* repo" as a requested source before adopting it.
|
|
499
499
|
*/
|
|
500
500
|
export function canonicalGitRemote(url) {
|
|
501
|
-
|
|
501
|
+
const canonical = url
|
|
502
502
|
.trim()
|
|
503
503
|
.replace(/\/+$/, '') // trailing slashes first, so a trailing-slash-after-.git still strips
|
|
504
504
|
.replace(/\.git$/i, '')
|
|
@@ -506,6 +506,36 @@ export function canonicalGitRemote(url) {
|
|
|
506
506
|
.replace(/^[^@/]+@/, '') // strip user@ (git@, ssh user)
|
|
507
507
|
.replace(':', '/') // scp-style host:owner/repo → host/owner/repo (first colon only)
|
|
508
508
|
.toLowerCase();
|
|
509
|
+
// Fold a renamed repo's old name onto its new one so both compare equal
|
|
510
|
+
// everywhere (see RENAMED_REMOTE_ALIASES).
|
|
511
|
+
return RENAMED_REMOTE_ALIASES[canonical] ?? canonical;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Git remotes that denote the SAME repository under an old and a new name,
|
|
515
|
+
* keyed by canonical `host/owner/repo`. `phnx-labs/.agents-system` was renamed
|
|
516
|
+
* to `phnx-labs/.agents` on GitHub (PHNX-3394); {@link DEFAULT_SYSTEM_REPO}
|
|
517
|
+
* still points at the pre-rename slug (GitHub's own redirect makes that
|
|
518
|
+
* resolve fine), so folding the new name onto it here means both compare equal
|
|
519
|
+
* everywhere remotes are compared: {@link sameGitRemote} (repo adoption),
|
|
520
|
+
* {@link isSystemRepoRemote} (the system-origin check), and the
|
|
521
|
+
* DotAgents-layer classifier in state.ts.
|
|
522
|
+
*/
|
|
523
|
+
const RENAMED_REMOTE_ALIASES = {
|
|
524
|
+
'github.com/phnx-labs/.agents': 'github.com/phnx-labs/.agents-system',
|
|
525
|
+
};
|
|
526
|
+
/**
|
|
527
|
+
* True when a git remote URL (any transport form: ssh, https, scp-style) points
|
|
528
|
+
* at the system DotAgents repo — {@link DEFAULT_SYSTEM_REPO}'s current slug OR
|
|
529
|
+
* its `phnx-labs/.agents` rename target (PHNX-3394), which
|
|
530
|
+
* {@link canonicalGitRemote} folds onto it via {@link RENAMED_REMOTE_ALIASES}.
|
|
531
|
+
* Pure string check with no git spawn, so it is unit-testable off a live
|
|
532
|
+
* checkout; {@link isSystemRepoOrigin} reads a dir's origin and delegates here.
|
|
533
|
+
*/
|
|
534
|
+
export function isSystemRepoRemote(remote) {
|
|
535
|
+
if (!remote)
|
|
536
|
+
return false;
|
|
537
|
+
const c = canonicalGitRemote(remote);
|
|
538
|
+
return c === canonicalGitRemote(`https://github.com/${systemRepoSlug(DEFAULT_SYSTEM_REPO)}`);
|
|
509
539
|
}
|
|
510
540
|
/** True when two git remote URLs point at the same repo across transport forms. */
|
|
511
541
|
export function sameGitRemote(a, b) {
|
|
@@ -1062,18 +1092,17 @@ export async function adoptUserRepoIfNeeded(dir, opts = {}) {
|
|
|
1062
1092
|
return adoptRepoInPlace(dir, url);
|
|
1063
1093
|
}
|
|
1064
1094
|
/**
|
|
1065
|
-
* Check if the repo's origin points to the system repo
|
|
1095
|
+
* Check if the repo's origin points to the system repo — `phnx-labs/.agents-system`
|
|
1096
|
+
* or its GitHub rename target `phnx-labs/.agents` (PHNX-3394), across any
|
|
1097
|
+
* transport form. Reads the dir's origin and delegates the match to the pure
|
|
1098
|
+
* {@link isSystemRepoRemote}.
|
|
1066
1099
|
*/
|
|
1067
1100
|
export async function isSystemRepoOrigin(dir) {
|
|
1068
1101
|
try {
|
|
1069
1102
|
const git = simpleGit(dir);
|
|
1070
1103
|
const remotes = await git.getRemotes(true);
|
|
1071
1104
|
const origin = remotes.find(r => r.name === 'origin');
|
|
1072
|
-
|
|
1073
|
-
return false;
|
|
1074
|
-
const url = origin.refs.fetch.toLowerCase();
|
|
1075
|
-
const currentSlug = systemRepoSlug(DEFAULT_SYSTEM_REPO).toLowerCase();
|
|
1076
|
-
return url.includes(currentSlug);
|
|
1105
|
+
return isSystemRepoRemote(origin?.refs?.fetch);
|
|
1077
1106
|
}
|
|
1078
1107
|
catch {
|
|
1079
1108
|
/* not a git repo or no remotes */
|
|
@@ -44,13 +44,13 @@ export interface ExecConfigEnvCtx {
|
|
|
44
44
|
/** resolveInteractive(options) — computed once by the caller. */
|
|
45
45
|
interactive: boolean;
|
|
46
46
|
/**
|
|
47
|
-
* The role marked on THIS machine (worker | personal | undefined),
|
|
48
|
-
* once by the caller from selfConfiguredDeviceRole(). A
|
|
49
|
-
*
|
|
50
|
-
* credential decision MUST defer to it for EVERY run — interactive
|
|
51
|
-
* — never the worker-only setup-token (RUSH-2395). Injected as a
|
|
52
|
-
* (not imported) to keep the adapter import-leaf. Absent/undefined
|
|
53
|
-
* as non-
|
|
47
|
+
* The role marked on THIS machine (worker | personal | desktop | undefined),
|
|
48
|
+
* resolved once by the caller from selfConfiguredDeviceRole(). A headed device
|
|
49
|
+
* (`personal` or `desktop` — see isHeadedDeviceRole) holds a real per-version
|
|
50
|
+
* login and the credential decision MUST defer to it for EVERY run — interactive
|
|
51
|
+
* OR headless — never the worker-only setup-token (RUSH-2395). Injected as a
|
|
52
|
+
* plain value (not imported) to keep the adapter import-leaf. Absent/undefined
|
|
53
|
+
* is treated as non-headed (worker-equivalent).
|
|
54
54
|
*/
|
|
55
55
|
deviceRole?: ConfiguredDeviceRole;
|
|
56
56
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
2
|
import { stripForeignConfigDir } from '../adapter.js';
|
|
3
|
+
import { isHeadedDeviceRole } from '../../device-config.js';
|
|
3
4
|
export const claudeAdapter = {
|
|
4
5
|
id: 'claude',
|
|
5
6
|
applyExecConfigEnv(result, ctx) {
|
|
@@ -47,8 +48,8 @@ export const claudeAdapter = {
|
|
|
47
48
|
// path — agents.ts `isClaudeCredentialFileBlank`), so this path defers to
|
|
48
49
|
// Claude Code, which reads its own ACL-trusted login item without a prompt and
|
|
49
50
|
// asks a present human to log in only if the login is missing.
|
|
50
|
-
const
|
|
51
|
-
if (ctx.interactive ||
|
|
51
|
+
const headedDevice = isHeadedDeviceRole(ctx.deviceRole);
|
|
52
|
+
if (ctx.interactive || headedDevice) {
|
|
52
53
|
// Drop an INHERITED copy of OUR OWN setup-token: a launch from inside a
|
|
53
54
|
// headless agent's shell inherits that agent's injected value via
|
|
54
55
|
// sanitizeProcessEnv(process.env) and would keep authenticating as it,
|
|
@@ -177,6 +177,15 @@ export declare function buildWindowsAgentsCommand(cmd: WindowsAgentsCommand): st
|
|
|
177
177
|
* (Credential Manager, or the headless file store when there's no logon
|
|
178
178
|
* session), matching a local `agents secrets import`.
|
|
179
179
|
*/
|
|
180
|
+
/**
|
|
181
|
+
* Run `agents <args> --from <tmp>` on a Windows peer, feeding the ssh-piped stdin
|
|
182
|
+
* through a temp file — the `agents.ps1` shim does not forward piped stdin to the
|
|
183
|
+
* node process, so a verb that reads stdin must be handed a file instead. The
|
|
184
|
+
* generic sibling of {@link buildWindowsStdinImportCommand}; the receiving verb
|
|
185
|
+
* MUST accept `--from <path>` (see `usage-ingest.ts`). The temp file is removed in
|
|
186
|
+
* a `finally` so a throw mid-run never leaves the payload behind.
|
|
187
|
+
*/
|
|
188
|
+
export declare function buildWindowsStdinAgentsCommand(args: string[]): string;
|
|
180
189
|
export declare function buildWindowsStdinImportCommand(bundle: string, opts?: {
|
|
181
190
|
force?: boolean;
|
|
182
191
|
policyNever?: boolean;
|
|
@@ -323,6 +323,28 @@ export function buildWindowsAgentsCommand(cmd) {
|
|
|
323
323
|
* (Credential Manager, or the headless file store when there's no logon
|
|
324
324
|
* session), matching a local `agents secrets import`.
|
|
325
325
|
*/
|
|
326
|
+
/**
|
|
327
|
+
* Run `agents <args> --from <tmp>` on a Windows peer, feeding the ssh-piped stdin
|
|
328
|
+
* through a temp file — the `agents.ps1` shim does not forward piped stdin to the
|
|
329
|
+
* node process, so a verb that reads stdin must be handed a file instead. The
|
|
330
|
+
* generic sibling of {@link buildWindowsStdinImportCommand}; the receiving verb
|
|
331
|
+
* MUST accept `--from <path>` (see `usage-ingest.ts`). The temp file is removed in
|
|
332
|
+
* a `finally` so a throw mid-run never leaves the payload behind.
|
|
333
|
+
*/
|
|
334
|
+
export function buildWindowsStdinAgentsCommand(args) {
|
|
335
|
+
const forwarded = args.map(powershellQuote).join(' ');
|
|
336
|
+
const script = [
|
|
337
|
+
POWERSHELL_PROGRESS_SILENCE,
|
|
338
|
+
'$in = [Console]::In.ReadToEnd()',
|
|
339
|
+
'$tmp = $null',
|
|
340
|
+
`try { $tmp = [System.IO.Path]::GetTempFileName(); [System.IO.File]::WriteAllText($tmp, $in); ` +
|
|
341
|
+
`& agents ${forwarded} --from $tmp; $code = $LASTEXITCODE } ` +
|
|
342
|
+
`finally { if ($tmp) { Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue } }`,
|
|
343
|
+
'if ($null -eq $code) { $code = 1 }',
|
|
344
|
+
'exit $code',
|
|
345
|
+
].join('; ');
|
|
346
|
+
return `powershell -NoProfile -EncodedCommand ${encodePowershell(script)}`;
|
|
347
|
+
}
|
|
326
348
|
export function buildWindowsStdinImportCommand(bundle, opts = {}) {
|
|
327
349
|
const force = opts.force ? ' --force' : '';
|
|
328
350
|
const policy = opts.policyNever ? ' --policy never --i-understand' : '';
|
package/dist/lib/perf/db.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Disposable performance warehouse — SQLite under ~/.agents/.cache/perf/.
|
|
3
3
|
*
|
|
4
|
-
* Opened only by `agents perf` / `hooks profile` (read path). Writers use
|
|
4
|
+
* Opened only by `agents insights perf` / `hooks profile` (read path). Writers use
|
|
5
5
|
* {@link recordSample} in `./spool.ts` (NDJSON, no SQLite).
|
|
6
6
|
*/
|
|
7
7
|
import Database from '../sqlite.js';
|
package/dist/lib/perf/db.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Disposable performance warehouse — SQLite under ~/.agents/.cache/perf/.
|
|
3
3
|
*
|
|
4
|
-
* Opened only by `agents perf` / `hooks profile` (read path). Writers use
|
|
4
|
+
* Opened only by `agents insights perf` / `hooks profile` (read path). Writers use
|
|
5
5
|
* {@link recordSample} in `./spool.ts` (NDJSON, no SQLite).
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'fs';
|
|
@@ -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
|
|
@@ -830,18 +823,7 @@ export declare function sessionProcessIsLocal(s: Pick<ActiveSession, 'machine' |
|
|
|
830
823
|
*/
|
|
831
824
|
export declare function sessionProcessHost(s: Pick<ActiveSession, 'machine' | 'offloadedFrom'>, self: string): string | undefined;
|
|
832
825
|
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
|
-
*/
|
|
826
|
+
/** Labels win, then the last assistant line, then the first-prompt topic. */
|
|
845
827
|
export declare function deriveSessionRecap(row: Pick<ActiveSession, 'label' | 'topic' | 'tail'>): {
|
|
846
828
|
title?: string;
|
|
847
829
|
recapSource?: RecapSource;
|
|
@@ -851,17 +833,7 @@ export declare function deriveSessionRecap(row: Pick<ActiveSession, 'label' | 't
|
|
|
851
833
|
};
|
|
852
834
|
/** Fold the recap ladder onto every row (see {@link deriveSessionRecap}). */
|
|
853
835
|
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
|
-
*/
|
|
836
|
+
/** Reap only stale sessions with proven-dead pids; unknown or live processes remain recoverable. */
|
|
865
837
|
export declare function isReapableOrphan(row: Pick<ActiveSession, 'status' | 'pidAlive'>): boolean;
|
|
866
838
|
/**
|
|
867
839
|
* 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
|
}
|
|
@@ -476,18 +469,8 @@ export function isPidAlive(pid, startedAtMs) {
|
|
|
476
469
|
return true;
|
|
477
470
|
}
|
|
478
471
|
/**
|
|
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.
|
|
472
|
+
* Keep dead entries only when their window heartbeat also stopped, proving a crash;
|
|
473
|
+
* a live duplicate always wins.
|
|
491
474
|
*/
|
|
492
475
|
function readLiveTerminals() {
|
|
493
476
|
let raw;
|
|
@@ -541,20 +524,8 @@ function readLiveTerminals() {
|
|
|
541
524
|
const CLAUDE_SESSION_FILE_CACHE_MAX = 256;
|
|
542
525
|
const claudeSessionFileCache = new Map();
|
|
543
526
|
/**
|
|
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.
|
|
527
|
+
* Search every version home because the live ~/.claude symlink moves after upgrades
|
|
528
|
+
* while older running sessions keep writing to their original home.
|
|
558
529
|
*/
|
|
559
530
|
function findClaudeSessionFile(cwd, sessionId) {
|
|
560
531
|
// Only memoize when the exact session UUID is known. Without an id the
|
|
@@ -2080,17 +2051,7 @@ export function foldHostLink(rows) {
|
|
|
2080
2051
|
}
|
|
2081
2052
|
if (link === 'host-gone' && s.status === 'closed')
|
|
2082
2053
|
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.
|
|
2054
|
+
// A clientless running remote pane is normal; only stopped work can be called orphaned.
|
|
2094
2055
|
else if (link === 'no-client' && (s.status === 'idle' || s.status === 'input_required')) {
|
|
2095
2056
|
s.status = 'orphaned';
|
|
2096
2057
|
}
|
|
@@ -2103,18 +2064,7 @@ function recapLine(s, max = 120) {
|
|
|
2103
2064
|
return undefined;
|
|
2104
2065
|
return t.length > max ? t.slice(0, max - 1).trimEnd() + '…' : t;
|
|
2105
2066
|
}
|
|
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
|
-
*/
|
|
2067
|
+
/** Labels win, then the last assistant line, then the first-prompt topic. */
|
|
2118
2068
|
export function deriveSessionRecap(row) {
|
|
2119
2069
|
const lastAgentLine = recapLine(row.tail?.length ? row.tail[row.tail.length - 1] : undefined);
|
|
2120
2070
|
const { clean: userPromptClean, kind: userPromptKind } = classifyUserPrompt(row.topic ?? '');
|
|
@@ -2147,17 +2097,7 @@ export function foldRecap(rows) {
|
|
|
2147
2097
|
s.lastAgentLine = recap.lastAgentLine;
|
|
2148
2098
|
}
|
|
2149
2099
|
}
|
|
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
|
-
*/
|
|
2100
|
+
/** Reap only stale sessions with proven-dead pids; unknown or live processes remain recoverable. */
|
|
2161
2101
|
export function isReapableOrphan(row) {
|
|
2162
2102
|
return row.status === 'abandoned' && row.pidAlive === false;
|
|
2163
2103
|
}
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -567,46 +567,15 @@ interface TopCostSession {
|
|
|
567
567
|
* vanished, mirroring querySessions' liveness filter.
|
|
568
568
|
*/
|
|
569
569
|
export declare function topSessionsByCost(n: number, options?: QueryOptions): TopCostSession[];
|
|
570
|
-
/** Look up a single session by its unique ID. */
|
|
571
570
|
/**
|
|
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).
|
|
571
|
+
* Read machine attribution in batches without materializing full sessions.
|
|
572
|
+
* Failure is best-effort so the live view can still render local attribution.
|
|
579
573
|
*/
|
|
580
574
|
export declare function findSessionMachinesByIds(ids: string[]): Map<string, string>;
|
|
581
575
|
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
|
-
*/
|
|
576
|
+
/** Exact ids win over prefixes; the normal existence check preserves archived content but excludes phantoms. */
|
|
596
577
|
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
|
-
*/
|
|
578
|
+
/** Batch-resolve pane short ids; on collision the most recently active session wins. */
|
|
610
579
|
export declare function findSessionsByShortIds(shortIds: string[]): Map<string, SessionMeta>;
|
|
611
580
|
/** A single full-text search result with ranking score. */
|
|
612
581
|
interface FtsHit {
|
package/dist/lib/session/db.js
CHANGED
|
@@ -3390,15 +3390,9 @@ export function topSessionsByCost(n, options = {}) {
|
|
|
3390
3390
|
durationMs: r.duration_ms ?? 0,
|
|
3391
3391
|
}));
|
|
3392
3392
|
}
|
|
3393
|
-
/** Look up a single session by its unique ID. */
|
|
3394
3393
|
/**
|
|
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).
|
|
3394
|
+
* Read machine attribution in batches without materializing full sessions.
|
|
3395
|
+
* Failure is best-effort so the live view can still render local attribution.
|
|
3402
3396
|
*/
|
|
3403
3397
|
export function findSessionMachinesByIds(ids) {
|
|
3404
3398
|
const out = new Map();
|
|
@@ -3429,20 +3423,7 @@ export function getSessionById(id) {
|
|
|
3429
3423
|
const row = db.prepare(`SELECT * FROM sessions WHERE id = ?`).get(id);
|
|
3430
3424
|
return row ? rowToMeta(row) : null;
|
|
3431
3425
|
}
|
|
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
|
-
*/
|
|
3426
|
+
/** Exact ids win over prefixes; the normal existence check preserves archived content but excludes phantoms. */
|
|
3446
3427
|
export function findSessionsById(idQuery, scope = {}) {
|
|
3447
3428
|
const q = idQuery.trim();
|
|
3448
3429
|
if (!q)
|
|
@@ -3452,19 +3433,7 @@ export function findSessionsById(idQuery, scope = {}) {
|
|
|
3452
3433
|
return exact;
|
|
3453
3434
|
return querySessions({ ...scope, idPrefix: q });
|
|
3454
3435
|
}
|
|
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
|
-
*/
|
|
3436
|
+
/** Batch-resolve pane short ids; on collision the most recently active session wins. */
|
|
3468
3437
|
export function findSessionsByShortIds(shortIds) {
|
|
3469
3438
|
const out = new Map();
|
|
3470
3439
|
const uniq = [...new Set(shortIds.map((s) => s.trim().toLowerCase()).filter(Boolean))];
|
|
@@ -54,14 +54,7 @@ export interface DiscoverOptions {
|
|
|
54
54
|
idExact?: string;
|
|
55
55
|
/** Session id prefix — a targeted indexed lookup with no scan (RUSH-2477). */
|
|
56
56
|
idPrefix?: string;
|
|
57
|
-
/**
|
|
58
|
-
* Cold-miss repair: when another live process already holds the scan claim,
|
|
59
|
-
* wait (bounded) for that in-flight scan to finish before reading the index,
|
|
60
|
-
* instead of returning the pre-scan snapshot (RUSH-2682). A caller repairing a
|
|
61
|
-
* "not indexed yet" miss wants the fresh result the concurrent scan is about to
|
|
62
|
-
* write, not the stale read that just missed. Ignored when THIS process wins
|
|
63
|
-
* the claim (it scans itself) or no scan is in progress.
|
|
64
|
-
*/
|
|
57
|
+
/** On a cold miss, briefly await the scan already holding the single-flight claim. */
|
|
65
58
|
waitForScan?: boolean;
|
|
66
59
|
}
|
|
67
60
|
/** Progress report emitted during incremental scanning. */
|
|
@@ -181,41 +174,15 @@ export declare function discoverSessions(options?: DiscoverOptions): Promise<Ses
|
|
|
181
174
|
interface IncrementalScanResult {
|
|
182
175
|
/** True when this process won the single-flight claim and ran the scan. */
|
|
183
176
|
claimed: boolean;
|
|
184
|
-
/**
|
|
185
|
-
* Transcripts parsed this scan — i.e. those whose (mtime, size) changed. Zero
|
|
186
|
-
* is the steady state on an idle box and does NOT mean the scan was skipped;
|
|
187
|
-
* read `claimed` for that.
|
|
188
|
-
*
|
|
189
|
-
* Twelve of the 13 SESSION_AGENTS contribute, including OpenCode, whose
|
|
190
|
-
* scanner filters to sessions whose own per-session stamp changed and reports
|
|
191
|
-
* that batch — so a tick whose only changed sessions live there no longer
|
|
192
|
-
* reports 0 (RUSH-2691). OpenClaw is the exception and contributes nothing:
|
|
193
|
-
* its scanner has no change detection to report (a TTL gate, a fresh stamp
|
|
194
|
-
* every run, and an entry list rebuilt as the current inventory), so counting
|
|
195
|
-
* it would overstate rather than measure. See `scanOpenClawIncremental`.
|
|
196
|
-
*/
|
|
177
|
+
/** Changed transcripts parsed; zero with `claimed: true` is a successful no-op scan. */
|
|
197
178
|
scanned: number;
|
|
198
179
|
}
|
|
199
|
-
/**
|
|
200
|
-
* The write half of {@link discoverSessions}: claim the single-flight scan slot,
|
|
201
|
-
* incrementally index this host's transcript dirs, and report what was parsed.
|
|
202
|
-
*
|
|
203
|
-
* Split out so a caller that only wants the index refreshed — the daemon's warm
|
|
204
|
-
* tick — can run it WITHOUT the listing query `discoverSessions` ends with. That
|
|
205
|
-
* query is not free: it applies a cwd filter, runs the `archived_at`-writing
|
|
206
|
-
* existence check, and can issue a Linear fetch, none of which index anything
|
|
207
|
-
* (RUSH-2691). Keeping one implementation here is also what stops the tick and
|
|
208
|
-
* the foreground path from drifting apart.
|
|
209
|
-
*/
|
|
180
|
+
/** Separate write half so daemon warming does not pay for listing or external enrichment. */
|
|
210
181
|
export declare function scanSessionsIncremental(options?: {
|
|
211
182
|
agent?: SessionAgentId;
|
|
212
183
|
onProgress?: (p: ScanProgress) => void;
|
|
213
184
|
}): Promise<IncrementalScanResult>;
|
|
214
|
-
/**
|
|
215
|
-
* Poll until no live process holds the scan claim, or the bound elapses
|
|
216
|
-
* (RUSH-2682). Bounded so a wedged/slow scan can never hang a foreground preview.
|
|
217
|
-
* Exported for the cold-miss repair test.
|
|
218
|
-
*/
|
|
185
|
+
/** Bounded wait so a wedged scan cannot hang a foreground cold-miss repair. */
|
|
219
186
|
export declare function waitForScanToSettle(timeoutMs?: number, pollMs?: number): Promise<boolean>;
|
|
220
187
|
/** Read the current SQLite snapshot without scanning or parsing transcript files. */
|
|
221
188
|
export declare function queryIndexedSessions(options?: DiscoverOptions, indexedOptions?: {
|
|
@@ -223,27 +190,8 @@ export declare function queryIndexedSessions(options?: DiscoverOptions, indexedO
|
|
|
223
190
|
skipExistenceCheck?: boolean;
|
|
224
191
|
}): Promise<SessionMeta[]>;
|
|
225
192
|
/**
|
|
226
|
-
* Resolve
|
|
227
|
-
*
|
|
228
|
-
* A plain WAL read through `queryIndexedSessions` — same origin-machine
|
|
229
|
-
* attribution and managed scoping every indexed read gets — with NO incremental
|
|
230
|
-
* discovery scan (so none of `tryClaimScan`/`releaseScan`'s `BEGIN IMMEDIATE`
|
|
231
|
-
* writer lock) and NO fleet SSH fan-out. This is the crash-restart storm path
|
|
232
|
-
* (RUSH-2477): dozens of `sessions resume <id>` at once for a known local id must
|
|
233
|
-
* each be a cheap read, never a writer-lock contender or a dial into the
|
|
234
|
-
* not-yet-up tailnet. Exact id first, then prefix (matching `findSessionsById`),
|
|
235
|
-
* so a complete id never also drags in its prefix siblings. Returns `[]` on a
|
|
236
|
-
* genuine local miss, leaving the caller to fall back to the fleet resolver.
|
|
237
|
-
*
|
|
238
|
-
* The existence check is left ON (`skipExistenceCheck: false`), exactly as the old
|
|
239
|
-
* `discoverSessions` path and `findSessionsById` do (RUSH-2436): it KEEPS a
|
|
240
|
-
* file-gone session whose user turns still live in `session_text` (flagged
|
|
241
|
-
* archived) and SUPPRESSES a contentless phantom — so a phantom id misses here and
|
|
242
|
-
* falls through to the fleet resolver, instead of resolving to a row with no real
|
|
243
|
-
* transcript to resume. For a present transcript — the storm's actual case, since
|
|
244
|
-
* the crashed tabs' files are on disk — the check does no writes, so the lock-free
|
|
245
|
-
* guarantee holds; it only writes to (un)archive a genuinely missing or resurrected
|
|
246
|
-
* file, which is not the 20-at-once resume path.
|
|
193
|
+
* Resolve locally without scanning or fleet I/O, keeping concurrent crash recovery lock-light.
|
|
194
|
+
* The existence check preserves archived content while rejecting transcriptless phantoms.
|
|
247
195
|
*/
|
|
248
196
|
export declare function resolveIndexedSessionById(idQuery: string): Promise<SessionMeta[]>;
|
|
249
197
|
/**
|
|
@@ -131,12 +131,7 @@ async function applyJsonlAppend(filePath, fromOffset, wasDroppingOversizedLine,
|
|
|
131
131
|
* between `agents sessions` calls but is still being written to.
|
|
132
132
|
*/
|
|
133
133
|
const HOT_FILE_WINDOW_MS = 600_000;
|
|
134
|
-
/**
|
|
135
|
-
* Kill-switch: set `AGENTS_SESSIONS_NO_DIR_LEDGER=1` to force the old full-walk
|
|
136
|
-
* path (readdir + per-file stat every dir, every run — the pre-A-2 behavior),
|
|
137
|
-
* skipping the dir_ledger short-circuit entirely. One env var reverts a field
|
|
138
|
-
* regression to today's behavior.
|
|
139
|
-
*/
|
|
134
|
+
/** Emergency kill-switch for the directory-ledger optimization. */
|
|
140
135
|
function dirLedgerDisabled() {
|
|
141
136
|
const v = process.env.AGENTS_SESSIONS_NO_DIR_LEDGER;
|
|
142
137
|
return v === '1' || v === 'true';
|
|
@@ -175,17 +170,7 @@ export async function discoverSessions(options) {
|
|
|
175
170
|
skipExistenceCheck: options?.skipExistenceCheck ?? false,
|
|
176
171
|
});
|
|
177
172
|
}
|
|
178
|
-
/**
|
|
179
|
-
* The write half of {@link discoverSessions}: claim the single-flight scan slot,
|
|
180
|
-
* incrementally index this host's transcript dirs, and report what was parsed.
|
|
181
|
-
*
|
|
182
|
-
* Split out so a caller that only wants the index refreshed — the daemon's warm
|
|
183
|
-
* tick — can run it WITHOUT the listing query `discoverSessions` ends with. That
|
|
184
|
-
* query is not free: it applies a cwd filter, runs the `archived_at`-writing
|
|
185
|
-
* existence check, and can issue a Linear fetch, none of which index anything
|
|
186
|
-
* (RUSH-2691). Keeping one implementation here is also what stops the tick and
|
|
187
|
-
* the foreground path from drifting apart.
|
|
188
|
-
*/
|
|
173
|
+
/** Separate write half so daemon warming does not pay for listing or external enrichment. */
|
|
189
174
|
export async function scanSessionsIncremental(options) {
|
|
190
175
|
// Touch the DB so the schema is ready and connection is cached for this run.
|
|
191
176
|
getDB();
|
|
@@ -222,11 +207,7 @@ export async function scanSessionsIncremental(options) {
|
|
|
222
207
|
scanned += n;
|
|
223
208
|
return { claimed: true, scanned };
|
|
224
209
|
}
|
|
225
|
-
/**
|
|
226
|
-
* Poll until no live process holds the scan claim, or the bound elapses
|
|
227
|
-
* (RUSH-2682). Bounded so a wedged/slow scan can never hang a foreground preview.
|
|
228
|
-
* Exported for the cold-miss repair test.
|
|
229
|
-
*/
|
|
210
|
+
/** Bounded wait so a wedged scan cannot hang a foreground cold-miss repair. */
|
|
230
211
|
export async function waitForScanToSettle(timeoutMs = WAIT_FOR_SCAN_TIMEOUT_MS, pollMs = WAIT_FOR_SCAN_POLL_MS) {
|
|
231
212
|
const deadline = Date.now() + timeoutMs;
|
|
232
213
|
while (scanInProgressByLivePid()) {
|
|
@@ -259,27 +240,8 @@ export async function queryIndexedSessions(options, indexedOptions = {}) {
|
|
|
259
240
|
return scopeToManaged(sessions, agents, options);
|
|
260
241
|
}
|
|
261
242
|
/**
|
|
262
|
-
* Resolve
|
|
263
|
-
*
|
|
264
|
-
* A plain WAL read through `queryIndexedSessions` — same origin-machine
|
|
265
|
-
* attribution and managed scoping every indexed read gets — with NO incremental
|
|
266
|
-
* discovery scan (so none of `tryClaimScan`/`releaseScan`'s `BEGIN IMMEDIATE`
|
|
267
|
-
* writer lock) and NO fleet SSH fan-out. This is the crash-restart storm path
|
|
268
|
-
* (RUSH-2477): dozens of `sessions resume <id>` at once for a known local id must
|
|
269
|
-
* each be a cheap read, never a writer-lock contender or a dial into the
|
|
270
|
-
* not-yet-up tailnet. Exact id first, then prefix (matching `findSessionsById`),
|
|
271
|
-
* so a complete id never also drags in its prefix siblings. Returns `[]` on a
|
|
272
|
-
* genuine local miss, leaving the caller to fall back to the fleet resolver.
|
|
273
|
-
*
|
|
274
|
-
* The existence check is left ON (`skipExistenceCheck: false`), exactly as the old
|
|
275
|
-
* `discoverSessions` path and `findSessionsById` do (RUSH-2436): it KEEPS a
|
|
276
|
-
* file-gone session whose user turns still live in `session_text` (flagged
|
|
277
|
-
* archived) and SUPPRESSES a contentless phantom — so a phantom id misses here and
|
|
278
|
-
* falls through to the fleet resolver, instead of resolving to a row with no real
|
|
279
|
-
* transcript to resume. For a present transcript — the storm's actual case, since
|
|
280
|
-
* the crashed tabs' files are on disk — the check does no writes, so the lock-free
|
|
281
|
-
* guarantee holds; it only writes to (un)archive a genuinely missing or resurrected
|
|
282
|
-
* file, which is not the 20-at-once resume path.
|
|
243
|
+
* Resolve locally without scanning or fleet I/O, keeping concurrent crash recovery lock-light.
|
|
244
|
+
* The existence check preserves archived content while rejecting transcriptless phantoms.
|
|
283
245
|
*/
|
|
284
246
|
export async function resolveIndexedSessionById(idQuery) {
|
|
285
247
|
const q = idQuery.trim();
|