@phnx-labs/agents-cli 1.22.52 → 1.22.54

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 (180) hide show
  1. package/CHANGELOG.md +336 -0
  2. package/README.md +42 -9
  3. package/dist/bootstrap.js +55 -154
  4. package/dist/cli/command-registry.d.ts +5 -0
  5. package/dist/cli/command-registry.js +8 -1
  6. package/dist/commands/accounts.js +220 -174
  7. package/dist/commands/apply.js +6 -3
  8. package/dist/commands/auth-mint.d.ts +8 -0
  9. package/dist/commands/auth-mint.js +96 -0
  10. package/dist/commands/auth.js +5 -1
  11. package/dist/commands/browser.js +1 -1
  12. package/dist/commands/cost.js +8 -2
  13. package/dist/commands/daemon.js +2 -2
  14. package/dist/commands/doctor.js +6 -1
  15. package/dist/commands/exec.js +26 -17
  16. package/dist/commands/fleet-capture.js +7 -0
  17. package/dist/commands/focus.d.ts +1 -0
  18. package/dist/commands/focus.js +4 -2
  19. package/dist/commands/go.d.ts +5 -4
  20. package/dist/commands/go.js +8 -7
  21. package/dist/commands/insights.js +9 -0
  22. package/dist/commands/monitors.js +85 -30
  23. package/dist/commands/output.js +8 -2
  24. package/dist/commands/repo.js +18 -0
  25. package/dist/commands/secrets.js +33 -14
  26. package/dist/commands/sessions-inject.js +8 -3
  27. package/dist/commands/sessions-picker.js +2 -1
  28. package/dist/commands/sessions.d.ts +20 -12
  29. package/dist/commands/sessions.js +94 -40
  30. package/dist/commands/setup-accounts.d.ts +8 -0
  31. package/dist/commands/setup-accounts.js +47 -0
  32. package/dist/commands/setup.d.ts +1 -1
  33. package/dist/commands/setup.js +11 -2
  34. package/dist/commands/share.d.ts +52 -3
  35. package/dist/commands/share.js +262 -18
  36. package/dist/commands/ssh.d.ts +7 -0
  37. package/dist/commands/ssh.js +53 -14
  38. package/dist/commands/status.js +14 -0
  39. package/dist/commands/sync.js +44 -0
  40. package/dist/commands/view.d.ts +3 -1
  41. package/dist/commands/view.js +5 -4
  42. package/dist/lib/account-registry.d.ts +15 -5
  43. package/dist/lib/account-registry.js +165 -53
  44. package/dist/lib/accounting/rotate.d.ts +20 -6
  45. package/dist/lib/accounting/rotate.js +38 -7
  46. package/dist/lib/accounting/usage.d.ts +37 -1
  47. package/dist/lib/accounting/usage.js +71 -6
  48. package/dist/lib/agent-spec/agents.d.ts +5 -2
  49. package/dist/lib/agent-spec/agents.js +25 -7
  50. package/dist/lib/analytics/mix-commands.js +12 -6
  51. package/dist/lib/answer-router.js +2 -1
  52. package/dist/lib/auth-mint.d.ts +150 -0
  53. package/dist/lib/auth-mint.js +434 -0
  54. package/dist/lib/browser/profiles.d.ts +18 -0
  55. package/dist/lib/browser/profiles.js +26 -1
  56. package/dist/lib/browser/registry.d.ts +44 -14
  57. package/dist/lib/browser/registry.js +141 -45
  58. package/dist/lib/browser/remote-control.d.ts +9 -7
  59. package/dist/lib/browser/remote-control.js +9 -7
  60. package/dist/lib/claude-account-token.d.ts +10 -0
  61. package/dist/lib/claude-account-token.js +14 -4
  62. package/dist/lib/config-drift.d.ts +37 -0
  63. package/dist/lib/config-drift.js +72 -0
  64. package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
  65. package/dist/lib/daemon/auth-sync-service.js +34 -0
  66. package/dist/lib/daemon/daemon.js +30 -4
  67. package/dist/lib/daemon/runner.js +10 -2
  68. package/dist/lib/daemon-services.d.ts +1 -1
  69. package/dist/lib/daemon-services.js +5 -0
  70. package/dist/lib/device-config.d.ts +3 -3
  71. package/dist/lib/device-config.js +8 -7
  72. package/dist/lib/devices/config-migration.js +147 -1
  73. package/dist/lib/devices/connect.d.ts +26 -0
  74. package/dist/lib/devices/connect.js +48 -1
  75. package/dist/lib/devices/device-docs.d.ts +35 -0
  76. package/dist/lib/devices/device-docs.js +163 -0
  77. package/dist/lib/devices/discovery-policy.d.ts +14 -2
  78. package/dist/lib/devices/discovery-policy.js +31 -21
  79. package/dist/lib/devices/doctor-findings.d.ts +5 -1
  80. package/dist/lib/devices/doctor-findings.js +19 -1
  81. package/dist/lib/devices/registry.d.ts +11 -5
  82. package/dist/lib/devices/registry.js +46 -18
  83. package/dist/lib/exec.d.ts +88 -30
  84. package/dist/lib/exec.js +138 -34
  85. package/dist/lib/feed/feed.d.ts +10 -2
  86. package/dist/lib/feed/feed.js +35 -2
  87. package/dist/lib/feed-broadcast.js +1 -1
  88. package/dist/lib/fleet/apply.d.ts +11 -0
  89. package/dist/lib/fleet/apply.js +23 -3
  90. package/dist/lib/fleet/auth-sync.js +5 -3
  91. package/dist/lib/help.d.ts +9 -0
  92. package/dist/lib/help.js +29 -1
  93. package/dist/lib/hosts/dispatch.d.ts +4 -3
  94. package/dist/lib/hosts/dispatch.js +12 -8
  95. package/dist/lib/hosts/passthrough.d.ts +1 -10
  96. package/dist/lib/hosts/passthrough.js +1 -13
  97. package/dist/lib/hosts/providers/local.d.ts +9 -3
  98. package/dist/lib/hosts/providers/local.js +23 -12
  99. package/dist/lib/hosts/reconnect.d.ts +7 -4
  100. package/dist/lib/hosts/reconnect.js +29 -25
  101. package/dist/lib/hosts/registry.js +4 -1
  102. package/dist/lib/hosts/remote-os.js +3 -1
  103. package/dist/lib/installations/versions.js +9 -1
  104. package/dist/lib/linux-userns.d.ts +58 -0
  105. package/dist/lib/linux-userns.js +116 -0
  106. package/dist/lib/memory.d.ts +26 -0
  107. package/dist/lib/memory.js +80 -1
  108. package/dist/lib/monitors/config.d.ts +11 -0
  109. package/dist/lib/monitors/config.js +8 -0
  110. package/dist/lib/monitors/engine.js +8 -1
  111. package/dist/lib/monitors/state.d.ts +37 -1
  112. package/dist/lib/monitors/state.js +79 -4
  113. package/dist/lib/permissions-registry.d.ts +2 -0
  114. package/dist/lib/permissions-registry.js +116 -14
  115. package/dist/lib/permissions.d.ts +5 -3
  116. package/dist/lib/permissions.js +25 -27
  117. package/dist/lib/profiles.d.ts +8 -7
  118. package/dist/lib/profiles.js +12 -0
  119. package/dist/lib/project-key.d.ts +9 -0
  120. package/dist/lib/project-key.js +11 -0
  121. package/dist/lib/secrets/bundles.d.ts +35 -0
  122. package/dist/lib/secrets/bundles.js +78 -1
  123. package/dist/lib/secrets/push.d.ts +3 -8
  124. package/dist/lib/secrets/push.js +18 -14
  125. package/dist/lib/secrets/remote.d.ts +9 -18
  126. package/dist/lib/secrets/remote.js +11 -26
  127. package/dist/lib/secrets/reserved-sync.d.ts +65 -0
  128. package/dist/lib/secrets/reserved-sync.js +129 -0
  129. package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
  130. package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
  131. package/dist/lib/self-heal/registry.js +4 -0
  132. package/dist/lib/self-heal/types.d.ts +1 -1
  133. package/dist/lib/session/active.d.ts +10 -1
  134. package/dist/lib/session/active.js +8 -5
  135. package/dist/lib/session/actor-sidecar.d.ts +7 -0
  136. package/dist/lib/session/actor-sidecar.js +2 -0
  137. package/dist/lib/session/db.d.ts +39 -4
  138. package/dist/lib/session/db.js +168 -31
  139. package/dist/lib/session/discover.d.ts +32 -4
  140. package/dist/lib/session/discover.js +126 -37
  141. package/dist/lib/session/insights.d.ts +14 -0
  142. package/dist/lib/session/insights.js +25 -2
  143. package/dist/lib/session/linear.js +1 -1
  144. package/dist/lib/session/live-metadata.js +1 -0
  145. package/dist/lib/session/pid-registry.d.ts +7 -0
  146. package/dist/lib/session/prompt.d.ts +15 -0
  147. package/dist/lib/session/prompt.js +21 -0
  148. package/dist/lib/session/shell-programs.d.ts +17 -0
  149. package/dist/lib/session/shell-programs.js +21 -0
  150. package/dist/lib/session/state.js +2 -1
  151. package/dist/lib/session/stream-render.js +2 -1
  152. package/dist/lib/session/tool-calls.js +2 -5
  153. package/dist/lib/session/trajectory-html.js +2 -1
  154. package/dist/lib/session/trajectory.js +3 -12
  155. package/dist/lib/session/types.d.ts +25 -0
  156. package/dist/lib/session/types.js +10 -0
  157. package/dist/lib/share/publish.d.ts +53 -5
  158. package/dist/lib/share/publish.js +99 -17
  159. package/dist/lib/share/worker-template.js +594 -64
  160. package/dist/lib/startup/root-command.js +2 -1
  161. package/dist/lib/state.d.ts +24 -0
  162. package/dist/lib/state.js +318 -54
  163. package/dist/lib/sync-status.d.ts +4 -0
  164. package/dist/lib/sync-status.js +3 -0
  165. package/dist/lib/terminal/resolve.d.ts +7 -0
  166. package/dist/lib/terminal/resolve.js +41 -2
  167. package/dist/lib/traces/classify.js +24 -19
  168. package/dist/lib/traces/insights.d.ts +67 -0
  169. package/dist/lib/traces/insights.js +178 -0
  170. package/dist/lib/traces/phenotype.d.ts +67 -0
  171. package/dist/lib/traces/phenotype.js +437 -0
  172. package/dist/lib/traces/segments.d.ts +133 -0
  173. package/dist/lib/traces/segments.js +301 -0
  174. package/dist/lib/traces/sync.d.ts +33 -0
  175. package/dist/lib/traces/sync.js +11 -2
  176. package/dist/lib/types.d.ts +47 -1
  177. package/dist/lib/usage-refresh.js +2 -1
  178. package/dist/lib/view-types.d.ts +2 -0
  179. package/dist/lib/watchdog/runner.js +18 -4
  180. package/package.json +2 -1
