cc-viewer 1.6.348 → 1.7.1

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 (222) hide show
  1. package/README.md +13 -1
  2. package/cli.js +116 -37
  3. package/dist/assets/App-gUSmU6Ny.js +2 -0
  4. package/dist/assets/{MdxEditorPanel-ualoRFOL.js → MdxEditorPanel-DAR39A3S.js} +1 -1
  5. package/dist/assets/Mobile-qfRsRth0.js +1 -0
  6. package/dist/assets/{_baseUniq-CG81mgN_.js → _baseUniq-D9_E5ZDp.js} +1 -1
  7. package/dist/assets/{arc-C161J4f2.js → arc-D4iFrlhb.js} +1 -1
  8. package/dist/assets/{architectureDiagram-Q4EWVU46-BmbDMdn9.js → architectureDiagram-Q4EWVU46-f1neo7hg.js} +1 -1
  9. package/dist/assets/{blockDiagram-DXYQGD6D-DXpMvdxd.js → blockDiagram-DXYQGD6D-DWi9e9tG.js} +1 -1
  10. package/dist/assets/{c4Diagram-AHTNJAMY-8Mypg0IU.js → c4Diagram-AHTNJAMY-DAdj49A2.js} +1 -1
  11. package/dist/assets/{channel-B2khdmvc.js → channel-B3VthT0m.js} +1 -1
  12. package/dist/assets/{chunk-4BX2VUAB-I61r1g6z.js → chunk-4BX2VUAB-BrPcAHV3.js} +1 -1
  13. package/dist/assets/{chunk-4TB4RGXK-VFPlRE5E.js → chunk-4TB4RGXK-Bvopc1V9.js} +1 -1
  14. package/dist/assets/{chunk-55IACEB6-DvCamg_6.js → chunk-55IACEB6-Juw-7Z3i.js} +1 -1
  15. package/dist/assets/{chunk-EDXVE4YY-Dj4Q8RwR.js → chunk-EDXVE4YY-DVYP9tpB.js} +1 -1
  16. package/dist/assets/{chunk-FMBD7UC4-_bgQuuJh.js → chunk-FMBD7UC4-VQ7VOXl3.js} +1 -1
  17. package/dist/assets/{chunk-OYMX7WX6-3to38LyN.js → chunk-OYMX7WX6-F8Rj4ZUZ.js} +1 -1
  18. package/dist/assets/{chunk-QZHKN3VN-Zl26wKKz.js → chunk-QZHKN3VN-Dn7K3yi9.js} +1 -1
  19. package/dist/assets/{chunk-YZCP3GAM-RLYPrlh0.js → chunk-YZCP3GAM-UjO-KTsn.js} +1 -1
  20. package/dist/assets/classDiagram-6PBFFD2Q-DmSozxoO.js +1 -0
  21. package/dist/assets/classDiagram-v2-HSJHXN6E-DmSozxoO.js +1 -0
  22. package/dist/assets/clone-BQtvy9Ae.js +1 -0
  23. package/dist/assets/{cose-bilkent-S5V4N54A-CsYOjJ0V.js → cose-bilkent-S5V4N54A-BDhNj8rq.js} +1 -1
  24. package/dist/assets/{dagre-KV5264BT-C3YXXaFU.js → dagre-KV5264BT-BzdcDuyt.js} +1 -1
  25. package/dist/assets/{diagram-5BDNPKRD-DUCOg1ld.js → diagram-5BDNPKRD-DP_SX65N.js} +1 -1
  26. package/dist/assets/{diagram-G4DWMVQ6-BtlLl_zX.js → diagram-G4DWMVQ6-B-pBFw8Q.js} +1 -1
  27. package/dist/assets/{diagram-MMDJMWI5-1Qp9xfOk.js → diagram-MMDJMWI5-6XSfN1eN.js} +1 -1
  28. package/dist/assets/{diagram-TYMM5635-Cj7VMp3L.js → diagram-TYMM5635-DJvGxvpE.js} +1 -1
  29. package/dist/assets/{erDiagram-SMLLAGMA-CVZVn541.js → erDiagram-SMLLAGMA-CpqdpGYj.js} +1 -1
  30. package/dist/assets/{flowDiagram-DWJPFMVM-CiU9Fkkv.js → flowDiagram-DWJPFMVM-C9m7wjwI.js} +1 -1
  31. package/dist/assets/{ganttDiagram-T4ZO3ILL-aRaG97ID.js → ganttDiagram-T4ZO3ILL-CU0XOfY3.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-UUTBAWPF-Cr2LJlvX.js → gitGraphDiagram-UUTBAWPF-ipyGULnP.js} +1 -1
  33. package/dist/assets/{graph-B59lDyMn.js → graph-BR2KmES9.js} +1 -1
  34. package/dist/assets/{index-C1ptnCpq.js → index--wgRW_OB.js} +1 -1
  35. package/dist/assets/index-B51Ci0GL.js +2 -0
  36. package/dist/assets/{index-CRAWA8PC.js → index-C-be29ey.js} +1 -1
  37. package/dist/assets/{index-CSM1Kw8l.js → index-C16fiVhv.js} +1 -1
  38. package/dist/assets/{index-Ddk-kg22.js → index-D5YruOKD.js} +1 -1
  39. package/dist/assets/{index-BnhNIjbj.js → index-D5tSeMU7.js} +1 -1
  40. package/dist/assets/{index-vFEQq-NB.js → index-DuZQ6FqR.js} +1 -1
  41. package/dist/assets/{index-CO2hZ1s4.js → index-OWzSX2T8.js} +1 -1
  42. package/dist/assets/{infoDiagram-42DDH7IO-CnO22bw8.js → infoDiagram-42DDH7IO-BQWQjfFX.js} +1 -1
  43. package/dist/assets/{ishikawaDiagram-UXIWVN3A-ByNzpScH.js → ishikawaDiagram-UXIWVN3A-Df-KH_Hz.js} +1 -1
  44. package/dist/assets/{journeyDiagram-VCZTEJTY-yaVaCWPq.js → journeyDiagram-VCZTEJTY-CMbB_4G9.js} +1 -1
  45. package/dist/assets/{jszip.min-BdVFP3OW.js → jszip.min-B6k2t9Ch.js} +1 -1
  46. package/dist/assets/{kanban-definition-6JOO6SKY-RIPR_RQ3.js → kanban-definition-6JOO6SKY-CImdlKN3.js} +1 -1
  47. package/dist/assets/{layout-0rzel0v_.js → layout-DQl4aw09.js} +1 -1
  48. package/dist/assets/{linear-sktN0tB6.js → linear-CLD3aPlK.js} +1 -1
  49. package/dist/assets/mermaid.core-CN35dvKJ.js +7 -0
  50. package/dist/assets/{min-rspGOkz_.js → min-_D86henJ.js} +1 -1
  51. package/dist/assets/{mindmap-definition-QFDTVHPH-BsVzZzTz.js → mindmap-definition-QFDTVHPH-2JVcu1nk.js} +1 -1
  52. package/dist/assets/{pieDiagram-DEJITSTG-CsoWsw4f.js → pieDiagram-DEJITSTG-Dik-m5u7.js} +1 -1
  53. package/dist/assets/{quadrantDiagram-34T5L4WZ-CTMKuCAI.js → quadrantDiagram-34T5L4WZ-Bm5l40Jq.js} +1 -1
  54. package/dist/assets/{requirementDiagram-MS252O5E-C67256te.js → requirementDiagram-MS252O5E-Ck9w5rTj.js} +1 -1
  55. package/dist/assets/{sankeyDiagram-XADWPNL6-X8Y7KnW0.js → sankeyDiagram-XADWPNL6-BbGVCFEf.js} +1 -1
  56. package/dist/assets/{seqResourceLoaders-B5Zy_CC_.css → seqResourceLoaders-DFrHmgnK.css} +1 -1
  57. package/dist/assets/seqResourceLoaders-DZJxMMaR.js +2 -0
  58. package/dist/assets/{sequenceDiagram-FGHM5R23-Cils0TAp.js → sequenceDiagram-FGHM5R23-vjvEdksk.js} +1 -1
  59. package/dist/assets/{stateDiagram-FHFEXIEX-DFnohvpG.js → stateDiagram-FHFEXIEX-DTqMoiB2.js} +1 -1
  60. package/dist/assets/{stateDiagram-v2-QKLJ7IA2-BTX_4o81.js → stateDiagram-v2-QKLJ7IA2-CNbAwnhu.js} +1 -1
  61. package/dist/assets/{timeline-definition-GMOUNBTQ-DrIYFrzZ.js → timeline-definition-GMOUNBTQ-Cw8O-52d.js} +1 -1
  62. package/dist/assets/{vendor-antd-D5HmSDSg.js → vendor-antd-DI7JL-mE.js} +1 -1
  63. package/dist/assets/{vendor-codemirror-C5yuj7ze.js → vendor-codemirror-B9c49dtM.js} +1 -1
  64. package/dist/assets/{vendor-mdxeditor-BcEooTvb.js → vendor-mdxeditor-B6cpBtIE.js} +2 -2
  65. package/dist/assets/{vendor-qrcode-d9LCn0-9.js → vendor-qrcode-Dn90-s-u.js} +1 -1
  66. package/dist/assets/{vendor-virtuoso-K9m4MN6a.js → vendor-virtuoso-CpdSKD_6.js} +1 -1
  67. package/dist/assets/{vennDiagram-DHZGUBPP-lRqBYTr9.js → vennDiagram-DHZGUBPP-DCuqymb7.js} +1 -1
  68. package/dist/assets/{wardley-RL74JXVD-e8YfxpOe.js → wardley-RL74JXVD-BJ20wJUU.js} +1 -1
  69. package/dist/assets/{wardleyDiagram-NUSXRM2D-Bxrg774h.js → wardleyDiagram-NUSXRM2D-Vz6US3qt.js} +1 -1
  70. package/dist/assets/{xychartDiagram-5P7HB3ND-DWhZqhq8.js → xychartDiagram-5P7HB3ND-CzkLKrmi.js} +1 -1
  71. package/dist/index.html +4 -4
  72. package/findcc.js +4 -3
  73. package/package.json +3 -1
  74. package/server/i18n.js +21 -141
  75. package/server/interceptor.js +211 -403
  76. package/server/lib/async-write-queue.js +12 -1
  77. package/server/lib/delta-reconstructor.js +3 -3
  78. package/server/lib/error-report.js +45 -0
  79. package/server/lib/im-log-watcher.js +68 -8
  80. package/server/lib/interceptor-core.js +19 -174
  81. package/server/lib/log-management.js +214 -247
  82. package/server/lib/log-stream.js +59 -108
  83. package/server/lib/log-watcher.js +62 -274
  84. package/server/lib/log-zip.js +148 -0
  85. package/server/lib/session-boundary.js +266 -0
  86. package/server/lib/session-pin-store.js +10 -19
  87. package/server/lib/stats-worker.js +138 -267
  88. package/server/lib/user-prompt-extract.js +250 -0
  89. package/server/lib/v2/adapter.js +1124 -0
  90. package/server/lib/v2/blob-store.js +44 -0
  91. package/server/lib/v2/conversation-store.js +217 -0
  92. package/server/lib/v2/convert-manager.js +79 -0
  93. package/server/lib/v2/convert-worker.js +42 -0
  94. package/server/lib/v2/convert.js +409 -0
  95. package/server/lib/v2/identity.js +204 -0
  96. package/server/lib/v2/journal.js +62 -0
  97. package/server/lib/v2/layout.js +167 -0
  98. package/server/lib/v2/live-feed.js +558 -0
  99. package/server/lib/v2/meta-rows.js +360 -0
  100. package/server/lib/v2/migrate-prompt.js +54 -0
  101. package/server/lib/v2/replay.js +191 -0
  102. package/server/lib/v2/session-select.js +222 -0
  103. package/server/lib/v2/singleflight.js +104 -0
  104. package/server/lib/v2/v2-writer.js +528 -0
  105. package/server/lib/v2/verify.js +233 -0
  106. package/server/lib/wire-compress.js +167 -0
  107. package/server/routes/events.js +120 -150
  108. package/server/routes/files-fs.js +4 -2
  109. package/server/routes/im.js +11 -2
  110. package/server/routes/logs.js +209 -164
  111. package/server/routes/preferences.js +0 -3
  112. package/server/routes/project-meta.js +2 -3
  113. package/server/routes/session-pin.js +7 -8
  114. package/server/routes/v2.js +60 -0
  115. package/server/routes/workspaces.js +50 -14
  116. package/server/server.js +70 -66
  117. package/server/workspace-registry.js +24 -4
  118. package/src/utils/apiUrl.js +26 -0
  119. package/src/utils/askFallback.js +21 -0
  120. package/src/utils/askOptionDesc.js +55 -0
  121. package/src/utils/askPortalMatcher.js +54 -0
  122. package/src/utils/autoApproveOptions.js +27 -0
  123. package/src/utils/avatarAnimationPostPass.js +49 -0
  124. package/src/utils/builtinPresets.js +18 -0
  125. package/src/utils/clearCheckpoint.js +7 -0
  126. package/src/utils/commandValidator.js +45 -0
  127. package/src/utils/contentFilter.js +620 -0
  128. package/src/utils/contextRaw.js +18 -0
  129. package/src/utils/contextSidebarNavigation.js +16 -0
  130. package/src/utils/contextTurns.js +117 -0
  131. package/src/utils/displayScaleHelper.js +47 -0
  132. package/src/utils/effectiveModel.js +13 -0
  133. package/src/utils/entry-slim.js +563 -0
  134. package/src/utils/entryCache.js +144 -0
  135. package/src/utils/errorReport.js +6 -0
  136. package/src/utils/fileExpandedPathsStorage.js +70 -0
  137. package/src/utils/fileIcons.jsx +36 -0
  138. package/src/utils/fileOpen.js +37 -0
  139. package/src/utils/formatters.js +48 -0
  140. package/src/utils/gitApi.js +50 -0
  141. package/src/utils/gitTreeBuilder.js +19 -0
  142. package/src/utils/helpers.js +846 -0
  143. package/src/utils/identityHeal.js +99 -0
  144. package/src/utils/imConnState.js +59 -0
  145. package/src/utils/imOrigin.js +24 -0
  146. package/src/utils/imTr.js +7 -0
  147. package/src/utils/imageCompress.js +83 -0
  148. package/src/utils/imageDownscale.js +28 -0
  149. package/src/utils/imageResize.js +112 -0
  150. package/src/utils/ingestPipeline.js +47 -0
  151. package/src/utils/lazyWithReload.js +53 -0
  152. package/src/utils/markdown.js +30 -0
  153. package/src/utils/markdownIncremental.js +39 -0
  154. package/src/utils/markdownProfiler.js +113 -0
  155. package/src/utils/mdExtensionDetect.js +64 -0
  156. package/src/utils/memoryLinkParser.js +44 -0
  157. package/src/utils/modalMask.js +15 -0
  158. package/src/utils/monotime.js +6 -0
  159. package/src/utils/pinnedMenu.js +55 -0
  160. package/src/utils/presetShortcuts.js +14 -0
  161. package/src/utils/projectAlias.js +135 -0
  162. package/src/utils/promptClassifier.js +112 -0
  163. package/src/utils/promptDetect.js +196 -0
  164. package/src/utils/promptNav.js +60 -0
  165. package/src/utils/ptyChunkBuilder.js +251 -0
  166. package/src/utils/quickMenuHoverIntent.js +35 -0
  167. package/src/utils/rateLimitParser.js +119 -0
  168. package/src/utils/readResultPool.js +106 -0
  169. package/src/utils/refreshCachedItemProp.js +30 -0
  170. package/src/utils/requestType.js +208 -0
  171. package/src/utils/resizeCalc.js +21 -0
  172. package/src/utils/resolveLocalized.js +17 -0
  173. package/src/utils/searchApi.js +47 -0
  174. package/src/utils/searchReplace.js +51 -0
  175. package/src/utils/seqResourceLoaders.js +79 -0
  176. package/src/utils/sessionManager.js +471 -0
  177. package/src/utils/sessionMerge.js +180 -0
  178. package/src/utils/skillModalController.js +111 -0
  179. package/src/utils/skillsParser.js +116 -0
  180. package/src/utils/slashCommandLabels.js +80 -0
  181. package/src/utils/splitDragCalc.js +73 -0
  182. package/src/utils/stickyBottomController.js +643 -0
  183. package/src/utils/svgSanitize.js +48 -0
  184. package/src/utils/systemTags.js +47 -0
  185. package/src/utils/tClaude.js +20 -0
  186. package/src/utils/teamModalBuilder.js +351 -0
  187. package/src/utils/teamSessionParser.js +190 -0
  188. package/src/utils/teammateAvatars.js +194 -0
  189. package/src/utils/teammateDetector.js +121 -0
  190. package/src/utils/termDiag.js +114 -0
  191. package/src/utils/terminalClipboard.js +117 -0
  192. package/src/utils/terminalWriteQueue.js +398 -0
  193. package/src/utils/thinkingMerge.js +49 -0
  194. package/src/utils/toolCatalog.js +29 -0
  195. package/src/utils/toolResultBuilder.js +393 -0
  196. package/src/utils/toolResultClassifier.js +16 -0
  197. package/src/utils/toolResultCore.js +167 -0
  198. package/src/utils/toolsDiff.js +44 -0
  199. package/src/utils/toolsXmlFormatter.js +3 -0
  200. package/src/utils/ultraplanController.js +109 -0
  201. package/src/utils/ultraplanExperts.js +86 -0
  202. package/src/utils/ultraplanTemplates.js +154 -0
  203. package/src/utils/userImageRefs.js +49 -0
  204. package/src/utils/v3Assembler.js +112 -0
  205. package/src/utils/v3Rows.js +48 -0
  206. package/src/utils/voicePackPlayer.js +233 -0
  207. package/src/utils/webSearchGrouping.js +111 -0
  208. package/src/utils/workflowFormat.js +52 -0
  209. package/src/utils/workflowRuns.js +118 -0
  210. package/src/utils/workflowStore.js +94 -0
  211. package/dist/assets/App-VTo4NVdo.js +0 -2
  212. package/dist/assets/Mobile-CcmYvMm_.js +0 -1
  213. package/dist/assets/classDiagram-6PBFFD2Q-BXnyUgsj.js +0 -1
  214. package/dist/assets/classDiagram-v2-HSJHXN6E-BXnyUgsj.js +0 -1
  215. package/dist/assets/clone-md-Mc1-T.js +0 -1
  216. package/dist/assets/index-DCo650PG.js +0 -2
  217. package/dist/assets/mermaid.core-C2Parj1q.js +0 -7
  218. package/dist/assets/seqResourceLoaders-BVozq68i.js +0 -2
  219. package/server/lib/instance-registry.js +0 -57
  220. package/server/lib/jsonl-archive.js +0 -180
  221. package/server/lib/teammate-detect.js +0 -32
  222. package/server/lib/zip-safety.js +0 -68
