@phnx-labs/agents-cli 1.22.75 → 1.22.77

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 (138) hide show
  1. package/CHANGELOG.md +121 -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/registry.js +10 -3
  52. package/dist/lib/browser/types.d.ts +18 -0
  53. package/dist/lib/config-keys.d.ts +7 -2
  54. package/dist/lib/config-keys.js +17 -2
  55. package/dist/lib/daemon/daemon.js +8 -0
  56. package/dist/lib/daemon/session-summarizer-service.d.ts +24 -0
  57. package/dist/lib/daemon/session-summarizer-service.js +39 -0
  58. package/dist/lib/daemon-services.d.ts +1 -1
  59. package/dist/lib/daemon-services.js +5 -0
  60. package/dist/lib/daemon-ticks.d.ts +2 -2
  61. package/dist/lib/daemon-ticks.js +2 -1
  62. package/dist/lib/deeplink/register.js +10 -9
  63. package/dist/lib/deeplink/url.d.ts +4 -4
  64. package/dist/lib/deeplink/url.js +4 -4
  65. package/dist/lib/device-config.js +25 -0
  66. package/dist/lib/devices/doctor-findings.d.ts +4 -4
  67. package/dist/lib/devices/doctor-findings.js +14 -8
  68. package/dist/lib/devices/registry.js +2 -0
  69. package/dist/lib/devices/stats-cache.d.ts +4 -0
  70. package/dist/lib/devices/stats-cache.js +19 -0
  71. package/dist/lib/drift-sync.d.ts +3 -1
  72. package/dist/lib/drift-sync.js +16 -5
  73. package/dist/lib/exec.d.ts +2 -0
  74. package/dist/lib/exec.js +16 -1
  75. package/dist/lib/fleet-shared-state.d.ts +8 -0
  76. package/dist/lib/git.js +6 -0
  77. package/dist/lib/heal.d.ts +4 -3
  78. package/dist/lib/heal.js +5 -4
  79. package/dist/lib/hosts/ready.d.ts +1 -1
  80. package/dist/lib/hosts/ready.js +16 -4
  81. package/dist/lib/hosts/reconnect.js +4 -2
  82. package/dist/lib/identity/client.d.ts +6 -0
  83. package/dist/lib/identity/index.d.ts +16 -0
  84. package/dist/lib/identity/index.js +25 -1
  85. package/dist/lib/installations/migrate.d.ts +10 -0
  86. package/dist/lib/installations/migrate.js +18 -0
  87. package/dist/lib/profiles.d.ts +2 -0
  88. package/dist/lib/profiles.js +28 -9
  89. package/dist/lib/project-resources.d.ts +23 -0
  90. package/dist/lib/project-resources.js +94 -0
  91. package/dist/lib/reconcile-and-repair.d.ts +109 -0
  92. package/dist/lib/reconcile-and-repair.js +267 -0
  93. package/dist/lib/routers.d.ts +12 -1
  94. package/dist/lib/routers.js +30 -1
  95. package/dist/lib/scheduling/routines.js +8 -2
  96. package/dist/lib/session/active.d.ts +13 -0
  97. package/dist/lib/session/db.d.ts +47 -7
  98. package/dist/lib/session/db.js +114 -12
  99. package/dist/lib/session/mirror.js +58 -0
  100. package/dist/lib/session/remote/watch.js +22 -2
  101. package/dist/lib/session/session-cache.d.ts +19 -0
  102. package/dist/lib/session/session-cache.js +46 -0
  103. package/dist/lib/session/types.d.ts +34 -0
  104. package/dist/lib/share/backend.d.ts +6 -4
  105. package/dist/lib/share/backend.js +10 -8
  106. package/dist/lib/share/config.d.ts +4 -3
  107. package/dist/lib/share/config.js +10 -1
  108. package/dist/lib/share/delete.d.ts +1 -1
  109. package/dist/lib/share/delete.js +1 -1
  110. package/dist/lib/share/html.d.ts +1 -1
  111. package/dist/lib/share/html.js +1 -1
  112. package/dist/lib/share/provision.d.ts +1 -1
  113. package/dist/lib/share/provision.js +2 -2
  114. package/dist/lib/share/publish.d.ts +23 -7
  115. package/dist/lib/share/publish.js +58 -12
  116. package/dist/lib/share/worker-template.js +221 -60
  117. package/dist/lib/startup/command-registry.js +2 -2
  118. package/dist/lib/state.d.ts +58 -1
  119. package/dist/lib/state.js +138 -16
  120. package/dist/lib/summarizer/config.d.ts +46 -0
  121. package/dist/lib/summarizer/config.js +83 -0
  122. package/dist/lib/summarizer/pass.d.ts +45 -0
  123. package/dist/lib/summarizer/pass.js +112 -0
  124. package/dist/lib/summarizer/summarize.d.ts +68 -0
  125. package/dist/lib/summarizer/summarize.js +120 -0
  126. package/dist/lib/teams/agents.d.ts +4 -3
  127. package/dist/lib/teams/agents.js +12 -4
  128. package/dist/lib/teams/scheduler.d.ts +4 -2
  129. package/dist/lib/teams/scheduler.js +6 -6
  130. package/dist/lib/tmux/session.d.ts +2 -0
  131. package/dist/lib/tmux/session.js +7 -1
  132. package/dist/lib/types.d.ts +20 -0
  133. package/dist/lib/verbs.d.ts +23 -0
  134. package/dist/lib/verbs.js +24 -0
  135. package/dist/lib/view-types.d.ts +4 -0
  136. package/dist/lib/watchdog/rotate.d.ts +1 -1
  137. package/dist/lib/watchdog/rotate.js +1 -1
  138. package/package.json +1 -1
