@phnx-labs/agents-cli 1.20.44 → 1.20.46
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 +12 -1
- package/dist/commands/exec.js +54 -11
- package/dist/commands/secrets.d.ts +18 -0
- package/dist/commands/secrets.js +105 -30
- package/dist/commands/teams.js +61 -3
- package/dist/index.js +14 -119
- package/dist/lib/daemon.js +9 -6
- package/dist/lib/hosts/dispatch.d.ts +29 -0
- package/dist/lib/hosts/dispatch.js +46 -1
- package/dist/lib/hosts/remote-cmd.d.ts +17 -0
- package/dist/lib/hosts/remote-cmd.js +27 -0
- package/dist/lib/hosts/session-index.d.ts +15 -0
- package/dist/lib/hosts/session-index.js +28 -2
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/rotate.d.ts +33 -0
- package/dist/lib/rotate.js +37 -0
- package/dist/lib/secrets/remote.d.ts +14 -0
- package/dist/lib/secrets/remote.js +18 -1
- package/dist/lib/self-heal/checks/path.d.ts +2 -0
- package/dist/lib/self-heal/checks/path.js +30 -0
- package/dist/lib/self-heal/checks/resources.d.ts +2 -0
- package/dist/lib/self-heal/checks/resources.js +36 -0
- package/dist/lib/self-heal/checks/shadowing.d.ts +2 -0
- package/dist/lib/self-heal/checks/shadowing.js +48 -0
- package/dist/lib/self-heal/checks/shims.d.ts +2 -0
- package/dist/lib/self-heal/checks/shims.js +35 -0
- package/dist/lib/self-heal/registry.d.ts +22 -0
- package/dist/lib/self-heal/registry.js +66 -0
- package/dist/lib/self-heal/types.d.ts +41 -0
- package/dist/lib/self-heal/types.js +21 -0
- package/dist/lib/session/active.d.ts +4 -0
- package/dist/lib/session/active.js +2 -0
- package/dist/lib/session/db.d.ts +16 -9
- package/dist/lib/session/db.js +66 -44
- package/dist/lib/session/discover.d.ts +4 -0
- package/dist/lib/session/discover.js +84 -13
- package/dist/lib/session/run-names.d.ts +9 -7
- package/dist/lib/session/run-names.js +9 -7
- package/dist/lib/session/state.d.ts +29 -3
- package/dist/lib/session/state.js +84 -5
- package/dist/lib/session/types.d.ts +19 -8
- package/dist/lib/shim-heal.d.ts +23 -0
- package/dist/lib/shim-heal.js +109 -0
- package/dist/lib/shims.d.ts +6 -0
- package/dist/lib/shims.js +1 -1
- package/dist/lib/teams/agents.js +9 -0
- package/package.json +1 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// shadowing check — when a harness's own launcher shadows our shim on PATH, adopt
|
|
2
|
+
// it (symlink-only, reversible) so version management wins regardless of PATH order.
|
|
3
|
+
// A REAL native binary is never moved — it's surfaced as needsAttention so the
|
|
4
|
+
// interactive layer can inform the user once. POSIX-only (the launcher convention
|
|
5
|
+
// and PATH-order problem are POSIX; Windows resolves via the registry PATH).
|
|
6
|
+
import { resultOf } from '../types.js';
|
|
7
|
+
import { AGENTS } from '../../agents.js';
|
|
8
|
+
import { getPathShadowingExecutable, adoptShadowingLauncher, listAgentsWithInstalledVersions, } from '../../shims.js';
|
|
9
|
+
import { getGlobalDefault } from '../../versions.js';
|
|
10
|
+
export const shadowingCheck = {
|
|
11
|
+
id: 'shadowing',
|
|
12
|
+
title: 'Launcher shadowing the version-managed shim',
|
|
13
|
+
platforms: ['darwin', 'linux'],
|
|
14
|
+
cadence: 'frequent',
|
|
15
|
+
async run(ctx) {
|
|
16
|
+
const fixed = [];
|
|
17
|
+
const needsAttention = [];
|
|
18
|
+
for (const agent of listAgentsWithInstalledVersions()) {
|
|
19
|
+
if (!getGlobalDefault(agent))
|
|
20
|
+
continue; // only default agents, like the interactive flow
|
|
21
|
+
const cmd = AGENTS[agent].cliCommand;
|
|
22
|
+
const shadowedBy = getPathShadowingExecutable(agent);
|
|
23
|
+
if (!shadowedBy)
|
|
24
|
+
continue;
|
|
25
|
+
if (ctx.dryRun) {
|
|
26
|
+
// Classify without mutating: adoption only ever touches a symlink.
|
|
27
|
+
let isSymlink = false;
|
|
28
|
+
try {
|
|
29
|
+
const fs = await import('node:fs');
|
|
30
|
+
isSymlink = fs.lstatSync(shadowedBy).isSymbolicLink();
|
|
31
|
+
}
|
|
32
|
+
catch { /* treat as real binary */ }
|
|
33
|
+
if (isSymlink)
|
|
34
|
+
fixed.push(`${cmd} launcher (${shadowedBy})`);
|
|
35
|
+
else
|
|
36
|
+
needsAttention.push(`${cmd}: real binary shadows the shim (${shadowedBy})`);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const res = adoptShadowingLauncher(agent);
|
|
40
|
+
if (res.adopted)
|
|
41
|
+
fixed.push(`adopted ${cmd} launcher (${res.launcher})`);
|
|
42
|
+
else if (res.reason === 'not-a-symlink') {
|
|
43
|
+
needsAttention.push(`${cmd}: real binary shadows the shim (${shadowedBy})`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return resultOf(fixed, needsAttention);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// shims check — keeps the dispatch shims and versioned aliases current, and clears
|
|
2
|
+
// pre-split legacy shim files. Formerly done in the interactive index.ts startup
|
|
3
|
+
// (which PRINTED "Updated <cli> shim" on every run); here it runs silently in the
|
|
4
|
+
// background so the shim schema settles without user-facing churn.
|
|
5
|
+
import { resultOf } from '../types.js';
|
|
6
|
+
import { AGENTS } from '../../agents.js';
|
|
7
|
+
import { ensureShimCurrent, ensureVersionedAliasCurrent, isShimCurrent, isVersionedAliasCurrent, removeLegacyUserShim, listAgentsWithInstalledVersions, } from '../../shims.js';
|
|
8
|
+
import { listInstalledVersions } from '../../versions.js';
|
|
9
|
+
export const shimsCheck = {
|
|
10
|
+
id: 'shims',
|
|
11
|
+
title: 'Dispatch shims + versioned aliases',
|
|
12
|
+
cadence: 'frequent',
|
|
13
|
+
async run(ctx) {
|
|
14
|
+
const fixed = [];
|
|
15
|
+
for (const agent of listAgentsWithInstalledVersions()) {
|
|
16
|
+
const cmd = AGENTS[agent].cliCommand;
|
|
17
|
+
if (!isShimCurrent(agent)) {
|
|
18
|
+
if (!ctx.dryRun)
|
|
19
|
+
ensureShimCurrent(agent);
|
|
20
|
+
fixed.push(`${cmd} shim`);
|
|
21
|
+
}
|
|
22
|
+
for (const version of listInstalledVersions(agent)) {
|
|
23
|
+
if (!isVersionedAliasCurrent(agent, version)) {
|
|
24
|
+
if (!ctx.dryRun)
|
|
25
|
+
ensureVersionedAliasCurrent(agent, version);
|
|
26
|
+
fixed.push(`${cmd}@${version} alias`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Pre-split ~/.agents/shims/<cli> files cause false-positive shadow hits.
|
|
30
|
+
if (!ctx.dryRun && removeLegacyUserShim(agent))
|
|
31
|
+
fixed.push(`removed legacy ${cmd} shim`);
|
|
32
|
+
}
|
|
33
|
+
return resultOf(fixed, []);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { HealCheck, HealCheckId, HealCadence, SelfHealReport } from './types.js';
|
|
2
|
+
export declare const HEAL_CHECKS: HealCheck[];
|
|
3
|
+
export interface SelfHealOptions {
|
|
4
|
+
/** Restrict to these check ids; omit to run every registered check. */
|
|
5
|
+
checks?: HealCheckId[];
|
|
6
|
+
/** Only run checks whose cadence is in this set (daemon scheduling). */
|
|
7
|
+
cadences?: HealCadence[];
|
|
8
|
+
/** 'safe' (daemon default) or 'full' (doctor --fix). Default 'safe'. */
|
|
9
|
+
mode?: 'safe' | 'full';
|
|
10
|
+
/** Detect only — never write. Default false. */
|
|
11
|
+
dryRun?: boolean;
|
|
12
|
+
/** Override the platform gate (tests). Default process.platform. */
|
|
13
|
+
platform?: NodeJS.Platform;
|
|
14
|
+
}
|
|
15
|
+
/** Run the selected checks, isolating per-check failures. */
|
|
16
|
+
export declare function runSelfHeal(opts?: SelfHealOptions): Promise<SelfHealReport>;
|
|
17
|
+
/** True if any check repaired something (for daemon logging / notification). */
|
|
18
|
+
export declare function selfHealChangedAnything(report: SelfHealReport): boolean;
|
|
19
|
+
/** True if any check surfaced something a human should look at. */
|
|
20
|
+
export declare function selfHealNeedsAttention(report: SelfHealReport): boolean;
|
|
21
|
+
/** One-line human summary, e.g. "shims: 2 fixed; path: 1 fixed". */
|
|
22
|
+
export declare function summarizeSelfHeal(report: SelfHealReport): string;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The self-heal registry + runner.
|
|
2
|
+
//
|
|
3
|
+
// One ordered list of HealChecks; one runner that executes the requested subset,
|
|
4
|
+
// isolating failures (one check throwing never aborts the rest) and aggregating a
|
|
5
|
+
// SelfHealReport. Both front doors — the daemon (by cadence) and `agents doctor`
|
|
6
|
+
// (all, or by id) — call runSelfHeal.
|
|
7
|
+
import { resourcesCheck } from './checks/resources.js';
|
|
8
|
+
import { shimsCheck } from './checks/shims.js';
|
|
9
|
+
import { shadowingCheck } from './checks/shadowing.js';
|
|
10
|
+
import { pathCheck } from './checks/path.js';
|
|
11
|
+
// Order matters: cheap structural fixes (shims, shadow adoption, PATH) before the
|
|
12
|
+
// heavier resource reconciliation, so a freshly-repaired shim is in place first.
|
|
13
|
+
export const HEAL_CHECKS = [
|
|
14
|
+
shimsCheck,
|
|
15
|
+
shadowingCheck,
|
|
16
|
+
pathCheck,
|
|
17
|
+
resourcesCheck,
|
|
18
|
+
];
|
|
19
|
+
/** Run the selected checks, isolating per-check failures. */
|
|
20
|
+
export async function runSelfHeal(opts = {}) {
|
|
21
|
+
const platform = opts.platform ?? process.platform;
|
|
22
|
+
const ctx = { mode: opts.mode ?? 'safe', dryRun: opts.dryRun ?? false };
|
|
23
|
+
const selected = HEAL_CHECKS.filter((c) => {
|
|
24
|
+
if (opts.checks && !opts.checks.includes(c.id))
|
|
25
|
+
return false;
|
|
26
|
+
if (opts.cadences && !opts.cadences.includes(c.cadence))
|
|
27
|
+
return false;
|
|
28
|
+
if (c.platforms && !c.platforms.includes(platform))
|
|
29
|
+
return false;
|
|
30
|
+
return true;
|
|
31
|
+
});
|
|
32
|
+
const reports = [];
|
|
33
|
+
for (const check of selected) {
|
|
34
|
+
try {
|
|
35
|
+
const result = await check.run(ctx);
|
|
36
|
+
reports.push({ id: check.id, title: check.title, result });
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
reports.push({ id: check.id, title: check.title, result: null, error: err.message });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { checks: reports };
|
|
43
|
+
}
|
|
44
|
+
/** True if any check repaired something (for daemon logging / notification). */
|
|
45
|
+
export function selfHealChangedAnything(report) {
|
|
46
|
+
return report.checks.some((c) => (c.result?.fixed.length ?? 0) > 0);
|
|
47
|
+
}
|
|
48
|
+
/** True if any check surfaced something a human should look at. */
|
|
49
|
+
export function selfHealNeedsAttention(report) {
|
|
50
|
+
return report.checks.some((c) => (c.result?.needsAttention.length ?? 0) > 0 || Boolean(c.error));
|
|
51
|
+
}
|
|
52
|
+
/** One-line human summary, e.g. "shims: 2 fixed; path: 1 fixed". */
|
|
53
|
+
export function summarizeSelfHeal(report) {
|
|
54
|
+
const parts = [];
|
|
55
|
+
for (const c of report.checks) {
|
|
56
|
+
if (c.error) {
|
|
57
|
+
parts.push(`${c.id}: error (${c.error})`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const n = c.result?.fixed.length ?? 0;
|
|
61
|
+
const a = c.result?.needsAttention.length ?? 0;
|
|
62
|
+
if (n > 0 || a > 0)
|
|
63
|
+
parts.push(`${c.id}: ${n} fixed${a > 0 ? `, ${a} to review` : ''}`);
|
|
64
|
+
}
|
|
65
|
+
return parts.join('; ') || 'nothing to heal';
|
|
66
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type HealCheckId = 'resources' | 'shims' | 'shadowing' | 'path';
|
|
2
|
+
/** When the daemon schedules a check. */
|
|
3
|
+
export type HealCadence = 'startup' | 'frequent' | 'periodic';
|
|
4
|
+
export interface HealCtx {
|
|
5
|
+
/** 'safe' = daemon (low-risk only); 'full' = doctor --fix (everything). */
|
|
6
|
+
mode: 'safe' | 'full';
|
|
7
|
+
/** Detect only — never write. Powers `agents doctor` (read-only) and previews. */
|
|
8
|
+
dryRun: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** Outcome of one check. `ok` means nothing was wrong. */
|
|
11
|
+
export interface CheckResult {
|
|
12
|
+
/** Things repaired (or, under dryRun, that WOULD be repaired). Human-readable. */
|
|
13
|
+
fixed: string[];
|
|
14
|
+
/** Detected but not auto-fixed: unfixable, or risky-in-safe-mode. Human-readable. */
|
|
15
|
+
needsAttention: string[];
|
|
16
|
+
/** True iff detect found nothing wrong (fixed and needsAttention both empty). */
|
|
17
|
+
ok: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface HealCheck {
|
|
20
|
+
id: HealCheckId;
|
|
21
|
+
title: string;
|
|
22
|
+
/** Restrict to these platforms; omit to run on all. */
|
|
23
|
+
platforms?: NodeJS.Platform[];
|
|
24
|
+
cadence: HealCadence;
|
|
25
|
+
/** Detect + (repair unless dryRun). Must be headless (no TTY/prompt) and idempotent. */
|
|
26
|
+
run(ctx: HealCtx): Promise<CheckResult>;
|
|
27
|
+
}
|
|
28
|
+
export interface CheckReport {
|
|
29
|
+
id: HealCheckId;
|
|
30
|
+
title: string;
|
|
31
|
+
result: CheckResult | null;
|
|
32
|
+
/** Set when the check itself threw (isolated — one check failing never aborts the run). */
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface SelfHealReport {
|
|
36
|
+
checks: CheckReport[];
|
|
37
|
+
}
|
|
38
|
+
/** Convenience: an all-clear result. */
|
|
39
|
+
export declare function okResult(): CheckResult;
|
|
40
|
+
/** Build a CheckResult from collected fixes/attention items (ok iff both empty). */
|
|
41
|
+
export declare function resultOf(fixed: string[], needsAttention: string[]): CheckResult;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Unified self-heal subsystem — shared shapes.
|
|
2
|
+
//
|
|
3
|
+
// agents-cli had ~37 separate repair routines scattered across the daemon, every
|
|
4
|
+
// CLI startup, and a handful of commands, each hand-rolling detect+fix on its own
|
|
5
|
+
// trigger. This subsystem gives every repairable class of problem ONE shape — a
|
|
6
|
+
// HealCheck — driven by ONE runner, hosted behind TWO front doors (the daemon,
|
|
7
|
+
// on tiered schedules, and `agents doctor`, on demand).
|
|
8
|
+
//
|
|
9
|
+
// A check's `run()` both detects and repairs in a single pass (repair is skipped
|
|
10
|
+
// when `ctx.dryRun`), mirroring the existing resource heal (heal.ts) which computes
|
|
11
|
+
// and applies together. `mode` gates how aggressive a repair may be: 'safe' (the
|
|
12
|
+
// daemon default) fixes only low-risk drift and merely reports risky conditions;
|
|
13
|
+
// 'full' (`agents doctor --fix`) applies everything.
|
|
14
|
+
/** Convenience: an all-clear result. */
|
|
15
|
+
export function okResult() {
|
|
16
|
+
return { fixed: [], needsAttention: [], ok: true };
|
|
17
|
+
}
|
|
18
|
+
/** Build a CheckResult from collected fixes/attention items (ok iff both empty). */
|
|
19
|
+
export function resultOf(fixed, needsAttention) {
|
|
20
|
+
return { fixed, needsAttention, ok: fixed.length === 0 && needsAttention.length === 0 };
|
|
21
|
+
}
|
|
@@ -36,6 +36,10 @@ export interface ActiveSession {
|
|
|
36
36
|
worktree?: DetectedWorktree;
|
|
37
37
|
/** Tracker ticket the session is tied to. */
|
|
38
38
|
ticket?: DetectedTicket;
|
|
39
|
+
/** Tracker refs the session CREATED (Linear create_issue / gh issue create). */
|
|
40
|
+
createdTickets?: string[];
|
|
41
|
+
/** Team name the session SPAWNED via `agents teams create/add`. */
|
|
42
|
+
spawnedTeam?: string;
|
|
39
43
|
sessionFile?: string;
|
|
40
44
|
startedAtMs?: number;
|
|
41
45
|
status: ActiveStatus;
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -22,7 +22,6 @@ export interface SessionRow {
|
|
|
22
22
|
git_branch: string | null;
|
|
23
23
|
topic: string | null;
|
|
24
24
|
label: string | null;
|
|
25
|
-
name: string | null;
|
|
26
25
|
message_count: number | null;
|
|
27
26
|
token_count: number | null;
|
|
28
27
|
cost_usd: number | null;
|
|
@@ -130,15 +129,23 @@ export declare function upsertSessionsBatch(entries: Array<{
|
|
|
130
129
|
*/
|
|
131
130
|
export declare function syncLabels(labelMap: Map<string, string | null>): number;
|
|
132
131
|
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
132
|
+
* Seed session labels from `agents run --name` handles, keyed by session id.
|
|
133
|
+
*
|
|
134
|
+
* `--name` is the universal launch-time way to set a session's label — the same
|
|
135
|
+
* field an agent later refines with a generated title (`syncLabels`) or the user
|
|
136
|
+
* with `/rename`. The seed's source of truth lives outside the transcript (host
|
|
137
|
+
* task sidecars, run-name sidecars written at launch), so it is re-applied by id
|
|
138
|
+
* every scan rather than parsed per-file. It only fills a label that is still
|
|
139
|
+
* EMPTY — an agent-generated title always wins over the seed, so a Claude run's
|
|
140
|
+
* `--name` shows until Claude titles it, and a non-Claude run keeps its `--name`
|
|
141
|
+
* as the label. Writes both `sessions.label` and the FTS5 label column so a
|
|
142
|
+
* seeded name is fuzzy-searchable. Cheap to call every run; returns rows updated.
|
|
143
|
+
*
|
|
144
|
+
* Ordering matters: this runs AFTER the per-agent scans (which apply
|
|
145
|
+
* agent-generated titles via {@link syncLabels}), so it never overwrites a real
|
|
146
|
+
* title — it only backfills the gap the seed was meant to cover.
|
|
140
147
|
*/
|
|
141
|
-
export declare function
|
|
148
|
+
export declare function seedLabelsFromNames(nameMap: Map<string, string | null>): number;
|
|
142
149
|
/**
|
|
143
150
|
* Sync topics (session titles) for a set of sessions, keyed by id. For agents
|
|
144
151
|
* whose human-readable title lives in a side index that updates independently
|
package/dist/lib/session/db.js
CHANGED
|
@@ -13,7 +13,7 @@ import { getSessionsDir, getSessionsDbPath } from '../state.js';
|
|
|
13
13
|
const SESSIONS_DIR = getSessionsDir();
|
|
14
14
|
const DB_PATH = getSessionsDbPath();
|
|
15
15
|
/** Current schema version; bumped when migrations are added. */
|
|
16
|
-
const SCHEMA_VERSION =
|
|
16
|
+
const SCHEMA_VERSION = 10;
|
|
17
17
|
/**
|
|
18
18
|
* Canonicalize a file path for use as a scan_ledger key. The same physical
|
|
19
19
|
* session file is reachable via multiple aliases — `~/.claude/projects/x.jsonl`
|
|
@@ -52,7 +52,6 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
52
52
|
git_branch TEXT,
|
|
53
53
|
topic TEXT,
|
|
54
54
|
label TEXT,
|
|
55
|
-
name TEXT,
|
|
56
55
|
message_count INTEGER,
|
|
57
56
|
token_count INTEGER,
|
|
58
57
|
cost_usd REAL,
|
|
@@ -198,6 +197,22 @@ function migrateSchema(db, fromVersion) {
|
|
|
198
197
|
if (!cols.some(c => c.name === 'name'))
|
|
199
198
|
db.exec(`ALTER TABLE sessions ADD COLUMN name TEXT`);
|
|
200
199
|
}
|
|
200
|
+
if (fromVersion < 10) {
|
|
201
|
+
// v9 → v10: `name` and `label` unify into a single `label`. `--name` now
|
|
202
|
+
// SEEDS the label at launch (refined later by an agent-generated title)
|
|
203
|
+
// instead of living in a separate immutable `name` column. Fold any existing
|
|
204
|
+
// name into label where the label is empty, mirror it into the FTS row, then
|
|
205
|
+
// drop the redundant column. Seeds re-apply from the run-name sidecars every
|
|
206
|
+
// scan (seedLabelsFromNames), so no rescan is required.
|
|
207
|
+
const cols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
208
|
+
if (cols.some(c => c.name === 'name')) {
|
|
209
|
+
db.exec(`UPDATE sessions SET label = name
|
|
210
|
+
WHERE (label IS NULL OR label = '') AND name IS NOT NULL AND name != ''`);
|
|
211
|
+
db.exec(`UPDATE session_text SET label = COALESCE(
|
|
212
|
+
(SELECT label FROM sessions WHERE sessions.id = session_text.session_id), '')`);
|
|
213
|
+
db.exec(`ALTER TABLE sessions DROP COLUMN name`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
201
216
|
}
|
|
202
217
|
/** Open (or return the cached) sessions database, applying migrations as needed. */
|
|
203
218
|
export function getDB() {
|
|
@@ -411,13 +426,13 @@ export function recordScans(entries) {
|
|
|
411
426
|
const upsertSessionStmt = (db) => db.prepare(`
|
|
412
427
|
INSERT INTO sessions (
|
|
413
428
|
id, short_id, agent, version, account, timestamp, last_activity,
|
|
414
|
-
project, cwd, git_branch, topic, label,
|
|
429
|
+
project, cwd, git_branch, topic, label, message_count, token_count,
|
|
415
430
|
cost_usd, duration_ms,
|
|
416
431
|
file_path, file_mtime_ms, file_size, scanned_at, is_team_origin,
|
|
417
432
|
pr_url, pr_number, worktree_slug, ticket_id
|
|
418
433
|
) VALUES (
|
|
419
434
|
@id, @short_id, @agent, @version, @account, @timestamp, @last_activity,
|
|
420
|
-
@project, @cwd, @git_branch, @topic, @label, @
|
|
435
|
+
@project, @cwd, @git_branch, @topic, @label, @message_count, @token_count,
|
|
421
436
|
@cost_usd, @duration_ms,
|
|
422
437
|
@file_path, @file_mtime_ms, @file_size, @scanned_at, @is_team_origin,
|
|
423
438
|
@pr_url, @pr_number, @worktree_slug, @ticket_id
|
|
@@ -481,7 +496,6 @@ export function upsertSession(meta, content, scan) {
|
|
|
481
496
|
git_branch: meta.gitBranch ?? null,
|
|
482
497
|
topic: meta.topic ?? null,
|
|
483
498
|
label: meta.label ?? null,
|
|
484
|
-
name: meta.name ?? null,
|
|
485
499
|
message_count: meta.messageCount ?? null,
|
|
486
500
|
token_count: meta.tokenCount ?? null,
|
|
487
501
|
cost_usd: meta.costUsd ?? null,
|
|
@@ -570,7 +584,6 @@ export function upsertSessionsBatch(entries) {
|
|
|
570
584
|
git_branch: meta.gitBranch ?? null,
|
|
571
585
|
topic: meta.topic ?? null,
|
|
572
586
|
label: meta.label ?? null,
|
|
573
|
-
name: meta.name ?? null,
|
|
574
587
|
message_count: meta.messageCount ?? null,
|
|
575
588
|
token_count: meta.tokenCount ?? null,
|
|
576
589
|
cost_usd: meta.costUsd ?? null,
|
|
@@ -639,15 +652,23 @@ export function syncLabels(labelMap) {
|
|
|
639
652
|
return updates.length;
|
|
640
653
|
}
|
|
641
654
|
/**
|
|
642
|
-
*
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
655
|
+
* Seed session labels from `agents run --name` handles, keyed by session id.
|
|
656
|
+
*
|
|
657
|
+
* `--name` is the universal launch-time way to set a session's label — the same
|
|
658
|
+
* field an agent later refines with a generated title (`syncLabels`) or the user
|
|
659
|
+
* with `/rename`. The seed's source of truth lives outside the transcript (host
|
|
660
|
+
* task sidecars, run-name sidecars written at launch), so it is re-applied by id
|
|
661
|
+
* every scan rather than parsed per-file. It only fills a label that is still
|
|
662
|
+
* EMPTY — an agent-generated title always wins over the seed, so a Claude run's
|
|
663
|
+
* `--name` shows until Claude titles it, and a non-Claude run keeps its `--name`
|
|
664
|
+
* as the label. Writes both `sessions.label` and the FTS5 label column so a
|
|
665
|
+
* seeded name is fuzzy-searchable. Cheap to call every run; returns rows updated.
|
|
666
|
+
*
|
|
667
|
+
* Ordering matters: this runs AFTER the per-agent scans (which apply
|
|
668
|
+
* agent-generated titles via {@link syncLabels}), so it never overwrites a real
|
|
669
|
+
* title — it only backfills the gap the seed was meant to cover.
|
|
649
670
|
*/
|
|
650
|
-
export function
|
|
671
|
+
export function seedLabelsFromNames(nameMap) {
|
|
651
672
|
if (nameMap.size === 0)
|
|
652
673
|
return 0;
|
|
653
674
|
const db = getDB();
|
|
@@ -658,21 +679,25 @@ export function syncNames(nameMap) {
|
|
|
658
679
|
const chunk = ids.slice(i, i + CHUNK);
|
|
659
680
|
const placeholders = chunk.map(() => '?').join(',');
|
|
660
681
|
const rows = db
|
|
661
|
-
.prepare(`SELECT id,
|
|
682
|
+
.prepare(`SELECT id, label FROM sessions WHERE id IN (${placeholders})`)
|
|
662
683
|
.all(...chunk);
|
|
663
684
|
for (const row of rows) {
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
685
|
+
const seed = nameMap.get(row.id);
|
|
686
|
+
// Only fill an empty label; a real agent title (non-empty) always wins.
|
|
687
|
+
if (seed && !(row.label ?? '').trim()) {
|
|
688
|
+
updates.push({ id: row.id, label: seed });
|
|
667
689
|
}
|
|
668
690
|
}
|
|
669
691
|
}
|
|
670
692
|
if (updates.length === 0)
|
|
671
693
|
return 0;
|
|
672
|
-
const
|
|
694
|
+
const updSessions = db.prepare(`UPDATE sessions SET label = ? WHERE id = ?`);
|
|
695
|
+
const updFts = db.prepare(`UPDATE session_text SET label = ? WHERE session_id = ?`);
|
|
673
696
|
const txn = db.transaction((items) => {
|
|
674
|
-
for (const { id,
|
|
675
|
-
|
|
697
|
+
for (const { id, label } of items) {
|
|
698
|
+
updSessions.run(label, id);
|
|
699
|
+
updFts.run(label, id);
|
|
700
|
+
}
|
|
676
701
|
});
|
|
677
702
|
txn(updates);
|
|
678
703
|
return updates.length;
|
|
@@ -739,7 +764,6 @@ function rowToMeta(row) {
|
|
|
739
764
|
account: row.account ?? undefined,
|
|
740
765
|
topic: row.topic ?? undefined,
|
|
741
766
|
label: row.label ?? undefined,
|
|
742
|
-
name: row.name ?? undefined,
|
|
743
767
|
isTeamOrigin: row.is_team_origin === 1,
|
|
744
768
|
prUrl: row.pr_url ?? undefined,
|
|
745
769
|
prNumber: row.pr_number ?? undefined,
|
|
@@ -1009,33 +1033,31 @@ export function ftsSearch(input, limit = 200) {
|
|
|
1009
1033
|
const seen = new Set();
|
|
1010
1034
|
const hits = [];
|
|
1011
1035
|
// Tier 1-3: handle-based matches, ordered by exactness. A session's handle is
|
|
1012
|
-
// its
|
|
1013
|
-
//
|
|
1014
|
-
//
|
|
1036
|
+
// its `label` — set by an agent title / `/rename`, or seeded at launch from
|
|
1037
|
+
// `agents run --name`. Typing it resolves the session ahead of any FTS content
|
|
1038
|
+
// hit.
|
|
1015
1039
|
const labelRows = db.prepare(`
|
|
1016
|
-
SELECT id, label
|
|
1017
|
-
WHERE
|
|
1018
|
-
|
|
1019
|
-
`).all(`%${lower}%`, `%${lower}%`);
|
|
1040
|
+
SELECT id, label FROM sessions
|
|
1041
|
+
WHERE label IS NOT NULL AND LOWER(label) LIKE ?
|
|
1042
|
+
`).all(`%${lower}%`);
|
|
1020
1043
|
let hasExactLabelMatch = false;
|
|
1021
1044
|
for (const row of labelRows) {
|
|
1022
|
-
// Score
|
|
1045
|
+
// Score the label by match quality (exact > prefix > contains).
|
|
1023
1046
|
let score = 0;
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
continue;
|
|
1047
|
+
const handle = row.label;
|
|
1048
|
+
if (handle) {
|
|
1027
1049
|
const h = handle.toLowerCase();
|
|
1028
|
-
if (
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1050
|
+
if (h.includes(lower)) {
|
|
1051
|
+
if (h === lower) {
|
|
1052
|
+
score = 1_000_000;
|
|
1053
|
+
hasExactLabelMatch = true;
|
|
1054
|
+
}
|
|
1055
|
+
else if (h.startsWith(lower)) {
|
|
1056
|
+
score = 900_000;
|
|
1057
|
+
}
|
|
1058
|
+
else {
|
|
1059
|
+
score = 800_000;
|
|
1060
|
+
}
|
|
1039
1061
|
}
|
|
1040
1062
|
}
|
|
1041
1063
|
if (score === 0)
|
|
@@ -64,6 +64,10 @@ interface ClaudeSessionScan {
|
|
|
64
64
|
prNumber?: number;
|
|
65
65
|
worktreeSlug?: string;
|
|
66
66
|
ticketId?: string;
|
|
67
|
+
/** Tracker refs the session CREATED (Linear create_issue / gh issue create). */
|
|
68
|
+
createdTickets?: string[];
|
|
69
|
+
/** Team name this session SPAWNED via `agents teams create/add` (not team-of-origin). */
|
|
70
|
+
spawnedTeam?: string;
|
|
67
71
|
}
|
|
68
72
|
/**
|
|
69
73
|
* Discover sessions. Scans only files whose (mtime, size) have changed since
|