@@ -0,0 +1,528 @@
1
+ // Wire Format v2 — write-path orchestrator (docs/refactor/WIRE_FORMAT_V2.md §13).
2
+ //
3
+ // The single entry point the interceptor's writeEntry seam will call in S3.
4
+ // Hard contract: a v2 failure must NEVER disturb the v1 path — every public
5
+ // method is fully caught and routed through reportSwallowed (CLAUDE.md rule);
6
+ // the caller does not need its own try/catch.
7
+ //
8
+ // Write-order protocol per request (spec §1.3):
9
+ // blob (sync, fsync barrier) → conversation event lines → journal req line.
10
+ // The HARD guarantee is the blob half: a journal line can never reference a
11
+ // missing blob (blobs are durable before the line is even enqueued). The conv
12
+ // half is best-effort batch grouping — AsyncWriteQueue._drain groups by path at
13
+ // first-enqueue position, so when several requests flush in one batch (e.g. the
14
+ // cold-start hold queue), a later request's journal line can land before its
15
+ // own conv line. The read side tolerates a missing conv tail by design
16
+ // (spec §14, pendingTail-style retry), so this is defense-in-depth, not a
17
+ // correctness dependency.
18
+
19
+ import { statfsSync, existsSync, readdirSync, readFileSync } from 'node:fs';
20
+ import { join, basename } from 'node:path';
21
+ import { AsyncWriteQueue } from '../async-write-queue.js';
22
+ import { reportSwallowed } from '../error-report.js';
23
+ import { ensureSessionDirSync, compactLocalTs14, sanitizePathComponent } from './layout.js';
24
+ import { resolveSessionDirName, latestMainSession } from './session-select.js';
25
+ import { BlobStore } from './blob-store.js';
26
+ import { Journal } from './journal.js';
27
+ import { ConversationStore } from './conversation-store.js';
28
+ import { parseUserId, classifyKind, ConvResolver } from './identity.js';
29
+ import { extractUserTexts, flattenPromptText, isSuggestionMode, readPromptsHead } from '../user-prompt-extract.js';
30
+
31
+ // Below this many free bytes on the log volume, v2 skips writing and reports
32
+ // once — logging must not be the thing that fills the disk (plan risk #9).
33
+ const MIN_FREE_BYTES = 1024 * 1024 * 1024; // 1GB
34
+
35
+ // Hard ceiling on distinct prompts recorded per session in prompts.jsonl —
36
+ // keeps both the in-memory dedup set and the side file bounded for extreme
37
+ // thousand-turn sessions (the read side additionally caps at a byte budget).
38
+ const PROMPTS_MAX_PER_SESSION = 2000;
39
+
40
+ export class V2Writer {
41
+ /**
42
+ * @param {object} opts
43
+ * @param {string} opts.logDir - LOG_DIR root
44
+ * @param {string|Function} opts.project - project directory name, or a getter —
45
+ * workspace mode rebinds the interceptor's _projectName at runtime; a falsy
46
+ * resolved project makes ingest a no-op (mirrors v1's empty-LOG_FILE no-op)
47
+ * @param {object|null} [opts.leader] - teammate processes: {agentName, teamName, parentSessionId}
48
+ * @param {boolean} [opts.enabled] - always on since 1.7.0; `enabled: false`
49
+ * is honored only as a test seam (fixtures asserting "nothing written")
50
+ * @param {AsyncWriteQueue} [opts.queue] - injected queue (tests); defaults to a
51
+ * dedicated instance so v2 volume never head-of-line-blocks v1's queue.
52
+ * @param {number} [opts.minFreeBytes] - disk guard threshold (tests)
53
+ * @param {Function} [opts.statfs] - injected statfsSync (tests)
54
+ */
55
+ constructor(opts = {}) {
56
+ this._logDir = opts.logDir || '';
57
+ this._projectFn = typeof opts.project === 'function'
58
+ ? opts.project
59
+ : () => (opts.project || '');
60
+ this._leader = opts.leader || null;
61
+ // 1.7.0: always on in production; opts.enabled === false is honored only
62
+ // as a test seam (fixtures that must assert "nothing written").
63
+ this._enabled = opts.enabled === false ? false : true;
64
+ this._queue = opts.queue || new AsyncWriteQueue(''); // paths are always explicit
65
+ this._minFreeBytes = typeof opts.minFreeBytes === 'number' ? opts.minFreeBytes : MIN_FREE_BYTES;
66
+ this._statfs = opts.statfs || statfsSync;
67
+ // Offline-converter seams (S8): write under a sibling staging dir name,
68
+ // stamp extra meta fields (e.g. {origin:'convert'}), and use exact
69
+ // conversation judgement fingerprints (byte-grade golden gate). Live
70
+ // writers pass none of these.
71
+ this._sessionsDirName = opts.sessionsDirName || 'sessions';
72
+ this._metaExtra = opts.metaExtra || null;
73
+ this._exactConvFps = !!opts.exactConvFps;
74
+ // S6b live feed: called with the session dir after every ingest so the
75
+ // in-process live cursor can read the fresh appends with zero fs-watch
76
+ // latency. Purely a nudge — the feed's data source stays the files.
77
+ this._onActivity = typeof opts.onActivity === 'function' ? opts.onActivity : null;
78
+ this._sessions = new Map(); // sessionId → {paths, blobs, journal, convs, resolver}
79
+ this._currentSid = null; // last successfully resolved session (fallback routing §8.3)
80
+ this._pendingNoSid = []; // requests seen before any sid (cold-start heartbeats).
81
+ // Contract with the caller: originalMessages held here must be a reference
82
+ // the caller never MUTATES (the interceptor's original array is only ever
83
+ // REASSIGNED away from body.messages, never mutated in place — safe).
84
+ this._lateHandles = new Map(); // rid → handle for held requests whose completion arrives after the flush
85
+ this._diskGuardTripped = false;
86
+ this._continuedSeen = false; // P2: wire-level `claude -c` continuation marker
87
+ // `-c` folder adoption (Claude 2.1.210 hands a fresh wire session_id on every
88
+ // continue, so a new folder would otherwise be minted each time). Set from
89
+ // the launch flags via setContinuationMode(); adoption fires at most once per
90
+ // process (the first main request that proves a continuation on the wire).
91
+ this._continuationLaunch = false;
92
+ this._forkSession = false;
93
+ this._resumeSession = false;
94
+ this._adopted = false;
95
+ }
96
+
97
+ /** P2: true once any session's FIRST main wire already carried assistant
98
+ * turns — the wire-level signature of a continued (-c/-r) conversation. */
99
+ sawContinuedSession() { return this._continuedSeen; }
100
+
101
+ /** Launch flags for `-c` folder adoption (set by the interceptor before any
102
+ * request flows). `continued` = a pre-request continuation signal
103
+ * (CCV_CLAUDE_CONTINUE / workspace launcher); `fork` = `--fork-session`
104
+ * (user wants a NEW session — never adopt); `resume` = explicit `-r`/
105
+ * `--resume` (user-chosen target session — adoption would misroute it to
106
+ * the LATEST main session, so it keeps its own folder). */
107
+ setContinuationMode({ continued, fork, resume } = {}) {
108
+ this._continuationLaunch = !!continued;
109
+ this._forkSession = !!fork;
110
+ this._resumeSession = !!resume;
111
+ }
112
+
113
+ // 1.7.0: v2 is the only format — the writer is always on. The getter is kept
114
+ // because read-side helpers key off it; the sole write inhibitor left is the
115
+ // in-band disk guard.
116
+ get enabled() { return this._enabled; }
117
+
118
+ /** Late wiring seam: the live feed is constructed by server.js AFTER this
119
+ * writer exists (module init order), so the nudge callback arrives here. */
120
+ setOnActivity(fn) { this._onActivity = typeof fn === 'function' ? fn : null; }
121
+
122
+ /**
123
+ * If this request should adopt the previous main session's folder — a `-c`
124
+ * continuation launch that Claude handed a FRESH wire session_id — return
125
+ * `{ dirName, identityUUID }` of the folder to reuse, else null. Decided at
126
+ * most once per process, before the folder is created. All conditions must
127
+ * hold: continuation launch via `-c`/`--continue` ONLY (not `--fork-session`,
128
+ * not an explicit `-r`/`--resume` — both keep their own folder); main
129
+ * (non-leader) writer; not already adopted; the wire sid has no folder of its
130
+ * own (never hijack a same-UUID restart); the wire actually replays assistant
131
+ * history (excludes the pty-manager `-c`-stripped retry and stray
132
+ * history-less requests); and a previous main session exists.
133
+ * @returns {{dirName:string, identityUUID:string}|null}
134
+ */
135
+ _resolveAdoption(sid, project, msgs, entry) {
136
+ if (this._adopted || this._leader) return null;
137
+ if (!this._continuationLaunch || this._forkSession || this._resumeSession) return null;
138
+ if (this._sessions.has(sid)) return null;
139
+ const m = Array.isArray(msgs) ? msgs
140
+ : (entry && entry.body && Array.isArray(entry.body.messages) ? entry.body.messages : null);
141
+ if (!m || m.length <= 1 || !m.some((x) => x && x.role === 'assistant')) return null;
142
+ const projectDir = join(this._logDir, sanitizePathComponent(project));
143
+ if (resolveSessionDirName(projectDir, sid, this._sessionsDirName)) return null; // same-UUID restart
144
+ const prev = latestMainSession(projectDir);
145
+ if (!prev || !prev.sessionId) return null;
146
+ return { dirName: basename(prev.dir), identityUUID: prev.sessionId };
147
+ }
148
+
149
+ _session(sessionId, userIdRaw, encoding, project, startTsIso, adoptTarget = null) {
150
+ let s = this._sessions.get(sessionId);
151
+ if (s) return s; // hot path: map hit is O(1), the scan below only runs on miss
152
+
153
+ // Task C: the dir name carries a creation-time prefix `<ts>_<uuid>`. On a
154
+ // map MISS (cold start / restart / `-c` re-attach) the same session must
155
+ // REUSE its existing dir, not mint a `<now>_<uuid>` sibling (which would
156
+ // split the conversation + reset seq + flip _seqEpoch). Scan for an existing
157
+ // dir by UUID; only when none exists create a new ts-prefixed one. The
158
+ // identity written into meta.sessionId / journal sentinel stays the UUID.
159
+ const startTs = startTsIso || new Date().toISOString();
160
+ const meta = {
161
+ pid: process.pid,
162
+ startTs,
163
+ ...(userIdRaw && { userIdRaw }),
164
+ ...(encoding && { userIdEncoding: encoding }),
165
+ ...(this._leader && { leader: this._leader }),
166
+ ...(this._metaExtra || {}),
167
+ };
168
+ const projectDir = join(this._logDir, sanitizePathComponent(project));
169
+ // Identity (meta.sessionId + journal sentinel) — normally the wire UUID; on
170
+ // `-c` adoption it stays the ADOPTED folder's UUID so the frontend session
171
+ // identity (`_seqEpoch = v2:<meta.sessionId>`) and all reuse machinery see
172
+ // one continuous session. meta.json/sentinel are first-write-wins, so writing
173
+ // into the existing adopted folder never overwrites its identity.
174
+ let identityId = sessionId;
175
+ let dirName;
176
+ if (adoptTarget) {
177
+ dirName = adoptTarget.dirName;
178
+ identityId = adoptTarget.identityUUID;
179
+ } else {
180
+ dirName = resolveSessionDirName(projectDir, sessionId, this._sessionsDirName);
181
+ if (!dirName) {
182
+ const ts = compactLocalTs14(startTs) || compactLocalTs14(new Date().toISOString());
183
+ dirName = `${ts}_${sessionId}`;
184
+ }
185
+ }
186
+ const paths = ensureSessionDirSync(this._logDir, project, identityId, meta, this._sessionsDirName, dirName);
187
+ s = {
188
+ paths,
189
+ blobs: new BlobStore(paths),
190
+ journal: new Journal(paths, this._queue),
191
+ convs: new ConversationStore(paths, this._queue, { exactFps: this._exactConvFps }),
192
+ resolver: new ConvResolver(),
193
+ };
194
+ this._sessions.set(sessionId, s);
195
+ return s;
196
+ }
197
+
198
+ _diskOk() {
199
+ if (this._diskGuardTripped) return false;
200
+ try {
201
+ const st = this._statfs(this._logDir);
202
+ const free = Number(st.bavail) * Number(st.bsize);
203
+ if (free < this._minFreeBytes) {
204
+ this._diskGuardTripped = true;
205
+ reportSwallowed('v2-write.disk-guard', new Error(`free ${free}B < ${this._minFreeBytes}B — v2 writes disabled for this process`));
206
+ return false;
207
+ }
208
+ return true;
209
+ } catch (err) {
210
+ // statfs failing is not a reason to stop logging — warn once, keep writing.
211
+ reportSwallowed('v2-write.statfs', err);
212
+ return true;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Request-initiation ingest. MUST be called in the interceptor's synchronous
218
+ * initiation segment (same place v1 assigns _seq), with the ORIGINAL messages
219
+ * array captured BEFORE the v1 delta path mutates body.messages in place.
220
+ *
221
+ * @param {object} entry - the fully materialized v1 requestEntry
222
+ * @param {Array|null} originalMessages - pre-mutation body.messages reference
223
+ * @returns {{sid:string, seq:number}|null} handle for ingestCompletion, or
224
+ * null when disabled/failed (caller passes it back verbatim either way).
225
+ */
226
+ ingestRequest(entry, originalMessages) {
227
+ if (!this._enabled || !entry) return null;
228
+ try {
229
+ const project = this._projectFn();
230
+ if (!project) return null; // workspace not selected yet — mirrors v1's empty-LOG_FILE no-op
231
+ if (!this._diskOk()) return null;
232
+
233
+ const parsed = parseUserId(entry.body && entry.body.metadata && entry.body.metadata.user_id);
234
+ let sid;
235
+ if (parsed) {
236
+ sid = parsed.sessionId;
237
+ this._currentSid = sid;
238
+ } else if (this._currentSid) {
239
+ sid = this._currentSid; // §8.3 fallback: route metadata-less requests to the active session
240
+ } else {
241
+ // Cold start before any sid-bearing request: hold in memory; flushed by
242
+ // the first resolved request. Bounded to avoid growing forever on a
243
+ // process that only ever sees metadata-less traffic.
244
+ if (this._pendingNoSid.length < 64) {
245
+ // rid captured NOW: the v1 seam deletes entry.requestId at completion,
246
+ // which can happen before the hold queue flushes (review P3-4 — the
247
+ // held request would otherwise never get its done line).
248
+ this._pendingNoSid.push({ entry, originalMessages, rid: entry.requestId || null });
249
+ return null;
250
+ }
251
+ sid = `noid-${process.pid}-${Date.now()}`;
252
+ this._currentSid = sid;
253
+ }
254
+
255
+ // `-c` folder adoption: when this launch is a real continuation but Claude
256
+ // handed a fresh wire session_id, route the writes into the previous main
257
+ // session's folder instead of minting a new (blank) one. Resolved once,
258
+ // before the folder is created; null on every non-adoption path.
259
+ let adoptTarget = null;
260
+ if (parsed) {
261
+ adoptTarget = this._resolveAdoption(sid, project, originalMessages, entry);
262
+ if (adoptTarget) this._adopted = true;
263
+ }
264
+
265
+ // entry.timestamp = this session's first-request time → meta.startTs +
266
+ // the dir-name ts prefix (task C). First-write-wins: meta is only written
267
+ // when the dir is first created, so this is the session's creation time
268
+ // (live ≈ now; convert = the historical first-entry ts).
269
+ const s = this._session(sid, parsed && entry.body.metadata.user_id, parsed && parsed.encoding, project, entry.timestamp, adoptTarget);
270
+
271
+ // Flush requests that arrived before the first sid (they belong here).
272
+ // Their handles are parked in _lateHandles so a completion that arrives
273
+ // after the flush still gets its done line (folded by rid).
274
+ if (this._pendingNoSid.length > 0) {
275
+ const pending = this._pendingNoSid.splice(0);
276
+ for (const p of pending) {
277
+ const pseq = this._ingestInto(s, sid, p.entry, p.originalMessages);
278
+ const prid = p.rid; // captured at hold time — the seam may have deleted entry.requestId by now
279
+ if (pseq != null && prid) {
280
+ this._lateHandles.set(prid, { s, sid, seq: pseq, rid: prid });
281
+ if (this._lateHandles.size > 64) {
282
+ this._lateHandles.delete(this._lateHandles.keys().next().value);
283
+ }
284
+ }
285
+ }
286
+ }
287
+
288
+ const seq = this._ingestInto(s, sid, entry, originalMessages);
289
+ if (seq != null && this._onActivity) {
290
+ try { this._onActivity(s.paths.dir); } catch { /* nudge only — never disturb the write */ }
291
+ }
292
+ // The handle carries the RESOLVED session object, not just the sid:
293
+ // resetSessions() (workspace switch) clears the _sessions map, and a
294
+ // completion arriving after the switch must still land its done/response
295
+ // lines in the session the request belonged to (review P2: a map lookup
296
+ // at completion time silently dropped them).
297
+ return seq == null ? null : { s, sid, seq, rid: entry.requestId || `${seq}` };
298
+ } catch (err) {
299
+ reportSwallowed('v2-write.ingestRequest', err);
300
+ return null;
301
+ }
302
+ }
303
+
304
+ _ingestInto(s, sid, entry, originalMessages) {
305
+ const kind = classifyKind(entry);
306
+ const seq = s.journal.nextSeq();
307
+ const rid = entry.requestId || `${seq}`;
308
+ const msgs = Array.isArray(originalMessages) ? originalMessages
309
+ : (entry.body && Array.isArray(entry.body.messages) ? entry.body.messages : null);
310
+
311
+ // P2 wire-level continuation detection: the very first main wire of a
312
+ // session that already carries assistant turns can only be `claude -c`
313
+ // (or -r) resuming an older conversation — a fresh one starts with a
314
+ // single user message. Used by the migrate prompt's `continued` flag.
315
+ if (!this._continuedSeen && kind === 'main' && !s._sawMain && msgs && msgs.length > 1
316
+ && msgs.some((m) => m && m.role === 'assistant')) {
317
+ this._continuedSeen = true;
318
+ }
319
+ if (kind === 'main') s._sawMain = true;
320
+
321
+ // 1. Blobs (sync + fsync — the durability barrier).
322
+ const toolsRef = s.blobs.put(entry.body && entry.body.tools);
323
+ const sysRef = s.blobs.put(entry.body && entry.body.system);
324
+
325
+ // 2. Conversation event (heartbeats carry no conversation).
326
+ let convKey = null;
327
+ let convResult = null;
328
+ if (kind !== 'heartbeat' && msgs) {
329
+ if (kind === 'main' || kind === 'teammate') convKey = 'main';
330
+ else if (kind === 'sub') convKey = s.resolver.resolveSub(msgs).convKey;
331
+ else convKey = 'misc'; // countTokens & friends: keep wire fidelity under misc
332
+ convResult = s.convs.ingest(convKey, msgs, { seq, rid });
333
+ }
334
+
335
+ // Residual body params — every top-level body field except the three that
336
+ // already have dedicated stores (messages → conv, system/tools → blobs).
337
+ // Inlined whole on the req line like headers, so the adapter can rebuild
338
+ // the full v1 body (max_tokens, temperature, thinking, metadata, …).
339
+ let params = null;
340
+ if (entry.body && typeof entry.body === 'object' && !Array.isArray(entry.body)) {
341
+ const rest = { ...entry.body };
342
+ delete rest.messages;
343
+ delete rest.system;
344
+ delete rest.tools;
345
+ if (Object.keys(rest).length > 0) params = rest;
346
+ }
347
+
348
+ // 3. Journal req line — LAST, so it never references missing content.
349
+ s.journal.writeReq({
350
+ seq,
351
+ rid,
352
+ ts: entry.timestamp,
353
+ kind,
354
+ ...(convKey && { conv: convKey }),
355
+ ...(convResult && { epoch: convResult.epoch }),
356
+ url: entry.url,
357
+ method: entry.method,
358
+ ...(entry.body && entry.body.model && { model: entry.body.model }),
359
+ ...(entry.isStream && { isStream: true }),
360
+ ...(entry.headers && { headers: entry.headers }),
361
+ ...(params && { params }),
362
+ ...((toolsRef || sysRef) && { blobs: { ...(toolsRef && { tools: toolsRef }), ...(sysRef && { sys: sysRef }) } }),
363
+ ...(convResult && { msgFrom: convResult.msgFrom, msgTo: convResult.msgTo }),
364
+ ...(convResult && convResult.evt && { evt: convResult.evt }),
365
+ ...(convResult && convResult.boundary && { boundary: convResult.boundary }),
366
+ ...(entry.proxyProfile && { proxy: { profile: entry.proxyProfile, ...(entry.proxyUrl && { url: entry.proxyUrl }) } }),
367
+ });
368
+
369
+ // 4. prompts.jsonl display cache — strictly AFTER the journal line (a
370
+ // prompts failure must never cost the request its journal record) and
371
+ // fully caught on its own. Main conversation only; the converter drives
372
+ // this same path, so migrated sessions get the cache for free.
373
+ if (convKey === 'main' && convResult && msgs) {
374
+ try { this._appendPrompts(s, seq, msgs, convResult); }
375
+ catch (err) { reportSwallowed('v2-write.prompts', err); }
376
+ }
377
+ return seq;
378
+ }
379
+
380
+ /**
381
+ * Append newly-seen user prompts of the main conversation to the session's
382
+ * `prompts.jsonl` ({seq, texts} lines). Idempotent across process restarts:
383
+ * the dedup set is seeded once from the existing file (bounded head read),
384
+ * so a resume/`-c` snapshot replaying the full history appends nothing new.
385
+ */
386
+ _appendPrompts(s, seq, msgs, convResult) {
387
+ // snapshot (msgFrom=0, full wire), append (new tail) — and replace-tail:
388
+ // a suggestion-mode probe replaced by the REAL next user prompt arrives as
389
+ // a same-length tail swap, so skipping ctl entirely would lose exactly
390
+ // those prompts.
391
+ const covered = convResult.evt === 'append' || convResult.evt === 'snapshot'
392
+ || (convResult.evt === 'ctl' && convResult.ctl === 'replace-tail');
393
+ if (!covered) return;
394
+ if (isSuggestionMode(msgs)) return; // next-input probes are not user prompts
395
+ if (!s._promptSeen) s._promptSeen = new Set(readPromptsHead(s.paths.promptsPath));
396
+ const slice = msgs.slice(convResult.msgFrom);
397
+ const texts = [];
398
+ for (const text of extractUserTexts(slice)) {
399
+ if (s._promptSeen.size >= PROMPTS_MAX_PER_SESSION) break;
400
+ const flat = flattenPromptText(text);
401
+ if (!flat || s._promptSeen.has(flat)) continue;
402
+ s._promptSeen.add(flat);
403
+ texts.push(flat);
404
+ }
405
+ if (texts.length > 0) {
406
+ this._queue.appendTo(s.paths.promptsPath, JSON.stringify({ seq, texts }) + '\n');
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Completion ingest: responses line + journal done line (+ Agent-spawn
412
+ * registration for future sub-conversation keying, spec §10).
413
+ * @param {{sid:string, seq:number}|null} handle - from ingestRequest
414
+ * @param {object} entry - completed v1 entry (response populated)
415
+ * @param {{doneTs?: string}} [opts] - offline converter: historical done
416
+ * timestamp (entry start + duration); live callers omit it (wall clock).
417
+ */
418
+ ingestCompletion(handle, entry, opts = {}) {
419
+ if (!this._enabled || !entry) return;
420
+ try {
421
+ // Held cold-start requests returned a null handle at ingestRequest time;
422
+ // recover it by rid (the seam calls us while entry.requestId is intact).
423
+ if (!handle && entry.requestId && this._lateHandles.has(entry.requestId)) {
424
+ handle = this._lateHandles.get(entry.requestId);
425
+ this._lateHandles.delete(entry.requestId);
426
+ }
427
+ if (!handle) return;
428
+ // Prefer the session object carried in the handle (survives
429
+ // resetSessions); the map lookup is only a legacy fallback.
430
+ const s = handle.s || this._sessions.get(handle.sid);
431
+ if (!s) return;
432
+
433
+ const resp = entry.response || null;
434
+ const respBody = resp && resp.body !== undefined ? resp.body : null;
435
+ if (respBody && typeof respBody === 'object') s.resolver.registerSpawns(respBody);
436
+
437
+ // rid comes from the handle, NOT entry.requestId — the v1 completion path
438
+ // deletes requestId from the entry before writing, and the seam must be
439
+ // free to call us before or after those deletes.
440
+ const rid = handle.rid || `${handle.seq}`;
441
+ this._queue.appendTo(
442
+ s.paths.responsesPath,
443
+ JSON.stringify({
444
+ seq: handle.seq,
445
+ rid,
446
+ body: respBody,
447
+ ...(resp && resp.headers && { headers: resp.headers }),
448
+ ...(resp && resp.statusText && { statusText: resp.statusText }),
449
+ }) + '\n'
450
+ );
451
+
452
+ const usage = respBody && respBody.usage ? respBody.usage : null;
453
+ s.journal.writeDone({
454
+ seq: handle.seq,
455
+ rid,
456
+ ts: opts.doneTs || new Date().toISOString(),
457
+ ...(typeof entry.duration === 'number' && { dur: entry.duration }),
458
+ status: resp && resp.error ? 'error' : (respBody == null ? 'capture-failed' : 'ok'),
459
+ ...(resp && typeof resp.status === 'number' && { http: resp.status }),
460
+ ...(usage && {
461
+ usage: {
462
+ ...(usage.input_tokens != null && { in: usage.input_tokens }),
463
+ ...(usage.output_tokens != null && { out: usage.output_tokens }),
464
+ ...(usage.cache_read_input_tokens != null && { cr: usage.cache_read_input_tokens }),
465
+ ...(usage.cache_creation_input_tokens != null && { cw: usage.cache_creation_input_tokens }),
466
+ },
467
+ }),
468
+ ...(respBody && respBody.stop_reason && { stop: respBody.stop_reason }),
469
+ });
470
+ if (this._onActivity && s.paths) {
471
+ try { this._onActivity(s.paths.dir); } catch { /* nudge only — never disturb the write */ }
472
+ }
473
+ } catch (err) {
474
+ reportSwallowed('v2-write.ingestCompletion', err);
475
+ }
476
+ }
477
+
478
+ /** Session directory of the writer's current (fallback-routing) session, or
479
+ * null before the first sid-bearing request. The read side uses this to
480
+ * resolve "the live session" for /events cold loads (S6b). */
481
+ currentSessionDir() {
482
+ if (!this._currentSid) return null;
483
+ const s = this._sessions.get(this._currentSid);
484
+ return s ? s.paths.dir : null;
485
+ }
486
+
487
+ /** Lifecycle hook (resume / workspace reset): drop in-memory conversation
488
+ * continuity so the next request snapshots fresh. Journal seq keeps counting
489
+ * (same process, same session dirs — seq must stay monotonic per session). */
490
+ resetConversations() {
491
+ try {
492
+ for (const s of this._sessions.values()) {
493
+ s.convs.reset();
494
+ s.resolver.reset();
495
+ }
496
+ this._currentSid = null;
497
+ } catch (err) {
498
+ reportSwallowed('v2-write.reset', err);
499
+ }
500
+ }
501
+
502
+ /** Lifecycle hook (workspace switch): drop ALL cached session bindings so the
503
+ * next request re-creates its session dir under the newly resolved project.
504
+ * A sid that persists across the switch gets a fresh dir (and fresh seq)
505
+ * under the new project — different directory, so no seq collision. */
506
+ resetSessions() {
507
+ try {
508
+ this._sessions.clear();
509
+ this._pendingNoSid.length = 0;
510
+ this._lateHandles.clear();
511
+ this._currentSid = null;
512
+ // A second workspace's `-c` must be able to adopt afresh — the previous
513
+ // workspace's adoption doesn't carry over.
514
+ this._adopted = false;
515
+ } catch (err) {
516
+ reportSwallowed('v2-write.reset-sessions', err);
517
+ }
518
+ }
519
+
520
+ /** Flush pending queue writes (tests / graceful shutdown). */
521
+ async flush() {
522
+ await this._queue.flush();
523
+ }
524
+
525
+ async close() {
526
+ await this._queue.close();
527
+ }
528
+ }