@@ -104,6 +104,8 @@ interface ClaudeSessionScan {
104
104
  entrypoint?: string;
105
105
  /** Concatenated user message text, ready to hand to FTS5. */
106
106
  contentText?: string;
107
+ /** Concatenated assistant-answer text, ready to hand to FTS5's `assistant` column. */
108
+ assistantText?: string;
107
109
  /** Durable state signals persisted to the index by the session-state engine. */
108
110
  prUrl?: string;
109
111
  prNumber?: number;
@@ -154,6 +156,7 @@ interface CodexSessionScan {
154
156
  durationMs?: number;
155
157
  lastActivity?: string;
156
158
  contentText?: string;
159
+ assistantText?: string;
157
160
  prUrl?: string;
158
161
  prNumber?: number;
159
162
  worktreeSlug?: string;
@@ -347,8 +350,15 @@ export declare function looksLikeSessionId(query: string): boolean;
347
350
  */
348
351
  export declare function resolveSessionById(sessions: SessionMeta[], idQuery: string): SessionMeta[];
349
352
  /**
350
- * Run an FTS5 search over the DB and intersect with the given session list,
351
- * preserving the existing SessionMeta[] contract so sessions.ts is unchanged.
353
+ * Run an FTS5 search over the DB and union hits with the given session list.
354
+ *
355
+ * The listing pool is a minority of the index — cwd-scoped, default-capped at
356
+ * 50, and skipping whole classes of indexed transcript — so intersecting FTS
357
+ * hits with it dropped grep-visible sessions that the index already found
358
+ * (PHNX-2767: `agents sessions "tmux pane"` returned 0 while the project
359
+ * transcripts matched). Hits already in the pool keep the caller's SessionMeta;
360
+ * hits the pool missed are hydrated from the index so a content query returns
361
+ * the transcript FTS matched.
352
362
  */
353
363
  export declare function searchContentIndex(sessions: SessionMeta[], query: string): Map<string, SessionMeta>;
354
364
  /**
@@ -375,6 +385,13 @@ interface PreStatEntry {
375
385
  * stat. Same debounce and change-detection as filterChangedFiles; the raw
376
386
  * mtime is floored here so warm files match the ledger exactly as the stat path
377
387
  * does (Math.floor(stat.mtimeMs)).
388
+ *
389
+ * A file is also treated as "changed" — independent of (mtime, size) — when
390
+ * its ledger row's `extractor_version` is behind {@link CONTENT_INDEX_VERSION}.
391
+ * This is the lever that backfills a content-extractor improvement (e.g.
392
+ * indexing assistant answers, not just user prompts) into every already-scanned
393
+ * session on its next pass, without a `DELETE FROM scan_ledger` that would also
394
+ * discard the Claude/Codex resumable parser_state.
378
395
  */
379
396
  export declare function filterChangedEntries(entries: PreStatEntry[]): Array<{
380
397
  filePath: string;
@@ -429,9 +446,11 @@ export declare function parseCodexThreadNameIndex(raw: string): Map<string, stri
429
446
  export declare function readCodexMeta(filePath: string, resolveAccount?: () => string | undefined, currentVersion?: string, scanStamp?: ScanStamp, priorRow?: {
430
447
  parserState: string | null;
431
448
  fileMtimeMs: number;
449
+ extractorVersion?: number | null;
432
450
  }): Promise<{
433
451
  meta: SessionMeta;
434
452
  content: string;
453
+ assistantContent: string;
435
454
  parserState?: string;
436
455
  contentText?: string;
437
456
  toolCalls?: IndexedToolCall[];
@@ -469,6 +488,10 @@ interface ClaudeParseState {
469
488
  lastTsMs?: number;
470
489
  seenAssistantIds: Set<string>;
471
490
  userTexts: string[];
491
+ /** Assistant-answer text (#PHNX content-search), accumulated the same way as
492
+ * `userTexts` but indexed into session_text's own lower-weighted `assistant`
493
+ * column instead of `content` — see BM25_WEIGHTS. */
494
+ assistantTexts: string[];
472
495
  sawPrCreate: boolean;
473
496
  prUrl?: string;
474
497
  prNumber?: number;
@@ -521,7 +544,7 @@ export declare function scanClaudeSession(filePath: string): Promise<ClaudeSessi
521
544
  * exact even when the recent window is smaller than the true count.
522
545
  */
523
546
  export interface ClaudeParserState {
524
- v: 4;
547
+ v: 5;
525
548
  /**
526
549
  * Fan-out tallies carried across a RESUMED parse (RUSH-3091/3095). They must
527
550
  * live in the durable blob: the resumable parser reads only new bytes, so
@@ -566,6 +589,7 @@ export interface ClaudeParserState {
566
589
  spawnedTeam?: string;
567
590
  ticketId?: string;
568
591
  contentText?: string;
592
+ assistantContentText?: string;
569
593
  checklistEvents: SessionEvent[];
570
594
  recentDirectoriesTouched: string[];
571
595
  toolCalls: ToolCallCollectorSnapshot;
@@ -663,6 +687,8 @@ interface CodexParseState {
663
687
  firstTsMs?: number;
664
688
  lastTsMs?: number;
665
689
  userTexts: string[];
690
+ /** See ClaudeParseState.assistantTexts — same accumulate-and-lower-weight FTS treatment. */
691
+ assistantTexts: string[];
666
692
  sawPrCreate: boolean;
667
693
  prUrl?: string;
668
694
  prNumber?: number;
@@ -693,7 +719,7 @@ export declare function initCodexParseState(): CodexParseState;
693
719
  * finalize is identical after a resume.
694
720
  */
695
721
  export interface CodexParserState {
696
- v: 2;
722
+ v: 3;
697
723
  offset: number;
698
724
  jsonlDroppingOversizedLine?: boolean;
699
725
  sessionId?: string;
@@ -716,6 +742,7 @@ export interface CodexParserState {
716
742
  spawnedTeam?: string;
717
743
  ticketId?: string;
718
744
  contentText?: string;
745
+ assistantContentText?: string;
719
746
  checklistEvents: SessionEvent[];
720
747
  recentDirectoriesTouched: string[];
721
748
  toolCalls: ToolCallCollectorSnapshot;
@@ -772,6 +799,7 @@ export declare function __resetCodexScanBranchCountsForTest(): void;
772
799
  export declare function readCursorMeta(filePath: string, currentVersion?: string): {
773
800
  meta: SessionMeta;
774
801
  content: string;
802
+ assistantContent: string;
775
803
  events: SessionEvent[];
776
804
  } | null;
777
805
  /** Parse a single Kimi session state.json file to extract session metadata. */
@@ -26,7 +26,7 @@ import { getConfigSymlinkVersion } from '../installations/shims.js';
26
26
  import { SESSION_AGENTS } from './types.js';
27
27
  import { deriveShortId } from './short-id.js';
28
28
  import { buildClaudeAccountIndex, resolveClaudeAccount } from './claude-accounts.js';
29
- import { extractSessionTopic, extractSlashCommandName, extractSlashCommandFromToolInput, classifyUserPrompt } from './prompt.js';
29
+ import { extractSessionTopic, extractSlashCommandName, extractSlashCommandFromToolInput, cleanGeneratedSessionLabel } from './prompt.js';
30
30
  import { isBackgroundShellStart, isSkillInvocation, extractSkills, extractSlashCommands, isSubAgentTool } from './highlights.js';
31
31
  import { parseAntigravity, parseCursor, splitSessionFilePath } from './parse.js';
32
32
  import { extractPrUrl, detectWorktree, detectTicket, isPrCreateCommand, detectSpawnedTeam, isTicketCreateTool, extractCreatedTicket, extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
@@ -34,7 +34,7 @@ import { costOfUsage, costOfUsageNoCache } from '../pricing/index.js';
34
34
  import { machineForSessionFile } from './origin-machine.js';
35
35
  export { machineForSessionFile } from './origin-machine.js';
36
36
  import { mapBounded } from '../concurrency.js';
37
- import { getDB, getScanStampByPath, getScanStampsForPaths, getParserStatesForPaths, getDirLedgerForPaths, recordDirScans, recordScans, syncLabels, seedLabelsFromNames, syncTopics, upsertSessionsBatch, querySessions, countSessions, ftsSearch, tryClaimScan, releaseScan, scanInProgressByLivePid, cacheLinearProject, } from './db.js';
37
+ import { getDB, getScanStampByPath, getScanStampsForPaths, getParserStatesForPaths, getDirLedgerForPaths, recordDirScans, recordScans, syncLabels, seedLabelsFromNames, syncTopics, upsertSessionsBatch, querySessions, countSessions, ftsSearch, getSessionById, tryClaimScan, releaseScan, scanInProgressByLivePid, cacheLinearProject, CONTENT_INDEX_VERSION, } from './db.js';
38
38
  import { buildRunNameMap } from './run-names.js';
39
39
  import { resolveLinearApiKey } from '../linear-cache.js';
40
40
  import { ToolCallCollector, collectClaudeToolCalls, collectCodexToolCalls, } from './tool-calls.js';
@@ -628,8 +628,15 @@ export function resolveSessionById(sessions, idQuery) {
628
628
  // Content-index search (FTS5-backed)
629
629
  // ---------------------------------------------------------------------------
630
630
  /**
631
- * Run an FTS5 search over the DB and intersect with the given session list,
632
- * preserving the existing SessionMeta[] contract so sessions.ts is unchanged.
631
+ * Run an FTS5 search over the DB and union hits with the given session list.
632
+ *
633
+ * The listing pool is a minority of the index — cwd-scoped, default-capped at
634
+ * 50, and skipping whole classes of indexed transcript — so intersecting FTS
635
+ * hits with it dropped grep-visible sessions that the index already found
636
+ * (PHNX-2767: `agents sessions "tmux pane"` returned 0 while the project
637
+ * transcripts matched). Hits already in the pool keep the caller's SessionMeta;
638
+ * hits the pool missed are hydrated from the index so a content query returns
639
+ * the transcript FTS matched.
633
640
  */
634
641
  export function searchContentIndex(sessions, query) {
635
642
  if (!query.trim())
@@ -640,13 +647,14 @@ export function searchContentIndex(sessions, query) {
640
647
  const byId = new Map(sessions.map(s => [s.id, s]));
641
648
  const result = new Map();
642
649
  for (const hit of hits) {
643
- const session = byId.get(hit.sessionId);
650
+ const session = byId.get(hit.sessionId) ?? getSessionById(hit.sessionId);
644
651
  if (!session)
645
652
  continue;
646
653
  result.set(hit.sessionId, {
647
654
  ...session,
648
655
  _matchedTerms: hit.matchedTerms,
649
656
  _bm25Score: hit.score,
657
+ snippet: hit.snippet,
650
658
  });
651
659
  }
652
660
  return result;
@@ -678,6 +686,13 @@ export function filterChangedFiles(filePaths) {
678
686
  * stat. Same debounce and change-detection as filterChangedFiles; the raw
679
687
  * mtime is floored here so warm files match the ledger exactly as the stat path
680
688
  * does (Math.floor(stat.mtimeMs)).
689
+ *
690
+ * A file is also treated as "changed" — independent of (mtime, size) — when
691
+ * its ledger row's `extractor_version` is behind {@link CONTENT_INDEX_VERSION}.
692
+ * This is the lever that backfills a content-extractor improvement (e.g.
693
+ * indexing assistant answers, not just user prompts) into every already-scanned
694
+ * session on its next pass, without a `DELETE FROM scan_ledger` that would also
695
+ * discard the Claude/Codex resumable parser_state.
681
696
  */
682
697
  export function filterChangedEntries(entries) {
683
698
  const ledger = getScanStampsForPaths(entries.map(e => e.filePath));
@@ -689,10 +704,11 @@ export function filterChangedEntries(entries) {
689
704
  fileSize: entry.fileSize,
690
705
  };
691
706
  const prev = ledger.get(entry.filePath);
692
- if (prev && prev.fileMtimeMs === scan.fileMtimeMs && prev.fileSize === scan.fileSize) {
707
+ const contentIndexStale = prev?.extractorVersion !== CONTENT_INDEX_VERSION;
708
+ if (prev && prev.fileMtimeMs === scan.fileMtimeMs && prev.fileSize === scan.fileSize && !contentIndexStale) {
693
709
  continue;
694
710
  }
695
- if (prev && shouldDeferRecentAppend(prev, scan, now)) {
711
+ if (prev && !contentIndexStale && shouldDeferRecentAppend(prev, scan, now)) {
696
712
  continue;
697
713
  }
698
714
  out.push({ filePath: entry.filePath, scan });
@@ -1000,6 +1016,7 @@ async function scanRoutineArchivesIncremental(agent, onProgress) {
1000
1016
  entries.push({
1001
1017
  meta: result.meta,
1002
1018
  content: result.content,
1019
+ assistantContent: result.assistantContent,
1003
1020
  scan,
1004
1021
  events: result.events,
1005
1022
  toolCalls: result.toolCalls,
@@ -1152,6 +1169,7 @@ async function scanClaudeIncremental(onProgress) {
1152
1169
  entries.push({
1153
1170
  meta: result.meta,
1154
1171
  content: result.content,
1172
+ assistantContent: result.assistantContent,
1155
1173
  scan,
1156
1174
  parserState: result.parserState,
1157
1175
  contentText: result.contentText,
@@ -1185,7 +1203,12 @@ async function scanClaudeIncremental(onProgress) {
1185
1203
  * next scan.
1186
1204
  */
1187
1205
  async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, label) {
1188
- const prior = parsePriorClaudeState(priorRow);
1206
+ // A prior continuation extracted at an older CONTENT_INDEX_VERSION is missing
1207
+ // whatever the current extractor adds (e.g. assistant-answer text) — resuming
1208
+ // from it would fold only the NEWLY appended lines into that gap, never
1209
+ // backfilling the file's earlier assistant text. Treat it as absent so the
1210
+ // file gets one full from-offset-0 reparse instead.
1211
+ const prior = priorRow?.extractorVersion === CONTENT_INDEX_VERSION ? parsePriorClaudeState(priorRow) : null;
1189
1212
  const { scan, newState, toolCalls, mode } = await scanClaudeSessionResumable(filePath, prior, scanStamp.fileMtimeMs, scanStamp.fileSize, priorRow?.fileMtimeMs);
1190
1213
  if (mode === 'incremental')
1191
1214
  claudeIncrementalScanCount++;
@@ -1290,6 +1313,7 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, label) {
1290
1313
  return {
1291
1314
  meta,
1292
1315
  content: scan.contentText || '',
1316
+ assistantContent: scan.assistantText || '',
1293
1317
  // Persist the continuation so the next scan of this file can resume from the
1294
1318
  // offset instead of a full reparse. content_text is the same accumulated user
1295
1319
  // doc, cached so the resume can hydrate userTexts without re-reading the file.
@@ -1431,6 +1455,7 @@ async function scanCodexIncremental(onProgress) {
1431
1455
  entries.push({
1432
1456
  meta: result.meta,
1433
1457
  content: result.content,
1458
+ assistantContent: result.assistantContent,
1434
1459
  scan,
1435
1460
  parserState: result.parserState,
1436
1461
  contentText: result.contentText,
@@ -1541,7 +1566,10 @@ export async function readCodexMeta(filePath, resolveAccount, currentVersion, sc
1541
1566
  let toolCalls;
1542
1567
  let toolIndexMode = 'replace';
1543
1568
  if (scanStamp) {
1544
- const prior = parsePriorCodexState(priorRow);
1569
+ // See readClaudeMeta: a prior continuation from an older content extractor
1570
+ // is missing what the current extractor adds, so it must not be resumed
1571
+ // from — treat it as absent and force one full reparse.
1572
+ const prior = priorRow?.extractorVersion === CONTENT_INDEX_VERSION ? parsePriorCodexState(priorRow) : null;
1545
1573
  const result = await scanCodexSessionResumable(filePath, prior, scanStamp.fileMtimeMs, scanStamp.fileSize, priorRow?.fileMtimeMs);
1546
1574
  if (result.mode === 'incremental')
1547
1575
  codexIncrementalScanCount++;
@@ -1597,6 +1625,7 @@ export async function readCodexMeta(filePath, resolveAccount, currentVersion, sc
1597
1625
  return {
1598
1626
  meta,
1599
1627
  content: scan.contentText || '',
1628
+ assistantContent: scan.assistantText || '',
1600
1629
  // Persist the continuation so the next scan of this rollout resumes from the
1601
1630
  // offset instead of a full reparse; content_text caches the accumulated user
1602
1631
  // doc for the resume's hydrate. Absent when no stamp was supplied.
@@ -1675,7 +1704,7 @@ async function scanGeminiIncremental(onProgress) {
1675
1704
  const result = readGeminiMeta(filePath, hashDir, projectMap, currentVersion);
1676
1705
  if (result && !seen.has(result.meta.id)) {
1677
1706
  seen.add(result.meta.id);
1678
- entries.push({ meta: result.meta, content: result.content, scan });
1707
+ entries.push({ meta: result.meta, content: result.content, assistantContent: result.assistantContent, scan });
1679
1708
  }
1680
1709
  else {
1681
1710
  // Gemini file without a sessionId — record scan so we don't re-parse it next run.
@@ -1729,6 +1758,7 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
1729
1758
  let firstTsMs;
1730
1759
  let lastTsMs;
1731
1760
  const userTexts = [];
1761
+ const assistantTexts = [];
1732
1762
  for (const message of messages) {
1733
1763
  if (message.type === 'user') {
1734
1764
  const text = extractGeminiMessageText(message.content);
@@ -1740,8 +1770,10 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
1740
1770
  }
1741
1771
  }
1742
1772
  else if (message.type === 'gemini') {
1743
- if (extractGeminiMessageText(message.content)) {
1773
+ const text = extractGeminiMessageText(message.content);
1774
+ if (text) {
1744
1775
  messageCount++;
1776
+ assistantTexts.push(text);
1745
1777
  }
1746
1778
  }
1747
1779
  // Duration: messages carry a `timestamp` on most Gemini CLI versions.
@@ -1819,7 +1851,7 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
1819
1851
  costUsdNoCache: sawCost ? costUsdNoCache : undefined,
1820
1852
  durationMs,
1821
1853
  };
1822
- return { meta, content: userTexts.join('\n') };
1854
+ return { meta, content: userTexts.join('\n'), assistantContent: assistantTexts.join('\n') };
1823
1855
  }
1824
1856
  /** Build a hash-to-project mapping from Gemini's projects.json and history directories. */
1825
1857
  function buildGeminiProjectMap() {
@@ -2396,7 +2428,7 @@ async function scanRushIncremental(onProgress) {
2396
2428
  const sessionId = path.basename(path.dirname(filePath));
2397
2429
  const result = await readRushMeta(filePath, sessionId);
2398
2430
  if (result) {
2399
- entries.push({ meta: result.meta, content: result.content, scan });
2431
+ entries.push({ meta: result.meta, content: result.content, assistantContent: result.assistantContent, scan });
2400
2432
  }
2401
2433
  else {
2402
2434
  touched.push({ filePath, scan });
@@ -2428,7 +2460,7 @@ async function readRushMeta(filePath, sessionId) {
2428
2460
  topic: scan.topic,
2429
2461
  messageCount: scan.messageCount,
2430
2462
  };
2431
- return { meta, content: scan.contentText || '' };
2463
+ return { meta, content: scan.contentText || '', assistantContent: scan.assistantText || '' };
2432
2464
  }
2433
2465
  /** Stream a Rush messages.jsonl file and extract scan-level metadata. */
2434
2466
  async function scanRushSession(filePath) {
@@ -2439,6 +2471,7 @@ async function scanRushSession(filePath) {
2439
2471
  let agentId;
2440
2472
  let messageCount = 0;
2441
2473
  const userTexts = [];
2474
+ const assistantTexts = [];
2442
2475
  try {
2443
2476
  for await (const line of rl) {
2444
2477
  if (!line.trim())
@@ -2475,6 +2508,9 @@ async function scanRushSession(filePath) {
2475
2508
  if (!topic)
2476
2509
  topic = extractSessionTopic(cleaned);
2477
2510
  }
2511
+ else if (parsed.role === 'assistant') {
2512
+ assistantTexts.push(cleaned);
2513
+ }
2478
2514
  }
2479
2515
  }
2480
2516
  finally {
@@ -2487,6 +2523,7 @@ async function scanRushSession(filePath) {
2487
2523
  agentId,
2488
2524
  messageCount,
2489
2525
  contentText: userTexts.length > 0 ? userTexts.join('\n') : undefined,
2526
+ assistantText: assistantTexts.length > 0 ? assistantTexts.join('\n') : undefined,
2490
2527
  };
2491
2528
  }
2492
2529
  // ---------------------------------------------------------------------------
@@ -2529,7 +2566,7 @@ async function scanHermesIncremental(onProgress) {
2529
2566
  const result = readHermesMeta(filePath);
2530
2567
  if (result && !seen.has(result.meta.id)) {
2531
2568
  seen.add(result.meta.id);
2532
- scanEntries.push({ meta: result.meta, content: result.content, scan });
2569
+ scanEntries.push({ meta: result.meta, content: result.content, assistantContent: result.assistantContent, scan });
2533
2570
  }
2534
2571
  else {
2535
2572
  touched.push({ filePath, scan });
@@ -2558,6 +2595,7 @@ function readHermesMeta(filePath) {
2558
2595
  return null;
2559
2596
  const messages = Array.isArray(session.messages) ? session.messages : [];
2560
2597
  const userTexts = [];
2598
+ const assistantTexts = [];
2561
2599
  let topic;
2562
2600
  let messageCount = 0;
2563
2601
  for (const msg of messages) {
@@ -2570,6 +2608,9 @@ function readHermesMeta(filePath) {
2570
2608
  if (!topic)
2571
2609
  topic = extractSessionTopic(text);
2572
2610
  }
2611
+ else if (msg?.role === 'assistant') {
2612
+ assistantTexts.push(text);
2613
+ }
2573
2614
  }
2574
2615
  const stat = safeStatSync(filePath);
2575
2616
  const timestamp = typeof session.last_updated === 'string'
@@ -2592,7 +2633,7 @@ function readHermesMeta(filePath) {
2592
2633
  topic,
2593
2634
  messageCount: messageCount || (typeof session.message_count === 'number' ? session.message_count : undefined),
2594
2635
  };
2595
- return { meta, content: userTexts.join('\n') };
2636
+ return { meta, content: userTexts.join('\n'), assistantContent: assistantTexts.join('\n') };
2596
2637
  }
2597
2638
  /**
2598
2639
  * Muse Code stores one session per directory under
@@ -2660,7 +2701,7 @@ function scanMuseIncremental(onProgress) {
2660
2701
  const result = readMuseMeta(filePath);
2661
2702
  if (result && !seen.has(result.meta.id)) {
2662
2703
  seen.add(result.meta.id);
2663
- scanEntries.push({ meta: result.meta, content: result.content, scan });
2704
+ scanEntries.push({ meta: result.meta, content: result.content, assistantContent: result.assistantContent, scan });
2664
2705
  }
2665
2706
  else {
2666
2707
  touched.push({ filePath, scan });
@@ -2690,6 +2731,7 @@ function readMuseMeta(filePath) {
2690
2731
  return null;
2691
2732
  }
2692
2733
  const userTexts = [];
2734
+ const assistantTexts = [];
2693
2735
  let topic;
2694
2736
  let messageCount = 0;
2695
2737
  let model;
@@ -2734,6 +2776,9 @@ function readMuseMeta(filePath) {
2734
2776
  const event = payload?.event ?? payload;
2735
2777
  if (event?.kind === 'assistant_message_committed' && typeof event.text === 'string') {
2736
2778
  messageCount++;
2779
+ const text = event.text.trim();
2780
+ if (text)
2781
+ assistantTexts.push(text);
2737
2782
  }
2738
2783
  if (event?.kind === 'model_request_configured' && typeof event.model === 'string') {
2739
2784
  model = event.model;
@@ -2757,7 +2802,7 @@ function readMuseMeta(filePath) {
2757
2802
  topic,
2758
2803
  messageCount: messageCount || undefined,
2759
2804
  };
2760
- return { meta, content: userTexts.join('\n') };
2805
+ return { meta, content: userTexts.join('\n'), assistantContent: assistantTexts.join('\n') };
2761
2806
  }
2762
2807
  /** Extract plain text from a Hermes message content field (string or list of parts). */
2763
2808
  function extractHermesMessageText(content) {
@@ -2805,7 +2850,7 @@ async function scanDroidIncremental(onProgress) {
2805
2850
  const result = await readDroidMeta(filePath, currentVersion);
2806
2851
  if (result && !seen.has(result.meta.id)) {
2807
2852
  seen.add(result.meta.id);
2808
- entries.push({ meta: result.meta, content: result.content, scan });
2853
+ entries.push({ meta: result.meta, content: result.content, assistantContent: result.assistantContent, scan });
2809
2854
  }
2810
2855
  else {
2811
2856
  touched.push({ filePath, scan });
@@ -2868,7 +2913,7 @@ async function readDroidMeta(filePath, currentVersion) {
2868
2913
  costUsdNoCache: costUsd > 0 ? costUsdNoCache : undefined,
2869
2914
  durationMs: scan.durationMs,
2870
2915
  };
2871
- return { meta, content: scan.contentText || '' };
2916
+ return { meta, content: scan.contentText || '', assistantContent: scan.assistantText || '' };
2872
2917
  }
2873
2918
  /** Read model + token usage from a Droid `<uuid>.settings.json` sidecar. */
2874
2919
  function readDroidSettings(settingsPath) {
@@ -2911,6 +2956,7 @@ async function scanDroidSession(filePath) {
2911
2956
  let firstTsMs;
2912
2957
  let lastTsMs;
2913
2958
  const userTexts = [];
2959
+ const assistantTexts = [];
2914
2960
  try {
2915
2961
  for await (const line of rl) {
2916
2962
  if (!line.trim())
@@ -2960,6 +3006,9 @@ async function scanDroidSession(filePath) {
2960
3006
  if (!firstUserTopic)
2961
3007
  firstUserTopic = extractSessionTopic(text);
2962
3008
  }
3009
+ else if (msg.role === 'assistant') {
3010
+ assistantTexts.push(text);
3011
+ }
2963
3012
  }
2964
3013
  }
2965
3014
  finally {
@@ -2981,6 +3030,7 @@ async function scanDroidSession(filePath) {
2981
3030
  durationMs,
2982
3031
  lastActivity: lastTsMs !== undefined ? new Date(lastTsMs).toISOString() : undefined,
2983
3032
  contentText: userTexts.length > 0 ? userTexts.join('\n') : undefined,
3033
+ assistantText: assistantTexts.length > 0 ? assistantTexts.join('\n') : undefined,
2984
3034
  };
2985
3035
  }
2986
3036
  /** Extract plain text from a Droid message content field (Anthropic-shaped blocks). */
@@ -3026,6 +3076,7 @@ export function initClaudeParseState() {
3026
3076
  lastTsMs: undefined,
3027
3077
  seenAssistantIds: new Set(),
3028
3078
  userTexts: [],
3079
+ assistantTexts: [],
3029
3080
  sawPrCreate: false,
3030
3081
  prUrl: undefined,
3031
3082
  prNumber: undefined,
@@ -3217,6 +3268,9 @@ export function applyClaudeLine(state, parsed) {
3217
3268
  return;
3218
3269
  state.seenAssistantIds.add(logicalId);
3219
3270
  state.messageCount++;
3271
+ const assistantText = extractClaudeAssistantText(parsed);
3272
+ if (assistantText)
3273
+ state.assistantTexts.push(assistantText);
3220
3274
  const usageObj = parsed.message?.usage || parsed.usage;
3221
3275
  const usage = getClaudeUsageTotal(usageObj);
3222
3276
  if (usage !== null) {
@@ -3268,15 +3322,10 @@ export function finalizeClaudeScan(state) {
3268
3322
  // A topic is the first meaningful prompt. Harness-owned names travel in the
3269
3323
  // separate label field so consumers can replace an early topic once Claude's
3270
3324
  // generated title (or a later `/rename`) arrives.
3271
- // Claude's generated `ai-title` is derived from the first turn, so a session
3272
- // opened with a skill gets named after the injected "Base directory for this
3273
- // skill: …" line instead of its task — and that name then wins on every
3274
- // surface for the session's whole life. `classifyUserPrompt` reports `skill`
3275
- // only for that injected line, so collapse it to `/<skill>`. A `/rename`
3276
- // (`custom-title`) is the user's own words and is never rewritten.
3277
- const generated = classifyUserPrompt(state.aiTitle ?? '');
3278
- const label = state.customTitle
3279
- || (generated.kind === 'skill' ? generated.clean : state.aiTitle);
3325
+ // Generated titles (Claude `ai-title`, Cursor `chatMeta.title`) go through
3326
+ // one shared cleaner so a skill-preamble echo collapses to `/<skill>`. A
3327
+ // `/rename` (`custom-title`) is the user's own words and is never rewritten.
3328
+ const label = state.customTitle || cleanGeneratedSessionLabel(state.aiTitle);
3280
3329
  const worktree = detectWorktree(state.cwd, state.gitBranch);
3281
3330
  const ticket = detectTicket(state.userTexts.join('\n') || undefined, state.gitBranch);
3282
3331
  return {
@@ -3300,6 +3349,7 @@ export function finalizeClaudeScan(state) {
3300
3349
  durationMs,
3301
3350
  lastActivity: state.lastTsMs !== undefined ? new Date(state.lastTsMs).toISOString() : undefined,
3302
3351
  contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
3352
+ assistantText: state.assistantTexts.length > 0 ? state.assistantTexts.join('\n') : undefined,
3303
3353
  prUrl: state.prUrl,
3304
3354
  prNumber: state.prNumber,
3305
3355
  worktreeSlug: worktree?.slug,
@@ -3354,7 +3404,7 @@ export function serializeClaudeParserState(state, offset, jsonlDroppingOversized
3354
3404
  : allIds;
3355
3405
  const ticket = detectTicket(state.userTexts.join('\n') || undefined, state.gitBranch);
3356
3406
  return {
3357
- v: 4,
3407
+ v: 5,
3358
3408
  subAgents: state.subAgents,
3359
3409
  backgroundShells: state.backgroundShells,
3360
3410
  offset,
@@ -3396,6 +3446,7 @@ export function serializeClaudeParserState(state, offset, jsonlDroppingOversized
3396
3446
  // not be persisted.
3397
3447
  ticketId: ticket?.id,
3398
3448
  contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
3449
+ assistantContentText: state.assistantTexts.length > 0 ? state.assistantTexts.join('\n') : undefined,
3399
3450
  checklistEvents: state.checklistEvents,
3400
3451
  recentDirectoriesTouched: state.recentDirectoriesTouched,
3401
3452
  toolCalls: state.toolCollector.snapshot(),
@@ -3454,6 +3505,9 @@ export function hydrateClaudeParseState(prior) {
3454
3505
  lastTsMs: prior.lastTsMs,
3455
3506
  seenAssistantIds: seen,
3456
3507
  userTexts: prior.contentText !== undefined && prior.contentText.length > 0 ? [prior.contentText] : [],
3508
+ assistantTexts: prior.assistantContentText !== undefined && prior.assistantContentText.length > 0
3509
+ ? [prior.assistantContentText]
3510
+ : [],
3457
3511
  sawPrCreate: prior.sawPrCreate,
3458
3512
  prUrl: prior.prUrl,
3459
3513
  prNumber: prior.prNumber,
@@ -3597,7 +3651,7 @@ function parsePriorClaudeState(row) {
3597
3651
  return null;
3598
3652
  try {
3599
3653
  const parsed = JSON.parse(row.parserState);
3600
- if (parsed?.v !== 4 || typeof parsed.offset !== 'number' || parsed.toolCalls?.v !== 1)
3654
+ if (parsed?.v !== 5 || typeof parsed.offset !== 'number' || parsed.toolCalls?.v !== 1)
3601
3655
  return null;
3602
3656
  return parsed;
3603
3657
  }
@@ -3634,6 +3688,7 @@ export function initCodexParseState() {
3634
3688
  firstTsMs: undefined,
3635
3689
  lastTsMs: undefined,
3636
3690
  userTexts: [],
3691
+ assistantTexts: [],
3637
3692
  sawPrCreate: false,
3638
3693
  prUrl: undefined,
3639
3694
  prNumber: undefined,
@@ -3743,6 +3798,9 @@ function applyCodexLine(state, parsed) {
3743
3798
  if (!state.topic)
3744
3799
  state.topic = extractSessionTopic(text);
3745
3800
  }
3801
+ else {
3802
+ state.assistantTexts.push(text);
3803
+ }
3746
3804
  return;
3747
3805
  }
3748
3806
  if (parsed.type === 'event_msg' && parsed.payload?.type === 'token_count') {
@@ -3818,6 +3876,7 @@ function finalizeCodexScan(state) {
3818
3876
  durationMs,
3819
3877
  lastActivity: state.lastTsMs !== undefined ? new Date(state.lastTsMs).toISOString() : undefined,
3820
3878
  contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
3879
+ assistantText: state.assistantTexts.length > 0 ? state.assistantTexts.join('\n') : undefined,
3821
3880
  prUrl: state.prUrl,
3822
3881
  prNumber: state.prNumber,
3823
3882
  worktreeSlug: worktree?.slug,
@@ -3861,7 +3920,7 @@ async function scanCodexSession(filePath) {
3861
3920
  export function serializeCodexParserState(state, offset, jsonlDroppingOversizedLine = false) {
3862
3921
  const ticket = detectTicket(state.userTexts.join('\n') || undefined, state.gitBranch);
3863
3922
  return {
3864
- v: 2,
3923
+ v: 3,
3865
3924
  offset,
3866
3925
  jsonlDroppingOversizedLine: jsonlDroppingOversizedLine || undefined,
3867
3926
  sessionId: state.sessionId,
@@ -3888,6 +3947,7 @@ export function serializeCodexParserState(state, offset, jsonlDroppingOversizedL
3888
3947
  // be persisted.
3889
3948
  ticketId: ticket?.id,
3890
3949
  contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
3950
+ assistantContentText: state.assistantTexts.length > 0 ? state.assistantTexts.join('\n') : undefined,
3891
3951
  checklistEvents: state.checklistEvents,
3892
3952
  recentDirectoriesTouched: state.recentDirectoriesTouched,
3893
3953
  toolCalls: state.toolCollector.snapshot(),
@@ -3918,6 +3978,9 @@ function hydrateCodexParseState(prior) {
3918
3978
  firstTsMs: prior.firstTsMs,
3919
3979
  lastTsMs: prior.lastTsMs,
3920
3980
  userTexts: prior.contentText !== undefined && prior.contentText.length > 0 ? [prior.contentText] : [],
3981
+ assistantTexts: prior.assistantContentText !== undefined && prior.assistantContentText.length > 0
3982
+ ? [prior.assistantContentText]
3983
+ : [],
3921
3984
  sawPrCreate: prior.sawPrCreate,
3922
3985
  prUrl: prior.prUrl,
3923
3986
  prNumber: prior.prNumber,
@@ -4048,7 +4111,7 @@ function parsePriorCodexState(row) {
4048
4111
  return null;
4049
4112
  try {
4050
4113
  const parsed = JSON.parse(row.parserState);
4051
- if (parsed?.v !== 2 || typeof parsed.offset !== 'number' || parsed.toolCalls?.v !== 1)
4114
+ if (parsed?.v !== 3 || typeof parsed.offset !== 'number' || parsed.toolCalls?.v !== 1)
4052
4115
  return null;
4053
4116
  return parsed;
4054
4117
  }
@@ -4149,6 +4212,29 @@ function extractClaudeUserText(parsed) {
4149
4212
  function isLocalCommandMessage(text) {
4150
4213
  return /<local-command-caveat>|<bash-(input|stdout|stderr)>/i.test(text);
4151
4214
  }
4215
+ /**
4216
+ * Extract the assistant's own answer text from a Claude JSONL assistant event —
4217
+ * the reply text blocks, skipping tool_use/tool_result blocks and an
4218
+ * interrupted turn, mirroring {@link extractClaudeUserText}'s filters. Unlike
4219
+ * the user side (which keeps only the first text block), a single assistant
4220
+ * turn can legitimately carry multiple text blocks around tool calls, so every
4221
+ * one is joined.
4222
+ */
4223
+ function extractClaudeAssistantText(parsed) {
4224
+ const content = parsed.message?.content;
4225
+ if (typeof content === 'string') {
4226
+ const text = content.trim();
4227
+ return text && !text.startsWith('[Request interrupted') ? text : undefined;
4228
+ }
4229
+ if (!Array.isArray(content))
4230
+ return undefined;
4231
+ const text = content
4232
+ .filter((block) => block.type === 'text')
4233
+ .map((block) => String(block.text || '').trim())
4234
+ .filter((value) => value && !value.startsWith('[Request interrupted'))
4235
+ .join('\n');
4236
+ return text || undefined;
4237
+ }
4152
4238
  /** Sum all token usage fields from a Claude assistant message's usage object. */
4153
4239
  function getClaudeUsageTotal(usage) {
4154
4240
  if (!usage || typeof usage !== 'object')
@@ -4313,7 +4399,7 @@ async function scanCursorIncremental(onProgress) {
4313
4399
  const result = readCursorMeta(filePath, currentVersion);
4314
4400
  if (result && !seen.has(result.meta.id)) {
4315
4401
  seen.add(result.meta.id);
4316
- entries.push({ meta: result.meta, content: result.content, scan, events: result.events });
4402
+ entries.push({ meta: result.meta, content: result.content, assistantContent: result.assistantContent, scan, events: result.events });
4317
4403
  }
4318
4404
  else {
4319
4405
  touched.push({ filePath, scan });
@@ -4390,9 +4476,12 @@ export function readCursorMeta(filePath, currentVersion) {
4390
4476
  const userTexts = events
4391
4477
  .filter((event) => event.type === 'message' && event.role === 'user' && event.content)
4392
4478
  .map((event) => event.content);
4479
+ const assistantTexts = events
4480
+ .filter((event) => event.type === 'message' && event.role === 'assistant' && event.content)
4481
+ .map((event) => event.content);
4393
4482
  const firstUserText = userTexts[0];
4394
- const title = typeof chatMeta?.title === 'string' && chatMeta.title.trim()
4395
- ? chatMeta.title.trim()
4483
+ const title = typeof chatMeta?.title === 'string'
4484
+ ? cleanGeneratedSessionLabel(chatMeta.title)
4396
4485
  : undefined;
4397
4486
  const meta = {
4398
4487
  id: sessionId,
@@ -4409,7 +4498,7 @@ export function readCursorMeta(filePath, currentVersion) {
4409
4498
  messageCount: events.filter((event) => event.type === 'message').length,
4410
4499
  todos: extractTodoProgressFromEvents(events),
4411
4500
  };
4412
- return { meta, content: userTexts.join('\n'), events };
4501
+ return { meta, content: userTexts.join('\n'), assistantContent: assistantTexts.join('\n'), events };
4413
4502
  }
4414
4503
  // ---------------------------------------------------------------------------
4415
4504
  // Kimi