@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/dist/commands/exec.js +54 -11
  3. package/dist/commands/secrets.d.ts +18 -0
  4. package/dist/commands/secrets.js +105 -30
  5. package/dist/commands/teams.js +61 -3
  6. package/dist/index.js +14 -119
  7. package/dist/lib/daemon.js +9 -6
  8. package/dist/lib/hosts/dispatch.d.ts +29 -0
  9. package/dist/lib/hosts/dispatch.js +46 -1
  10. package/dist/lib/hosts/remote-cmd.d.ts +17 -0
  11. package/dist/lib/hosts/remote-cmd.js +27 -0
  12. package/dist/lib/hosts/session-index.d.ts +15 -0
  13. package/dist/lib/hosts/session-index.js +28 -2
  14. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  15. package/dist/lib/rotate.d.ts +33 -0
  16. package/dist/lib/rotate.js +37 -0
  17. package/dist/lib/secrets/remote.d.ts +14 -0
  18. package/dist/lib/secrets/remote.js +18 -1
  19. package/dist/lib/self-heal/checks/path.d.ts +2 -0
  20. package/dist/lib/self-heal/checks/path.js +30 -0
  21. package/dist/lib/self-heal/checks/resources.d.ts +2 -0
  22. package/dist/lib/self-heal/checks/resources.js +36 -0
  23. package/dist/lib/self-heal/checks/shadowing.d.ts +2 -0
  24. package/dist/lib/self-heal/checks/shadowing.js +48 -0
  25. package/dist/lib/self-heal/checks/shims.d.ts +2 -0
  26. package/dist/lib/self-heal/checks/shims.js +35 -0
  27. package/dist/lib/self-heal/registry.d.ts +22 -0
  28. package/dist/lib/self-heal/registry.js +66 -0
  29. package/dist/lib/self-heal/types.d.ts +41 -0
  30. package/dist/lib/self-heal/types.js +21 -0
  31. package/dist/lib/session/active.d.ts +4 -0
  32. package/dist/lib/session/active.js +2 -0
  33. package/dist/lib/session/db.d.ts +16 -9
  34. package/dist/lib/session/db.js +66 -44
  35. package/dist/lib/session/discover.d.ts +4 -0
  36. package/dist/lib/session/discover.js +84 -13
  37. package/dist/lib/session/run-names.d.ts +9 -7
  38. package/dist/lib/session/run-names.js +9 -7
  39. package/dist/lib/session/state.d.ts +29 -3
  40. package/dist/lib/session/state.js +84 -5
  41. package/dist/lib/session/types.d.ts +19 -8
  42. package/dist/lib/shim-heal.d.ts +23 -0
  43. package/dist/lib/shim-heal.js +109 -0
  44. package/dist/lib/shims.d.ts +6 -0
  45. package/dist/lib/shims.js +1 -1
  46. package/dist/lib/teams/agents.js +9 -0
  47. 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,2 @@
1
+ import type { HealCheck } from '../types.js';
2
+ export declare const shimsCheck: HealCheck;
@@ -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;
@@ -241,6 +241,8 @@ function applyState(base, state, fallbackFile) {
241
241
  pr: state.pr,
242
242
  worktree: state.worktree,
243
243
  ticket: state.ticket,
244
+ createdTickets: state.createdTickets,
245
+ spawnedTeam: state.spawnedTeam,
244
246
  };
245
247
  }
246
248
  /**
@@ -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
- * Sync `agents run --name` handles for a set of sessions, keyed by session id.
134
- * The name's source of truth lives outside the transcript (host task sidecars,
135
- * run-name sidecars written at launch), so like {@link syncLabels}it is
136
- * re-applied by id every scan rather than parsed per-file. Updates only
137
- * `sessions.name` (names resolve via a direct column tier in ftsSearch, not
138
- * FTS, so there's no session_text column to touch). Only writes when the value
139
- * differs; cheap to call every run. Returns the number of rows updated.
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 syncNames(nameMap: Map<string, string | null>): number;
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
@@ -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 = 9;
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, name, message_count, token_count,
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, @name, @message_count, @token_count,
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
- * Sync `agents run --name` handles for a set of sessions, keyed by session id.
643
- * The name's source of truth lives outside the transcript (host task sidecars,
644
- * run-name sidecars written at launch), so like {@link syncLabels}it is
645
- * re-applied by id every scan rather than parsed per-file. Updates only
646
- * `sessions.name` (names resolve via a direct column tier in ftsSearch, not
647
- * FTS, so there's no session_text column to touch). Only writes when the value
648
- * differs; cheap to call every run. Returns the number of rows updated.
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 syncNames(nameMap) {
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, name FROM sessions WHERE id IN (${placeholders})`)
682
+ .prepare(`SELECT id, label FROM sessions WHERE id IN (${placeholders})`)
662
683
  .all(...chunk);
663
684
  for (const row of rows) {
664
- const live = nameMap.get(row.id) ?? null;
665
- if ((live ?? '') !== (row.name ?? '')) {
666
- updates.push({ id: row.id, name: live });
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 upd = db.prepare(`UPDATE sessions SET name = ? WHERE id = ?`);
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, name } of items)
675
- upd.run(name, id);
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 /rename `label` OR its `agents run --name` handle both are user-chosen
1013
- // aliases and rank identically, so typing either the renamed title or the run
1014
- // name resolves the session ahead of any FTS content hit.
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, name FROM sessions
1017
- WHERE (label IS NOT NULL AND LOWER(label) LIKE ?)
1018
- OR (name IS NOT NULL AND LOWER(name) LIKE ?)
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 against whichever handle matches best (exact > prefix > contains).
1045
+ // Score the label by match quality (exact > prefix > contains).
1023
1046
  let score = 0;
1024
- for (const handle of [row.label, row.name]) {
1025
- if (!handle)
1026
- continue;
1047
+ const handle = row.label;
1048
+ if (handle) {
1027
1049
  const h = handle.toLowerCase();
1028
- if (!h.includes(lower))
1029
- continue;
1030
- if (h === lower) {
1031
- score = Math.max(score, 1_000_000);
1032
- hasExactLabelMatch = true;
1033
- }
1034
- else if (h.startsWith(lower)) {
1035
- score = Math.max(score, 900_000);
1036
- }
1037
- else {
1038
- score = Math.max(score, 800_000);
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