@phnx-labs/agents-cli 1.22.75 → 1.22.76

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 (132) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/README.md +20 -8
  3. package/dist/bootstrap.js +7 -7
  4. package/dist/cli/command-registry.js +5 -0
  5. package/dist/commands/artifacts-setup.js +1 -1
  6. package/dist/commands/artifacts.js +1 -1
  7. package/dist/commands/auth.js +7 -1
  8. package/dist/commands/browser.js +104 -10
  9. package/dist/commands/commands.js +7 -6
  10. package/dist/commands/config.js +27 -4
  11. package/dist/commands/cost.js +6 -4
  12. package/dist/commands/doctor.d.ts +6 -5
  13. package/dist/commands/doctor.js +32 -274
  14. package/dist/commands/exec.d.ts +2 -0
  15. package/dist/commands/exec.js +9 -2
  16. package/dist/commands/harness.d.ts +1 -0
  17. package/dist/commands/harness.js +11 -3
  18. package/dist/commands/hooks.js +7 -6
  19. package/dist/commands/mcp.js +7 -6
  20. package/dist/commands/memory.js +7 -7
  21. package/dist/commands/monitors.js +3 -2
  22. package/dist/commands/open.d.ts +25 -12
  23. package/dist/commands/open.js +24 -10
  24. package/dist/commands/permissions.js +7 -6
  25. package/dist/commands/plugins.js +21 -17
  26. package/dist/commands/route.js +33 -16
  27. package/dist/commands/rules.js +7 -12
  28. package/dist/commands/sessions-share.js +1 -1
  29. package/dist/commands/setup-watchdog.js +2 -2
  30. package/dist/commands/setup.js +22 -1
  31. package/dist/commands/share.js +26 -10
  32. package/dist/commands/skills.js +7 -6
  33. package/dist/commands/subagents.js +7 -6
  34. package/dist/commands/sync.js +81 -10
  35. package/dist/commands/view.js +4 -1
  36. package/dist/commands/watchdog.d.ts +1 -1
  37. package/dist/commands/watchdog.js +10 -10
  38. package/dist/commands/webhook.d.ts +4 -0
  39. package/dist/commands/webhook.js +22 -4
  40. package/dist/commands/workflows.js +7 -6
  41. package/dist/lib/accounting/rotate.d.ts +3 -1
  42. package/dist/lib/accounting/rotate.js +8 -4
  43. package/dist/lib/auth-health.d.ts +2 -0
  44. package/dist/lib/auth-health.js +2 -0
  45. package/dist/lib/browser/chrome.d.ts +21 -0
  46. package/dist/lib/browser/chrome.js +60 -3
  47. package/dist/lib/browser/drivers/local.d.ts +21 -0
  48. package/dist/lib/browser/drivers/local.js +102 -9
  49. package/dist/lib/browser/profiles.d.ts +29 -1
  50. package/dist/lib/browser/profiles.js +50 -1
  51. package/dist/lib/browser/types.d.ts +18 -0
  52. package/dist/lib/config-keys.d.ts +7 -2
  53. package/dist/lib/config-keys.js +17 -2
  54. package/dist/lib/daemon/daemon.js +8 -0
  55. package/dist/lib/daemon/session-summarizer-service.d.ts +24 -0
  56. package/dist/lib/daemon/session-summarizer-service.js +39 -0
  57. package/dist/lib/daemon-services.d.ts +1 -1
  58. package/dist/lib/daemon-services.js +5 -0
  59. package/dist/lib/daemon-ticks.d.ts +2 -2
  60. package/dist/lib/daemon-ticks.js +2 -1
  61. package/dist/lib/deeplink/register.js +10 -9
  62. package/dist/lib/deeplink/url.d.ts +4 -4
  63. package/dist/lib/deeplink/url.js +4 -4
  64. package/dist/lib/device-config.js +25 -0
  65. package/dist/lib/devices/doctor-findings.d.ts +4 -4
  66. package/dist/lib/devices/doctor-findings.js +14 -8
  67. package/dist/lib/devices/registry.js +2 -0
  68. package/dist/lib/devices/stats-cache.d.ts +4 -0
  69. package/dist/lib/devices/stats-cache.js +19 -0
  70. package/dist/lib/drift-sync.d.ts +3 -1
  71. package/dist/lib/drift-sync.js +16 -5
  72. package/dist/lib/exec.d.ts +2 -0
  73. package/dist/lib/exec.js +16 -1
  74. package/dist/lib/fleet-shared-state.d.ts +8 -0
  75. package/dist/lib/heal.d.ts +4 -3
  76. package/dist/lib/heal.js +5 -4
  77. package/dist/lib/hosts/ready.d.ts +1 -1
  78. package/dist/lib/hosts/ready.js +16 -4
  79. package/dist/lib/hosts/reconnect.js +4 -2
  80. package/dist/lib/identity/client.d.ts +6 -0
  81. package/dist/lib/identity/index.d.ts +16 -0
  82. package/dist/lib/identity/index.js +25 -1
  83. package/dist/lib/profiles.d.ts +2 -0
  84. package/dist/lib/profiles.js +28 -9
  85. package/dist/lib/reconcile-and-repair.d.ts +109 -0
  86. package/dist/lib/reconcile-and-repair.js +267 -0
  87. package/dist/lib/routers.d.ts +12 -1
  88. package/dist/lib/routers.js +30 -1
  89. package/dist/lib/scheduling/routines.js +8 -2
  90. package/dist/lib/session/active.d.ts +13 -0
  91. package/dist/lib/session/db.d.ts +47 -7
  92. package/dist/lib/session/db.js +114 -12
  93. package/dist/lib/session/mirror.js +58 -0
  94. package/dist/lib/session/remote/watch.js +22 -2
  95. package/dist/lib/session/session-cache.d.ts +19 -0
  96. package/dist/lib/session/session-cache.js +46 -0
  97. package/dist/lib/session/types.d.ts +34 -0
  98. package/dist/lib/share/backend.d.ts +6 -4
  99. package/dist/lib/share/backend.js +10 -8
  100. package/dist/lib/share/config.d.ts +4 -3
  101. package/dist/lib/share/config.js +10 -1
  102. package/dist/lib/share/delete.d.ts +1 -1
  103. package/dist/lib/share/delete.js +1 -1
  104. package/dist/lib/share/html.d.ts +1 -1
  105. package/dist/lib/share/html.js +1 -1
  106. package/dist/lib/share/provision.d.ts +1 -1
  107. package/dist/lib/share/provision.js +2 -2
  108. package/dist/lib/share/publish.d.ts +23 -7
  109. package/dist/lib/share/publish.js +58 -12
  110. package/dist/lib/share/worker-template.js +221 -60
  111. package/dist/lib/startup/command-registry.js +2 -2
  112. package/dist/lib/state.d.ts +15 -0
  113. package/dist/lib/state.js +29 -7
  114. package/dist/lib/summarizer/config.d.ts +46 -0
  115. package/dist/lib/summarizer/config.js +83 -0
  116. package/dist/lib/summarizer/pass.d.ts +45 -0
  117. package/dist/lib/summarizer/pass.js +112 -0
  118. package/dist/lib/summarizer/summarize.d.ts +68 -0
  119. package/dist/lib/summarizer/summarize.js +120 -0
  120. package/dist/lib/teams/agents.d.ts +4 -3
  121. package/dist/lib/teams/agents.js +12 -4
  122. package/dist/lib/teams/scheduler.d.ts +4 -2
  123. package/dist/lib/teams/scheduler.js +6 -6
  124. package/dist/lib/tmux/session.d.ts +2 -0
  125. package/dist/lib/tmux/session.js +7 -1
  126. package/dist/lib/types.d.ts +20 -0
  127. package/dist/lib/verbs.d.ts +23 -0
  128. package/dist/lib/verbs.js +24 -0
  129. package/dist/lib/view-types.d.ts +4 -0
  130. package/dist/lib/watchdog/rotate.d.ts +1 -1
  131. package/dist/lib/watchdog/rotate.js +1 -1
  132. package/package.json +1 -1