@@ -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
  /**
@@ -17,7 +17,16 @@ export const SHARE_TOKEN_ENV_KEY = 'SHARE_WRITE_TOKEN';
17
17
  export const DEFAULT_CF_BUNDLE = 'cloudflare';
18
18
  export const DEFAULT_WORKER_NAME = 'agents-share';
19
19
  export const DEFAULT_BUCKET_NAME = 'agents-share';
20
- export const DEFAULT_SHARE_DOMAIN = 'share.agents-cli.sh';
20
+ // The Rush product share endpoint (PHNX-3960): share.getrush.ai on the product
21
+ // domain. Publishing prints this host; bound to the `agents-share` Worker in the
22
+ // Phoenix CF account. share.agents-cli.sh + artifacts.prix.dev stay bound as legacy aliases.
23
+ export const DEFAULT_SHARE_DOMAIN = 'share.getrush.ai';
24
+ // Managed hosts still recognized as "the platform endpoint" for backward
25
+ // compatibility. `share.agents-cli.sh` is the pre-PHNX-3960 default: it stays
26
+ // attached to the same Worker, so every link already shared on it keeps
27
+ // resolving, and a persisted config pointing at it is still treated as managed
28
+ // (e.g. for deploy-time PHOENIX_ID_BASE binding). Compared lowercased.
29
+ export const LEGACY_MANAGED_SHARE_DOMAINS = ['share.agents-cli.sh', 'artifacts.prix.dev'];
21
30
  /** The write token may be injected ephemerally into fleet/cloud agents. */
22
31
  export function readWriteTokenEnv(env = process.env) {
23
32
  const token = env[SHARE_TOKEN_ENV_KEY]?.trim();
@@ -31,7 +31,7 @@ export interface ResolvedShareTarget {
31
31
  /**
32
32
  * Normalize any of the three accepted target forms to the R2 key that publish
33
33
  * would have written:
34
- * - a full share URL: `https://share.agents-cli.sh/<user>/<slug>`
34
+ * - a full share URL: `https://share.getrush.ai/<user>/<slug>`
35
35
  * - `<user>/<slug>`
36
36
  * - a bare `<slug>` — resolved against the caller's own namespace exactly as
37
37
  * `publishToEndpoint` resolves it at publish time (resolveShareUsername +
@@ -13,7 +13,7 @@ import { extractShareHttpError, formatShareHttpErrorDetail } from './http-error.
13
13
  /**
14
14
  * Normalize any of the three accepted target forms to the R2 key that publish
15
15
  * would have written:
16
- * - a full share URL: `https://share.agents-cli.sh/<user>/<slug>`
16
+ * - a full share URL: `https://share.getrush.ai/<user>/<slug>`
17
17
  * - `<user>/<slug>`
18
18
  * - a bare `<slug>` — resolved against the caller's own namespace exactly as
19
19
  * `publishToEndpoint` resolves it at publish time (resolveShareUsername +
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Make an HTML page self-contained enough to view on share.agents-cli.sh.
2
+ * Make an HTML page self-contained enough to view on share.getrush.ai.
3
3
  *
4
4
  * Chrome "Save as" (and some renderers) leave `file:///…html#section` TOC
5
5
  * links and sibling image paths. Those work on disk and 404 on the share
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Make an HTML page self-contained enough to view on share.agents-cli.sh.
2
+ * Make an HTML page self-contained enough to view on share.getrush.ai.
3
3
  *
4
4
  * Chrome "Save as" (and some renderers) leave `file:///…html#section` TOC
5
5
  * links and sibling image paths. Those work on disk and 404 on the share
@@ -110,6 +110,6 @@ export declare function setWorkerSecret(apiToken: string, accountId: string, wor
110
110
  export declare function enableWorkersDev(apiToken: string, accountId: string, workerName: string, opts?: ProvisionOptions): Promise<string>;
111
111
  /** Resolve a zone id for a domain the token can see, or null if not owned/visible. */
112
112
  export declare function findZoneId(apiToken: string, domain: string, opts?: ProvisionOptions): Promise<string | null>;
113
- /** Map a custom hostname (e.g. `share.agents-cli.sh`) to the Worker via Workers Custom Domains. */
113
+ /** Map a custom hostname (e.g. `share.getrush.ai`) to the Worker via Workers Custom Domains. */
114
114
  export declare function addCustomDomain(apiToken: string, accountId: string, workerName: string, zoneId: string, hostname: string, opts?: ProvisionOptions): Promise<void>;
115
115
  export {};
@@ -219,7 +219,7 @@ export async function enableWorkersDev(apiToken, accountId, workerName, opts = {
219
219
  /** Resolve a zone id for a domain the token can see, or null if not owned/visible. */
220
220
  export async function findZoneId(apiToken, domain, opts = {}) {
221
221
  const request = opts.request ?? defaultCloudflareRequester;
222
- // Try the exact name, then the registrable parent (share.agents-cli.sh -> agents-cli.sh).
222
+ // Try the exact name, then the registrable parent (share.getrush.ai -> getrush.ai).
223
223
  const candidates = [domain, domain.split('.').slice(-2).join('.')];
224
224
  for (const name of candidates) {
225
225
  const zones = await request({
@@ -232,7 +232,7 @@ export async function findZoneId(apiToken, domain, opts = {}) {
232
232
  }
233
233
  return null;
234
234
  }
235
- /** Map a custom hostname (e.g. `share.agents-cli.sh`) to the Worker via Workers Custom Domains. */
235
+ /** Map a custom hostname (e.g. `share.getrush.ai`) to the Worker via Workers Custom Domains. */
236
236
  export async function addCustomDomain(apiToken, accountId, workerName, zoneId, hostname, opts = {}) {
237
237
  const request = opts.request ?? defaultCloudflareRequester;
238
238
  try {