@@ -7,7 +7,7 @@
7
7
  * BM25 ranking, and label-first search for /rename'd sessions.
8
8
  */
9
9
  import Database from '../sqlite.js';
10
- import type { SessionAgentId, SessionEvent, SessionMeta } from './types.js';
10
+ import type { SessionAgentId, SessionCheckpoint, SessionChecklistItem, SessionEvent, SessionMeta, SummaryState } from './types.js';
11
11
  import { type IndexedToolCall } from './tool-calls.js';
12
12
  import { type ToolScanResumePoint } from './tool-store.js';
13
13
  /** Current schema version; bumped when migrations are added. Exported so tests
@@ -43,6 +43,8 @@ export declare const INSIGHTS_EXTRACTOR_VERSION = 7;
43
43
  export declare const SESSION_TOPIC_EXTRACTOR_VERSION = 2;
44
44
  /** Bump when classifyPhenotype's output changes so cached phenotypes recompute (PHNX-3327 v1). */
45
45
  export declare const SESSION_PHENOTYPE_EXTRACTOR_VERSION = 1;
46
+ /** Bump when the summarizer output shape changes so cached summaries recompute (PHNX-3939 v1). */
47
+ export declare const SESSION_SUMMARY_EXTRACTOR_VERSION = 1;
46
48
  /** File stat snapshot used to detect changes between scan runs. */
47
49
  export interface ScanStamp {
48
50
  fileMtimeMs: number;
@@ -337,8 +339,9 @@ export declare function countSessions(options?: QueryOptions): number;
337
339
  /** One grouped row in a cost/duration rollup. */
338
340
  interface UsageRollupRow {
339
341
  /**
340
- * Grouping key value: the agent id, project name, ISO date (YYYY-MM-DD), or
341
- * account identity (`claude:org=<uuid>` / `unattributed:<reason>`).
342
+ * Grouping key value: the agent id, project name, shortened model id,
343
+ * ISO date (YYYY-MM-DD), or account identity
344
+ * (`claude:org=<uuid>` / `unattributed:<reason>`).
342
345
  */
343
346
  key: string;
344
347
  /**
@@ -443,6 +446,38 @@ export declare function readSessionContent(id: string): string | undefined;
443
446
  * survives for this session.
444
447
  */
445
448
  export declare function readArchivedSessionPreview<T>(id: string): T | undefined;
449
+ /** The daemon-computed summary stored in `session_summaries` (PHNX-3939). */
450
+ export interface SessionSummaryEntry {
451
+ goal?: string;
452
+ checkpoints?: SessionCheckpoint[];
453
+ summaryChecklist?: SessionChecklistItem[];
454
+ summaryState: SummaryState;
455
+ }
456
+ /**
457
+ * Read one summary only when it matches the transcript bytes on disk (the
458
+ * reuse-or-recompute stamp the SessionSummarizerService gates on). Mirrors
459
+ * {@link readSessionPreviewCache}.
460
+ */
461
+ export declare function readSessionSummary(id: string, sourceStamp: {
462
+ fileMtimeMs: number | null;
463
+ fileSize: number | null;
464
+ }): SessionSummaryEntry | undefined;
465
+ /**
466
+ * Read the latest stored summary by session id, regardless of the transcript
467
+ * stamp — the low-cost merge read used by the display path (a live row through
468
+ * {@link applyImmutableMemo} or a history/mirror row) to surface the
469
+ * last-computed summary without a model call. A slightly-stale summary is the
470
+ * best available until the service recomputes; the SERVICE uses the stamped
471
+ * {@link readSessionSummary} to decide whether to recompute.
472
+ */
473
+ export declare function readSessionSummaryAny(id: string): SessionSummaryEntry | undefined;
474
+ /** Persist a computed summary against the exact transcript bytes it was derived from. */
475
+ export declare function writeSessionSummary(entry: {
476
+ id: string;
477
+ fileMtimeMs: number | null;
478
+ fileSize: number | null;
479
+ summary: SessionSummaryEntry;
480
+ }): void;
446
481
  /** A local-origin session, projected to the compact fields the fleet mirror publishes. */
447
482
  export interface LocalMirrorSource {
448
483
  id: string;
@@ -458,6 +493,8 @@ export interface LocalMirrorSource {
458
493
  timestamp: string;
459
494
  ticketId: string | null;
460
495
  prUrl: string | null;
496
+ /** Daemon-computed summary (PHNX-3939), so peers carry it without a transcript. */
497
+ summary: SessionSummaryEntry | null;
461
498
  }
462
499
  /**
463
500
  * The most-recently-active sessions whose transcript is genuinely local to this
@@ -482,6 +519,8 @@ export interface MirrorSessionUpsert {
482
519
  timestamp: string;
483
520
  ticketId?: string | null;
484
521
  prUrl?: string | null;
522
+ /** Daemon-computed summary carried from the publishing peer (PHNX-3939). */
523
+ summary?: SessionSummaryEntry | null;
485
524
  }
486
525
  /**
487
526
  * Write one peer session's digest into the local `sessions` index as a mirror
@@ -504,7 +543,7 @@ export declare function upsertMirrorSession(row: MirrorSessionUpsert, source: st
504
543
  export declare function pruneMirrorSessions(cutoffMs: number): number;
505
544
  /** Plugin provenance already indexed for resources used by one session. */
506
545
  export declare function getSessionPlugins(id: string): string[];
507
- export type UsageRollupGroup = 'agent' | 'project' | 'day' | 'account';
546
+ export type UsageRollupGroup = 'agent' | 'project' | 'day' | 'model' | 'account';
508
547
  /**
509
548
  * Smart-launch affinity priors: group sessions by origin machine, harness, or
510
549
  * joint (machine + agent). Ordered by launch count desc.
@@ -543,9 +582,10 @@ export declare function queryAffinityRollup(options: {
543
582
  }): AffinityRow[];
544
583
  /**
545
584
  * Aggregate cost / duration / tokens across sessions, grouped by agent,
546
- * project, or calendar day. Honors the same filter shape as querySessions
547
- * (agent, since/until, team-origin) so `agents insights cost --since 7d --by day`
548
- * lines up with what `agents sessions` would list. Ordered by cost desc.
585
+ * project, shortened model id, account, or calendar day. Honors the same
586
+ * filter shape as querySessions (agent, since/until, team-origin) so
587
+ * `agents insights cost --since 7d --by day` lines up with what
588
+ * `agents sessions` would list. Ordered by cost desc.
549
589
  */
550
590
  export declare function queryUsageRollup(options: QueryOptions & {
551
591
  groupBy: UsageRollupGroup;
@@ -405,6 +405,21 @@ CREATE TABLE IF NOT EXISTS session_preview_cache (
405
405
  preview_json TEXT NOT NULL
406
406
  );
407
407
 
408
+ -- Daemon-computed per-session summary (goal / checkpoints / checklist), PHNX-3939.
409
+ -- Modeled exactly on session_preview_cache: a lazy, stamp-validated cache keyed on
410
+ -- (session_id, file_mtime_ms, file_size) so a row is reused verbatim until the
411
+ -- transcript bytes change. Written ONLY by the background SessionSummarizerService
412
+ -- (or consumed from a peer's fleet mirror); the display/merge path reads it, never
413
+ -- computes. Independent of SCHEMA_VERSION, like the other lazy caches above.
414
+ CREATE TABLE IF NOT EXISTS session_summaries (
415
+ session_id TEXT PRIMARY KEY,
416
+ file_mtime_ms INTEGER,
417
+ file_size INTEGER,
418
+ extractor_version INTEGER NOT NULL,
419
+ computed_at INTEGER NOT NULL,
420
+ summary_json TEXT NOT NULL
421
+ );
422
+
408
423
  -- Durable metadata for one browser task (RUSH-2549). The browser daemon's
409
424
  -- tasks.json is LIVE state: saveTaskState writes the in-memory task map, so
410
425
  -- stopping a task drops its entry and a daemon restart empties the file. That is
@@ -484,6 +499,8 @@ export const SESSION_TOPIC_EXTRACTOR_VERSION = 2;
484
499
  const PREVIEW_EXTRACTOR_VERSION = 2;
485
500
  /** Bump when classifyPhenotype's output changes so cached phenotypes recompute (PHNX-3327 v1). */
486
501
  export const SESSION_PHENOTYPE_EXTRACTOR_VERSION = 1;
502
+ /** Bump when the summarizer output shape changes so cached summaries recompute (PHNX-3939 v1). */
503
+ export const SESSION_SUMMARY_EXTRACTOR_VERSION = 1;
487
504
  let dbInstance = null;
488
505
  /**
489
506
  * Apply schema migrations from `fromVersion` → SCHEMA_VERSION. The new
@@ -3340,6 +3357,66 @@ export function readArchivedSessionPreview(id) {
3340
3357
  return undefined;
3341
3358
  }
3342
3359
  }
3360
+ /**
3361
+ * Read one summary only when it matches the transcript bytes on disk (the
3362
+ * reuse-or-recompute stamp the SessionSummarizerService gates on). Mirrors
3363
+ * {@link readSessionPreviewCache}.
3364
+ */
3365
+ export function readSessionSummary(id, sourceStamp) {
3366
+ const row = getDB().prepare(`
3367
+ SELECT summary_json AS summaryJson
3368
+ FROM session_summaries
3369
+ WHERE session_id = ?
3370
+ AND extractor_version = ?
3371
+ AND file_mtime_ms IS ?
3372
+ AND file_size IS ?
3373
+ `).get(id, SESSION_SUMMARY_EXTRACTOR_VERSION, sourceStamp.fileMtimeMs, sourceStamp.fileSize);
3374
+ if (!row)
3375
+ return undefined;
3376
+ try {
3377
+ return JSON.parse(row.summaryJson);
3378
+ }
3379
+ catch {
3380
+ return undefined;
3381
+ }
3382
+ }
3383
+ /**
3384
+ * Read the latest stored summary by session id, regardless of the transcript
3385
+ * stamp — the low-cost merge read used by the display path (a live row through
3386
+ * {@link applyImmutableMemo} or a history/mirror row) to surface the
3387
+ * last-computed summary without a model call. A slightly-stale summary is the
3388
+ * best available until the service recomputes; the SERVICE uses the stamped
3389
+ * {@link readSessionSummary} to decide whether to recompute.
3390
+ */
3391
+ export function readSessionSummaryAny(id) {
3392
+ const row = getDB().prepare(`
3393
+ SELECT summary_json AS summaryJson
3394
+ FROM session_summaries
3395
+ WHERE session_id = ? AND extractor_version = ?
3396
+ `).get(id, SESSION_SUMMARY_EXTRACTOR_VERSION);
3397
+ if (!row)
3398
+ return undefined;
3399
+ try {
3400
+ return JSON.parse(row.summaryJson);
3401
+ }
3402
+ catch {
3403
+ return undefined;
3404
+ }
3405
+ }
3406
+ /** Persist a computed summary against the exact transcript bytes it was derived from. */
3407
+ export function writeSessionSummary(entry) {
3408
+ getDB().prepare(`
3409
+ INSERT INTO session_summaries
3410
+ (session_id, file_mtime_ms, file_size, extractor_version, computed_at, summary_json)
3411
+ VALUES (?, ?, ?, ?, ?, ?)
3412
+ ON CONFLICT(session_id) DO UPDATE SET
3413
+ file_mtime_ms = excluded.file_mtime_ms,
3414
+ file_size = excluded.file_size,
3415
+ extractor_version = excluded.extractor_version,
3416
+ computed_at = excluded.computed_at,
3417
+ summary_json = excluded.summary_json
3418
+ `).run(entry.id, entry.fileMtimeMs, entry.fileSize, SESSION_SUMMARY_EXTRACTOR_VERSION, Date.now(), JSON.stringify(entry.summary));
3419
+ }
3343
3420
  /**
3344
3421
  * The most-recently-active sessions whose transcript is genuinely local to this
3345
3422
  * box — a real `file_path`, not a peer mirror — for publishing to the fleet
@@ -3364,6 +3441,9 @@ export function queryLocalOriginSessionsForMirror(self, limit) {
3364
3441
  id: r.id, shortId: r.short_id, agent: r.agent, version: r.version, machine: r.machine,
3365
3442
  cwd: r.cwd, topic: r.topic, firstUserMessage: r.first_user_message, label: r.label,
3366
3443
  lastActivity: r.last_activity, timestamp: r.timestamp, ticketId: r.ticket_id, prUrl: r.pr_url,
3444
+ // Ride the daemon-computed summary alongside the digest so a peer renders it
3445
+ // without a transcript (PHNX-3939); only a summarized session carries one.
3446
+ summary: readSessionSummaryAny(r.id) ?? null,
3367
3447
  }));
3368
3448
  }
3369
3449
  /**
@@ -3437,6 +3517,14 @@ export function upsertMirrorSession(row, source, syncedAt) {
3437
3517
  INSERT INTO session_text (session_id, label, topic, project, content, assistant)
3438
3518
  VALUES (?, ?, ?, '', ?, '')
3439
3519
  `).run(row.id, row.label ?? '', row.topic ?? '', row.firstUser ?? '');
3520
+ // Carry the peer's daemon-computed summary into the same session_summaries
3521
+ // cache the local merge reads (PHNX-3939), so a peer session renders its
3522
+ // goal/checkpoints/checklist inline with no transcript. Stamp is null: the
3523
+ // display merge reads by id (readSessionSummaryAny), and this box's own
3524
+ // summarizer service only ever recomputes LOCAL live sessions, never a peer's.
3525
+ if (row.summary) {
3526
+ writeSessionSummary({ id: row.id, fileMtimeMs: null, fileSize: null, summary: row.summary });
3527
+ }
3440
3528
  return true;
3441
3529
  }
3442
3530
  /**
@@ -3453,11 +3541,13 @@ export function pruneMirrorSessions(cutoffMs) {
3453
3541
  const delRow = db.prepare(`DELETE FROM sessions WHERE id = ?`);
3454
3542
  const delText = db.prepare(`DELETE FROM session_text WHERE session_id = ?`);
3455
3543
  const delPreview = db.prepare(`DELETE FROM session_preview_cache WHERE session_id = ?`);
3544
+ const delSummary = db.prepare(`DELETE FROM session_summaries WHERE session_id = ?`);
3456
3545
  const txn = db.transaction(() => {
3457
3546
  for (const { id } of stale) {
3458
3547
  delRow.run(id);
3459
3548
  delText.run(id);
3460
3549
  delPreview.run(id);
3550
+ delSummary.run(id);
3461
3551
  }
3462
3552
  });
3463
3553
  txn();
@@ -3526,9 +3616,10 @@ export function queryAffinityRollup(options) {
3526
3616
  }
3527
3617
  /**
3528
3618
  * Aggregate cost / duration / tokens across sessions, grouped by agent,
3529
- * project, or calendar day. Honors the same filter shape as querySessions
3530
- * (agent, since/until, team-origin) so `agents insights cost --since 7d --by day`
3531
- * lines up with what `agents sessions` would list. Ordered by cost desc.
3619
+ * project, shortened model id, account, or calendar day. Honors the same
3620
+ * filter shape as querySessions (agent, since/until, team-origin) so
3621
+ * `agents insights cost --since 7d --by day` lines up with what
3622
+ * `agents sessions` would list. Ordered by cost desc.
3532
3623
  */
3533
3624
  export function queryUsageRollup(options) {
3534
3625
  const db = getDB();
@@ -3537,15 +3628,26 @@ export function queryUsageRollup(options) {
3537
3628
  ? 'agent'
3538
3629
  : options.groupBy === 'project'
3539
3630
  ? `IFNULL(NULLIF(project, ''), '(no project)')`
3540
- : options.groupBy === 'account'
3541
- // A NULL account_key means this harness has no account attribution yet —
3542
- // the mechanism is Claude-only today (see lib/session/claude-accounts.ts).
3543
- // Bucket per agent so the rows are named honestly instead of being called
3544
- // "not indexed", which they are not, and instead of joining a real account.
3545
- ? `IFNULL(NULLIF(account_key, ''), 'unattributed:' || agent)`
3546
- // ISO timestamps are lexicographically date-sortable; the date is the
3547
- // first 10 chars (YYYY-MM-DD).
3548
- : `substr(timestamp, 1, 10)`;
3631
+ : options.groupBy === 'model'
3632
+ // Match shortenModel(): remove Claude's redundant harness prefix and
3633
+ // an optional eight-digit release-date suffix before grouping.
3634
+ ? `IFNULL(NULLIF(CASE
3635
+ WHEN substr(CASE WHEN model LIKE 'claude-%' THEN substr(model, 8) ELSE model END, -9, 1) = '-'
3636
+ AND length(substr(CASE WHEN model LIKE 'claude-%' THEN substr(model, 8) ELSE model END, -8)) = 8
3637
+ AND substr(CASE WHEN model LIKE 'claude-%' THEN substr(model, 8) ELSE model END, -8) NOT GLOB '*[^0-9]*'
3638
+ THEN substr(CASE WHEN model LIKE 'claude-%' THEN substr(model, 8) ELSE model END, 1,
3639
+ length(CASE WHEN model LIKE 'claude-%' THEN substr(model, 8) ELSE model END) - 9)
3640
+ ELSE CASE WHEN model LIKE 'claude-%' THEN substr(model, 8) ELSE model END
3641
+ END, ''), '(unknown)')`
3642
+ : options.groupBy === 'account'
3643
+ // A NULL account_key means this harness has no account attribution yet —
3644
+ // the mechanism is Claude-only today (see lib/session/claude-accounts.ts).
3645
+ // Bucket per agent so the rows are named honestly instead of being called
3646
+ // "not indexed", which they are not, and instead of joining a real account.
3647
+ ? `IFNULL(NULLIF(account_key, ''), 'unattributed:' || agent)`
3648
+ // ISO timestamps are lexicographically date-sortable; the date is the
3649
+ // first 10 chars (YYYY-MM-DD).
3650
+ : `substr(timestamp, 1, 10)`;
3549
3651
  const sql = `
3550
3652
  SELECT
3551
3653
  ${keyExpr} AS key,
@@ -62,6 +62,27 @@ export async function publishSessionMirrorToSharedStore(options = {}) {
62
62
  timestamp: s.timestamp,
63
63
  ...(s.ticketId ? { ticketId: s.ticketId } : {}),
64
64
  ...(s.prUrl ? { prUrl: s.prUrl } : {}),
65
+ // Ride the daemon-computed summary (PHNX-3939) so a peer renders the goal /
66
+ // checkpoints / checklist inline — still no transcript, keeping the mirror light.
67
+ ...(s.summary?.goal ? { goal: snippet(s.summary.goal, 400) } : {}),
68
+ // Bound on publish exactly as toMirrorSummary bounds on consume (50/100 items,
69
+ // 400-char text, 40-char `at`) so this box's own model output can't ride raw
70
+ // into the git-synced fleet state file — matching the snippet() cap on goal above.
71
+ ...(s.summary?.checkpoints
72
+ ? {
73
+ checkpoints: s.summary.checkpoints
74
+ .slice(0, 50)
75
+ .map((c) => ({ text: String(c.text).slice(0, 400), at: String(c.at).slice(0, 40) })),
76
+ }
77
+ : {}),
78
+ ...(s.summary?.summaryChecklist
79
+ ? {
80
+ summaryChecklist: s.summary.summaryChecklist
81
+ .slice(0, 100)
82
+ .map((c) => ({ text: String(c.text).slice(0, 400), done: Boolean(c.done) })),
83
+ }
84
+ : {}),
85
+ ...(s.summary?.summaryState ? { summaryState: s.summary.summaryState } : {}),
65
86
  capturedAt,
66
87
  }));
67
88
  try {
@@ -111,6 +132,43 @@ function toUpsert(raw) {
111
132
  timestamp,
112
133
  ticketId: cap(r.ticketId, 64),
113
134
  prUrl: cap(r.prUrl, 512),
135
+ summary: toMirrorSummary(r),
136
+ };
137
+ }
138
+ /** One published summary state, validated back into the union or undefined. */
139
+ function toSummaryState(v) {
140
+ return v === 'pending' || v === 'ready' || v === 'skipped' ? v : undefined;
141
+ }
142
+ /**
143
+ * Validate an untrusted peer's published summary fields into a bounded
144
+ * {@link SessionSummaryEntry} (PHNX-3939), or undefined when it carries none.
145
+ * Bounds mirror the publish side so a hostile/oversized peer can't bloat the
146
+ * local cache. Only a row with a resolvable `summaryState` is kept.
147
+ */
148
+ function toMirrorSummary(r) {
149
+ const summaryState = toSummaryState(r.summaryState);
150
+ if (!summaryState)
151
+ return undefined;
152
+ const goal = isString(r.goal) ? r.goal.slice(0, 400) : undefined;
153
+ const checkpoints = Array.isArray(r.checkpoints)
154
+ ? r.checkpoints
155
+ .filter((c) => Boolean(c) && typeof c === 'object' && !Array.isArray(c))
156
+ .filter((c) => isString(c.text) && isString(c.at))
157
+ .slice(0, 50)
158
+ .map((c) => ({ text: String(c.text).slice(0, 400), at: String(c.at).slice(0, 40) }))
159
+ : undefined;
160
+ const summaryChecklist = Array.isArray(r.summaryChecklist)
161
+ ? r.summaryChecklist
162
+ .filter((c) => Boolean(c) && typeof c === 'object' && !Array.isArray(c))
163
+ .filter((c) => isString(c.text))
164
+ .slice(0, 100)
165
+ .map((c) => ({ text: String(c.text).slice(0, 400), done: Boolean(c.done) }))
166
+ : undefined;
167
+ return {
168
+ summaryState,
169
+ ...(goal ? { goal } : {}),
170
+ ...(checkpoints && checkpoints.length ? { checkpoints } : {}),
171
+ ...(summaryChecklist && summaryChecklist.length ? { summaryChecklist } : {}),
114
172
  };
115
173
  }
116
174
  /**
@@ -9,10 +9,10 @@ import { machineId, normalizeHost } from '../../machine-id.js';
9
9
  import { SSH_OPTS, controlOpts, shellQuote } from '../../ssh-exec.js';
10
10
  import { buildWindowsAgentsCommand, remoteShellFor } from '../../hosts/remote-cmd.js';
11
11
  import { isReapableOrphan } from '../active.js';
12
- import { querySessions } from '../db.js';
12
+ import { querySessions, readSessionSummaryAny } from '../db.js';
13
13
  import { linearIssueUrl } from '../linear.js';
14
14
  import { sessionAgentSupportsResume } from '../recovery.js';
15
- import { activeSessionsJournalPath, activeSessionJournalIdentity, readActiveSessionsCache, noteActiveSessionsJournalReader, } from '../session-cache.js';
15
+ import { activeSessionsJournalPath, activeSessionJournalIdentity, readActiveSessionsCache, noteActiveSessionsJournalReader, resolveStreamSummaryState, } from '../session-cache.js';
16
16
  export const SESSION_WATCH_VERSION = 1;
17
17
  export const SESSION_WATCH_HEARTBEAT_MS = 15_000;
18
18
  export const SESSION_WATCH_PREVIOUS_LIMIT = 50;
@@ -36,6 +36,10 @@ export function toSessionWatchRow(scope, row) {
36
36
  : null;
37
37
  return {
38
38
  ...row,
39
+ // The summarizer's per-row state is delivered on the stream: a live row with
40
+ // no computed summary reads `pending` when the summarizer is on, `skipped`
41
+ // when it is off (PHNX-3939). goal/checkpoints/summaryChecklist rode `...row`.
42
+ summaryState: resolveStreamSummaryState(row.summaryState),
39
43
  rowKey,
40
44
  sourceDevice: scope,
41
45
  previous,
@@ -67,6 +71,18 @@ export function toPreviousSessionWatchRow(scope, session) {
67
71
  const worktree = session.worktreeSlug && session.cwd
68
72
  ? { slug: session.worktreeSlug, path: session.cwd, ...(session.gitBranch ? { branch: session.gitBranch } : {}) }
69
73
  : undefined;
74
+ // Project the daemon-computed summary (PHNX-3939) onto the history row from the
75
+ // transcript-keyed cache — the same store the live merge reads, so a closed or
76
+ // fleet-mirrored session carries its goal/checkpoints/checklist with no
77
+ // transcript re-parse and no model call. Prefer a value already on the meta row.
78
+ const summary = session.goal !== undefined || session.summaryState !== undefined
79
+ ? {
80
+ goal: session.goal,
81
+ checkpoints: session.checkpoints,
82
+ summaryChecklist: session.summaryChecklist,
83
+ summaryState: session.summaryState,
84
+ }
85
+ : readSessionSummaryAny(session.id);
70
86
  return {
71
87
  context: 'recent',
72
88
  kind: session.agent,
@@ -88,6 +104,10 @@ export function toPreviousSessionWatchRow(scope, session) {
88
104
  ...(session.tokenCount != null ? { tokenCount: session.tokenCount } : {}),
89
105
  ...(session.durationMs != null ? { durationMs: session.durationMs } : {}),
90
106
  ...(session.subAgentCount != null ? { subAgentCount: session.subAgentCount } : {}),
107
+ ...(summary?.goal ? { goal: summary.goal } : {}),
108
+ ...(summary?.checkpoints ? { checkpoints: summary.checkpoints } : {}),
109
+ ...(summary?.summaryChecklist ? { summaryChecklist: summary.summaryChecklist } : {}),
110
+ summaryState: resolveStreamSummaryState(summary?.summaryState),
91
111
  startedAtMs,
92
112
  lastActivityMs,
93
113
  status: 'closed',
@@ -171,6 +171,25 @@ export declare function updateImmutableMemos(sessions: ReadonlyArray<ActiveSessi
171
171
  * mtime matches. Never overwrites a field the live gather already set, and
172
172
  * never copies live-status keys.
173
173
  */
174
+ /**
175
+ * Merge the daemon-computed session summary (PHNX-3939) onto a live row from the
176
+ * transcript-keyed `session_summaries` cache. This is the low-cost display merge:
177
+ * one indexed by-id read, NEVER a model call — the background
178
+ * SessionSummarizerService is the only producer. It fills only what the cache
179
+ * holds; the `pending`/`skipped` default for a row with no summary yet is applied
180
+ * at the watch-stream boundary ({@link resolveStreamSummaryState}), so a
181
+ * non-stream ActiveSession consumer is unaffected. Best-effort: a DB error leaves
182
+ * the row untouched.
183
+ */
184
+ export declare function mergeSessionSummary(s: ActiveSession): ActiveSession;
185
+ /**
186
+ * The `summaryState` a watch-stream row carries when the merge left it unset:
187
+ * `pending` while the summarizer is READY (enabled AND has an endpoint, so a
188
+ * summary is genuinely coming), `skipped` when it is off OR enabled-but-
189
+ * unconfigured (nothing will ever populate the cache). This is the only place
190
+ * the readiness flag is read on the read path, and it is memoized (PHNX-3939).
191
+ */
192
+ export declare function resolveStreamSummaryState(current: import('./types.js').SummaryState | undefined, nowMs?: number): import('./types.js').SummaryState;
174
193
  export declare function applyImmutableMemo(s: ActiveSession): ActiveSession;
175
194
  export interface LoadLocalActiveSessionsOptions {
176
195
  /** Skip the cache and re-gather (the force-refresh path). */
@@ -28,6 +28,8 @@ import * as path from 'path';
28
28
  import { createMemoryCache } from '../memory-cache.js';
29
29
  import { getCacheDir } from '../state.js';
30
30
  import { backfillActiveRowsFromIndex, sessionProcessIsLocal } from './active.js';
31
+ import { readSessionSummaryAny } from './db.js';
32
+ import { isSummarizerReady } from '../summarizer/config.js';
31
33
  /** Snapshot file under `getCacheDir()` (regenerable, gitignored). */
32
34
  const SNAPSHOT_FILE = '.active-sessions.json';
33
35
  /** Immutable-field memo file (keyed by sessionId + transcript mtime). */
@@ -423,9 +425,53 @@ export function updateImmutableMemos(sessions, nowMs = Date.now()) {
423
425
  * mtime matches. Never overwrites a field the live gather already set, and
424
426
  * never copies live-status keys.
425
427
  */
428
+ /**
429
+ * Merge the daemon-computed session summary (PHNX-3939) onto a live row from the
430
+ * transcript-keyed `session_summaries` cache. This is the low-cost display merge:
431
+ * one indexed by-id read, NEVER a model call — the background
432
+ * SessionSummarizerService is the only producer. It fills only what the cache
433
+ * holds; the `pending`/`skipped` default for a row with no summary yet is applied
434
+ * at the watch-stream boundary ({@link resolveStreamSummaryState}), so a
435
+ * non-stream ActiveSession consumer is unaffected. Best-effort: a DB error leaves
436
+ * the row untouched.
437
+ */
438
+ export function mergeSessionSummary(s) {
439
+ if (!s.sessionId)
440
+ return s;
441
+ try {
442
+ const stored = readSessionSummaryAny(s.sessionId);
443
+ if (!stored)
444
+ return s;
445
+ if (s.goal === undefined && stored.goal !== undefined)
446
+ s.goal = stored.goal;
447
+ if (s.checkpoints === undefined && stored.checkpoints !== undefined)
448
+ s.checkpoints = stored.checkpoints;
449
+ if (s.summaryChecklist === undefined && stored.summaryChecklist !== undefined)
450
+ s.summaryChecklist = stored.summaryChecklist;
451
+ if (s.summaryState === undefined)
452
+ s.summaryState = stored.summaryState;
453
+ }
454
+ catch {
455
+ // best-effort: never let the summary merge break the gather path.
456
+ }
457
+ return s;
458
+ }
459
+ /**
460
+ * The `summaryState` a watch-stream row carries when the merge left it unset:
461
+ * `pending` while the summarizer is READY (enabled AND has an endpoint, so a
462
+ * summary is genuinely coming), `skipped` when it is off OR enabled-but-
463
+ * unconfigured (nothing will ever populate the cache). This is the only place
464
+ * the readiness flag is read on the read path, and it is memoized (PHNX-3939).
465
+ */
466
+ export function resolveStreamSummaryState(current, nowMs = Date.now()) {
467
+ if (current)
468
+ return current;
469
+ return isSummarizerReady(nowMs) ? 'pending' : 'skipped';
470
+ }
426
471
  export function applyImmutableMemo(s) {
427
472
  if (!s.sessionId)
428
473
  return s;
474
+ mergeSessionSummary(s);
429
475
  const mtime = transcriptMtimeMs(s);
430
476
  if (mtime === null)
431
477
  return s;
@@ -126,6 +126,28 @@ export interface TodoProgress {
126
126
  /** The in-progress item's activeForm (falls back to its content). The live step. */
127
127
  activeForm?: string;
128
128
  }
129
+ /**
130
+ * One progress checkpoint in a daemon-computed session summary (PHNX-3939):
131
+ * a short line of what happened, stamped with an ISO time. Newest last.
132
+ */
133
+ export interface SessionCheckpoint {
134
+ text: string;
135
+ /** ISO timestamp for this checkpoint. */
136
+ at: string;
137
+ }
138
+ /** One item in a daemon-computed session summary checklist (PHNX-3939). */
139
+ export interface SessionChecklistItem {
140
+ text: string;
141
+ done: boolean;
142
+ }
143
+ /**
144
+ * Lifecycle of a session's daemon-computed summary (PHNX-3939):
145
+ * - `pending` — the summarizer is enabled and this session has no summary yet.
146
+ * - `ready` — a summary was computed and merged onto the row.
147
+ * - `skipped` — the summarizer is disabled/unconfigured, or a compute error
148
+ * left no summary; the row deliberately carries no goal/checkpoints.
149
+ */
150
+ export type SummaryState = 'pending' | 'ready' | 'skipped';
129
151
  /** Metadata attached when a session was spawned by `agents teams`. */
130
152
  export interface TeamOrigin {
131
153
  /** Teammate name if set, otherwise first 8 chars of the agent UUID. */
@@ -400,6 +422,18 @@ export interface SessionMeta {
400
422
  archived?: boolean;
401
423
  /** Epoch ms the transcript file was first confirmed gone (pairs with {@link archived}). */
402
424
  archivedAt?: number;
425
+ /**
426
+ * Daemon-computed 1–2 line goal extracted from the session's first user turn
427
+ * (PHNX-3939). Never on the request path — produced by the background
428
+ * `SessionSummarizerService` and merged from the `session_summaries` cache.
429
+ */
430
+ goal?: string;
431
+ /** Daemon-computed progress checkpoints, newest last (PHNX-3939). */
432
+ checkpoints?: SessionCheckpoint[];
433
+ /** Daemon-computed detailed checklist for the session (PHNX-3939). */
434
+ summaryChecklist?: SessionChecklistItem[];
435
+ /** Lifecycle of the daemon-computed summary; `skipped` when disabled (PHNX-3939). */
436
+ summaryState?: SummaryState;
403
437
  /** Terms that matched the current search query */
404
438
  _matchedTerms?: string[];
405
439
  /** BM25 relevance score from the most recent content-index search */
@@ -4,7 +4,7 @@
4
4
  * Two legitimate principals, picked once at publish time:
5
5
  * - **managed**: the caller is signed in to Phoenix (`readSession()`), no
6
6
  * explicit BYO override. Token is the Phoenix access_token; endpoint is
7
- * the already-live `share.agents-cli.sh`. Zero Cloudflare setup.
7
+ * the already-live `share.getrush.ai`. Zero Cloudflare setup.
8
8
  * - **byo**: the existing static WRITE_TOKEN path (`readShareConfig()` +
9
9
  * `readWriteToken()`). Unchanged for power users / custom domains / fleet
10
10
  * injection of `SHARE_WRITE_TOKEN`.
@@ -68,9 +68,11 @@ export declare function handleFromEmail(email: string | undefined): string;
68
68
  export declare function managedShareHandle(session: Pick<PhoenixSession, 'email' | 'userId'>): string;
69
69
  export declare function managedShareBaseUrl(): string;
70
70
  /**
71
- * True when this config is the platform managed endpoint (`share.agents-cli.sh`).
72
- * The deploy path uses this — not "the caller is signed in" — to decide whether
73
- * to bind `PHOENIX_ID_BASE` on the Worker.
71
+ * True when this config is the platform managed endpoint (`share.getrush.ai`, or a
72
+ * legacy alias — `share.agents-cli.sh` / `artifacts.prix.dev` — still bound to the
73
+ * same Worker). The
74
+ * deploy path uses this — not "the caller is signed in" — to decide whether to
75
+ * bind `PHOENIX_ID_BASE` on the Worker.
74
76
  */
75
77
  export declare function isManagedShareEndpoint(cfg: {
76
78
  baseUrl?: string;
@@ -4,7 +4,7 @@
4
4
  * Two legitimate principals, picked once at publish time:
5
5
  * - **managed**: the caller is signed in to Phoenix (`readSession()`), no
6
6
  * explicit BYO override. Token is the Phoenix access_token; endpoint is
7
- * the already-live `share.agents-cli.sh`. Zero Cloudflare setup.
7
+ * the already-live `share.getrush.ai`. Zero Cloudflare setup.
8
8
  * - **byo**: the existing static WRITE_TOKEN path (`readShareConfig()` +
9
9
  * `readWriteToken()`). Unchanged for power users / custom domains / fleet
10
10
  * injection of `SHARE_WRITE_TOKEN`.
@@ -19,7 +19,7 @@
19
19
  */
20
20
  import { PHOENIX_ID_BASE, readSession } from '../identity/client.js';
21
21
  import { selectStorageBackendKind } from '../storage/selection.js';
22
- import { DEFAULT_SHARE_DOMAIN, readShareConfig, readWriteToken, readWriteTokenEnv, } from './config.js';
22
+ import { DEFAULT_SHARE_DOMAIN, LEGACY_MANAGED_SHARE_DOMAINS, readShareConfig, readWriteToken, readWriteTokenEnv, } from './config.js';
23
23
  /** Env var that forces the BYO path. Value must be exactly `byo`. */
24
24
  export const SHARE_BACKEND_ENV = 'AGENTS_SHARE_BACKEND';
25
25
  /** URL-safe namespace: lowercase `[a-z0-9-]+`. Matches the Worker's sanitize.
@@ -55,15 +55,17 @@ function shareEndpointHost(value) {
55
55
  return value.replace(/^https?:\/\//i, '').replace(/\/+$/, '').toLowerCase();
56
56
  }
57
57
  /**
58
- * True when this config is the platform managed endpoint (`share.agents-cli.sh`).
59
- * The deploy path uses this — not "the caller is signed in" — to decide whether
60
- * to bind `PHOENIX_ID_BASE` on the Worker.
58
+ * True when this config is the platform managed endpoint (`share.getrush.ai`, or a
59
+ * legacy alias — `share.agents-cli.sh` / `artifacts.prix.dev` — still bound to the
60
+ * same Worker). The
61
+ * deploy path uses this — not "the caller is signed in" — to decide whether to
62
+ * bind `PHOENIX_ID_BASE` on the Worker.
61
63
  */
62
64
  export function isManagedShareEndpoint(cfg) {
63
- const managedHost = DEFAULT_SHARE_DOMAIN.toLowerCase();
64
- if (cfg.domain && shareEndpointHost(cfg.domain) === managedHost)
65
+ const managedHosts = new Set([DEFAULT_SHARE_DOMAIN, ...LEGACY_MANAGED_SHARE_DOMAINS].map((h) => h.toLowerCase()));
66
+ if (cfg.domain && managedHosts.has(shareEndpointHost(cfg.domain)))
65
67
  return true;
66
- if (cfg.baseUrl && shareEndpointHost(cfg.baseUrl) === managedHost)
68
+ if (cfg.baseUrl && managedHosts.has(shareEndpointHost(cfg.baseUrl)))
67
69
  return true;
68
70
  return false;
69
71
  }
@@ -1,10 +1,10 @@
1
1
  export interface ShareConfig {
2
- /** Public base, e.g. `https://share.agents-cli.sh` or `https://agent-share.<acct>.workers.dev`. */
2
+ /** Public base, e.g. `https://share.getrush.ai` or `https://agent-share.<acct>.workers.dev`. */
3
3
  baseUrl: string;
4
4
  accountId: string;
5
5
  workerName: string;
6
6
  bucketName: string;
7
- /** Custom domain when mapped (e.g. `share.agents-cli.sh`). */
7
+ /** Custom domain when mapped (e.g. `share.getrush.ai`). */
8
8
  domain?: string;
9
9
  /** Cloudflare Web Analytics token injected into published HTML pages. */
10
10
  analyticsToken?: string;
@@ -19,7 +19,8 @@ export declare const SHARE_TOKEN_ENV_KEY = "SHARE_WRITE_TOKEN";
19
19
  export declare const DEFAULT_CF_BUNDLE = "cloudflare";
20
20
  export declare const DEFAULT_WORKER_NAME = "agents-share";
21
21
  export declare const DEFAULT_BUCKET_NAME = "agents-share";
22
- export declare const DEFAULT_SHARE_DOMAIN = "share.agents-cli.sh";
22
+ export declare const DEFAULT_SHARE_DOMAIN = "share.getrush.ai";
23
+ export declare const LEGACY_MANAGED_SHARE_DOMAINS: readonly ["share.agents-cli.sh", "artifacts.prix.dev"];
23
24
  /** The write token may be injected ephemerally into fleet/cloud agents. */
24
25
  export declare function readWriteTokenEnv(env?: NodeJS.ProcessEnv): string | null;
25
26
  /**