tianshu-harness 3.23.0

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 (255) hide show
  1. package/LICENSE +189 -0
  2. package/README.en.md +1066 -0
  3. package/README.ja.md +1214 -0
  4. package/README.ko.md +1216 -0
  5. package/README.md +309 -0
  6. package/completions/rivet.bash +118 -0
  7. package/completions/rivet.fish +110 -0
  8. package/completions/rivet.ps1 +230 -0
  9. package/completions/rivet.zsh +187 -0
  10. package/dist/account-OAEKORZE.js +47 -0
  11. package/dist/account-login-V6FOLHJR.js +65 -0
  12. package/dist/account-status-KHNPPAFL.js +23 -0
  13. package/dist/agent/worker-process/child.js +407 -0
  14. package/dist/browser-cli-FLNY5ZHI.js +19 -0
  15. package/dist/browser-readiness-LWVGSBD5.js +12 -0
  16. package/dist/build-backends-5KXKR2S4.js +13 -0
  17. package/dist/build-options-66Y4ZE7C.js +9 -0
  18. package/dist/bundled-skills/brainstorming/SKILL.md +139 -0
  19. package/dist/bundled-skills/visual-acceptance/SKILL.md +64 -0
  20. package/dist/chain-7MAMFXZ3.js +10 -0
  21. package/dist/checkpoint-MND4DUIX.js +33 -0
  22. package/dist/chunk-2E6CK2RC.js +392 -0
  23. package/dist/chunk-2EBWGAXD.js +51 -0
  24. package/dist/chunk-2HDP7RMR.js +119 -0
  25. package/dist/chunk-2HKD2UFM.js +1000 -0
  26. package/dist/chunk-2VYRLULP.js +51 -0
  27. package/dist/chunk-2XN7PM4N.js +125 -0
  28. package/dist/chunk-35H7GVNR.js +66 -0
  29. package/dist/chunk-3C3R7JXY.js +77 -0
  30. package/dist/chunk-3VGD2FXB.js +118 -0
  31. package/dist/chunk-424AP7FM.js +534 -0
  32. package/dist/chunk-4J5TZGOE.js +3707 -0
  33. package/dist/chunk-4QJ3Y5JM.js +898 -0
  34. package/dist/chunk-4U2ET7ZY.js +1326 -0
  35. package/dist/chunk-4XPFLICE.js +876 -0
  36. package/dist/chunk-5B2OCU5P.js +31707 -0
  37. package/dist/chunk-5GGOGLSN.js +73 -0
  38. package/dist/chunk-5JRZPLI6.js +251 -0
  39. package/dist/chunk-5TQ3JYBU.js +397 -0
  40. package/dist/chunk-66JPZSGX.js +55 -0
  41. package/dist/chunk-6HSVXHCZ.js +764 -0
  42. package/dist/chunk-6S5FPUS7.js +21 -0
  43. package/dist/chunk-722LQO64.js +131 -0
  44. package/dist/chunk-73NJ7IT7.js +354 -0
  45. package/dist/chunk-7FW3I7WC.js +95 -0
  46. package/dist/chunk-7KUCBNLB.js +173 -0
  47. package/dist/chunk-7RDOV3DF.js +201 -0
  48. package/dist/chunk-A5MNH4ER.js +79 -0
  49. package/dist/chunk-ADIVZ2D5.js +15 -0
  50. package/dist/chunk-AE3P3F6W.js +366 -0
  51. package/dist/chunk-AERM3VGP.js +280 -0
  52. package/dist/chunk-AEWHC4WM.js +246 -0
  53. package/dist/chunk-AOVQ7VIF.js +169 -0
  54. package/dist/chunk-AOYHP3WI.js +12411 -0
  55. package/dist/chunk-B3YEBABJ.js +7160 -0
  56. package/dist/chunk-BCQXEZPJ.js +154 -0
  57. package/dist/chunk-BKTQAYO2.js +418 -0
  58. package/dist/chunk-BSECIPAQ.js +80 -0
  59. package/dist/chunk-BUIMGNDD.js +52 -0
  60. package/dist/chunk-BXHV3GSB.js +10320 -0
  61. package/dist/chunk-C6ZVZCPP.js +139 -0
  62. package/dist/chunk-CEZNQVWM.js +82 -0
  63. package/dist/chunk-CGFZGR7G.js +1492 -0
  64. package/dist/chunk-CGOADM2X.js +337 -0
  65. package/dist/chunk-CI4R6P2B.js +24 -0
  66. package/dist/chunk-D2TCGRSX.js +73 -0
  67. package/dist/chunk-D4PTYFOG.js +154 -0
  68. package/dist/chunk-D54ZWG4T.js +717 -0
  69. package/dist/chunk-DEBI37QB.js +145 -0
  70. package/dist/chunk-DWROHPXP.js +1051 -0
  71. package/dist/chunk-DZ6UOJ5D.js +92 -0
  72. package/dist/chunk-EBJTVRQN.js +81 -0
  73. package/dist/chunk-EGEI2RQA.js +1167 -0
  74. package/dist/chunk-EHHUASO5.js +1305 -0
  75. package/dist/chunk-EPKTIOED.js +41 -0
  76. package/dist/chunk-ER5JW2QZ.js +5681 -0
  77. package/dist/chunk-ETHFACAB.js +315 -0
  78. package/dist/chunk-EXUE5W2F.js +112 -0
  79. package/dist/chunk-EXXICUIW.js +73 -0
  80. package/dist/chunk-FFQSZJOO.js +169 -0
  81. package/dist/chunk-FHFKPGDO.js +85 -0
  82. package/dist/chunk-FW3QKMFX.js +158 -0
  83. package/dist/chunk-GD4ZNZ7W.js +30 -0
  84. package/dist/chunk-GHVMWF2I.js +581 -0
  85. package/dist/chunk-GMKAXSYQ.js +297 -0
  86. package/dist/chunk-HBVF6BWO.js +325 -0
  87. package/dist/chunk-HV4YGVMS.js +379 -0
  88. package/dist/chunk-ILBCKAV6.js +89 -0
  89. package/dist/chunk-J4U4AW46.js +440 -0
  90. package/dist/chunk-J6ESSM47.js +1791 -0
  91. package/dist/chunk-JBZHIC6T.js +2309 -0
  92. package/dist/chunk-JCFWOULR.js +245 -0
  93. package/dist/chunk-JRF5L6OD.js +131 -0
  94. package/dist/chunk-JSS75KCG.js +2014 -0
  95. package/dist/chunk-JSXWIWWJ.js +225 -0
  96. package/dist/chunk-JYV7I2QH.js +98 -0
  97. package/dist/chunk-KIOXTQQL.js +1596 -0
  98. package/dist/chunk-KWVTXT5B.js +89 -0
  99. package/dist/chunk-KX22RD6R.js +33 -0
  100. package/dist/chunk-LA6IPTGH.js +709 -0
  101. package/dist/chunk-LH5VCGLB.js +1796 -0
  102. package/dist/chunk-LQQ6XJ2Y.js +269 -0
  103. package/dist/chunk-MJQ55WZZ.js +65 -0
  104. package/dist/chunk-ML3UKMH3.js +40 -0
  105. package/dist/chunk-MOZPLHSU.js +5526 -0
  106. package/dist/chunk-NGEWJ4DF.js +89 -0
  107. package/dist/chunk-NH6VIOAR.js +211 -0
  108. package/dist/chunk-O5VZQKCB.js +26106 -0
  109. package/dist/chunk-OHSV42QB.js +748 -0
  110. package/dist/chunk-OO3TAHO5.js +4070 -0
  111. package/dist/chunk-OTNHSAGN.js +38282 -0
  112. package/dist/chunk-OUWAC7AQ.js +442 -0
  113. package/dist/chunk-OXMIV4WE.js +270 -0
  114. package/dist/chunk-P5KJURUR.js +229 -0
  115. package/dist/chunk-P6GJOVO6.js +307 -0
  116. package/dist/chunk-QARAGUXX.js +23 -0
  117. package/dist/chunk-R43HJCDX.js +2711 -0
  118. package/dist/chunk-RAXBAWRN.js +210 -0
  119. package/dist/chunk-RK3JHWPT.js +39 -0
  120. package/dist/chunk-RWA23GMM.js +24 -0
  121. package/dist/chunk-S4K4L6IM.js +179 -0
  122. package/dist/chunk-SAXK6LVF.js +124 -0
  123. package/dist/chunk-SEPGEJNK.js +1607 -0
  124. package/dist/chunk-SUJERP5I.js +285 -0
  125. package/dist/chunk-T3ED6BY2.js +913 -0
  126. package/dist/chunk-THKDVSEC.js +90 -0
  127. package/dist/chunk-TI4GSEL5.js +52 -0
  128. package/dist/chunk-TPI73L57.js +164 -0
  129. package/dist/chunk-TVHW77C7.js +338 -0
  130. package/dist/chunk-TWNHOSDT.js +1073 -0
  131. package/dist/chunk-UFMN76QD.js +347 -0
  132. package/dist/chunk-UHGLFRZE.js +205 -0
  133. package/dist/chunk-UTZYPKBF.js +15231 -0
  134. package/dist/chunk-UXNLELW6.js +25 -0
  135. package/dist/chunk-UXXS53EA.js +166 -0
  136. package/dist/chunk-V5DHFGI7.js +171 -0
  137. package/dist/chunk-V5YIPMHV.js +2353 -0
  138. package/dist/chunk-WMUWEO2C.js +754 -0
  139. package/dist/chunk-WOE3D7JM.js +945 -0
  140. package/dist/chunk-X2IK5LVR.js +2592 -0
  141. package/dist/chunk-XU2YXCDL.js +459 -0
  142. package/dist/chunk-YJG5HIX5.js +203 -0
  143. package/dist/chunk-Z3QE7TZM.js +72 -0
  144. package/dist/chunk-Z5Z3JHJY.js +64 -0
  145. package/dist/chunk-ZDIFBJFT.js +168 -0
  146. package/dist/chunk-ZGGCEXDC.js +176 -0
  147. package/dist/chunk-ZMZCZNKU.js +263 -0
  148. package/dist/chunk-ZNC5T65N.js +17 -0
  149. package/dist/chunk-ZO3I3A7O.js +308 -0
  150. package/dist/chunk-ZQKXBUB7.js +217 -0
  151. package/dist/chunk-ZTRINFM2.js +78 -0
  152. package/dist/chunk-ZU7JZCVP.js +193 -0
  153. package/dist/chunk-ZZAWNALS.js +350 -0
  154. package/dist/cli/entry.js +54 -0
  155. package/dist/connector-L5QQTR6Y.js +26 -0
  156. package/dist/context-62DC6EC3.js +31 -0
  157. package/dist/council-convene-NJWPNBDU.js +46 -0
  158. package/dist/create-agent-config-ODSQWNVC.js +55 -0
  159. package/dist/default-registry-2PU3BTAU.js +68 -0
  160. package/dist/deliver-task-KONV4C4M.js +57 -0
  161. package/dist/delivery-gate-v2-F2YVMVGK.js +27 -0
  162. package/dist/domain-knowledge-store-DDZ3F4J7.js +11 -0
  163. package/dist/fetch-core-ZPPYDSZH.js +14 -0
  164. package/dist/galaxy-K72BBDQN.js +91 -0
  165. package/dist/goal-criteria-SCALAU2D.js +141 -0
  166. package/dist/goal-persist-XWY2GDQX.js +19 -0
  167. package/dist/goal-tracker-FXMJ2GVK.js +11 -0
  168. package/dist/headless-LSB6I7PT.js +162 -0
  169. package/dist/headless-coordinator-VDPDMVUT.js +90 -0
  170. package/dist/inject-U55SMB6A.js +39 -0
  171. package/dist/login-flow-CD4ZGXOC.js +59 -0
  172. package/dist/logs-cli-SQVYFMMN.js +283 -0
  173. package/dist/loop-ULZYBDA3.js +76 -0
  174. package/dist/main.js +4064 -0
  175. package/dist/manager-ROZBCWPA.js +171 -0
  176. package/dist/manager-Z7XFFUOM.js +10670 -0
  177. package/dist/meridian-indexer-ZIW363L7.js +21 -0
  178. package/dist/native-resolver-PN34UIVV.js +13 -0
  179. package/dist/node_modules/better-sqlite3/lib/database.js +90 -0
  180. package/dist/node_modules/better-sqlite3/lib/index.js +3 -0
  181. package/dist/node_modules/better-sqlite3/lib/methods/aggregate.js +43 -0
  182. package/dist/node_modules/better-sqlite3/lib/methods/backup.js +67 -0
  183. package/dist/node_modules/better-sqlite3/lib/methods/function.js +31 -0
  184. package/dist/node_modules/better-sqlite3/lib/methods/inspect.js +7 -0
  185. package/dist/node_modules/better-sqlite3/lib/methods/pragma.js +12 -0
  186. package/dist/node_modules/better-sqlite3/lib/methods/serialize.js +16 -0
  187. package/dist/node_modules/better-sqlite3/lib/methods/table.js +189 -0
  188. package/dist/node_modules/better-sqlite3/lib/methods/transaction.js +78 -0
  189. package/dist/node_modules/better-sqlite3/lib/methods/wrappers.js +54 -0
  190. package/dist/node_modules/better-sqlite3/lib/sqlite-error.js +20 -0
  191. package/dist/node_modules/better-sqlite3/lib/util.js +12 -0
  192. package/dist/node_modules/better-sqlite3/package.json +59 -0
  193. package/dist/ownership-ledger-E3WIEKPL.js +9 -0
  194. package/dist/package.json +3 -0
  195. package/dist/path-grants-XRRXHXG7.js +40 -0
  196. package/dist/paths-LVV5IMQN.js +49 -0
  197. package/dist/plan-fact-anchors-LMVQZCW5.js +13 -0
  198. package/dist/plan-store-ODLTTPFB.js +17 -0
  199. package/dist/plan-templates-5FPFBOL6.js +109 -0
  200. package/dist/plugin-loader-ZZKX7ZE6.js +22 -0
  201. package/dist/prefix-budget-62OV5HQC.js +13 -0
  202. package/dist/project-trust-5WS5MOMU.js +31 -0
  203. package/dist/provider-cli-ALDQXIT7.js +37 -0
  204. package/dist/providers-XQYTAVPK.js +11 -0
  205. package/dist/proxy-resolver-V2Y6534D.js +15 -0
  206. package/dist/recovery-cli-KPGY6FY3.js +106 -0
  207. package/dist/review-discipline-MNSYCST5.js +55 -0
  208. package/dist/review-router-BVQZW3MX.js +18 -0
  209. package/dist/seed-capsules/seed-capsule-attack-methodology.md +62 -0
  210. package/dist/seed-capsules/seed-capsule-changgeng.md +31 -0
  211. package/dist/seed-capsules/seed-capsule-design-aesthetics.md +38 -0
  212. package/dist/seed-capsules/seed-capsule-diagnostic-ladder.md +54 -0
  213. package/dist/seed-capsules/seed-capsule-fu.md +39 -0
  214. package/dist/seed-capsules/seed-capsule-huagai.md +73 -0
  215. package/dist/seed-capsules/seed-capsule-kaiyang.md +65 -0
  216. package/dist/seed-capsules/seed-capsule-knowledge-work.md +40 -0
  217. package/dist/seed-capsules/seed-capsule-tanlang.md +47 -0
  218. package/dist/seed-capsules/seed-capsule-tianfu.md +23 -0
  219. package/dist/seed-capsules/seed-capsule-tianliang.md +63 -0
  220. package/dist/seed-capsules/seed-capsule-tianquan.md +32 -0
  221. package/dist/seed-capsules/seed-capsule-tianxuan-ccr.md +22 -0
  222. package/dist/seed-capsules/seed-capsule-tianxuan.md +64 -0
  223. package/dist/seed-capsules/seed-capsule-yaoguang.md +87 -0
  224. package/dist/serve-X2R2UTLF.js +170 -0
  225. package/dist/serve-agent-KHSU6RVV.js +965 -0
  226. package/dist/session-persist-HUUC772H.js +37 -0
  227. package/dist/session-registry-6XUD5C4Y.js +15 -0
  228. package/dist/skill-loader-CK3RMO47.js +48 -0
  229. package/dist/skills/cli-harness-sop.md +207 -0
  230. package/dist/spawn-git-4HFU65AH.js +29 -0
  231. package/dist/sse-EYUTBWUK.js +512 -0
  232. package/dist/starflow-NOFBMPFU.js +30 -0
  233. package/dist/streamableHttp-V67CNJMO.js +447 -0
  234. package/dist/task-contract-6XHBNJQK.js +32 -0
  235. package/dist/task-ledger-3K4TJIER.js +9 -0
  236. package/dist/team-orchestrate-S6NHZP7X.js +64 -0
  237. package/dist/title-extract-QX4XGCYN.js +50 -0
  238. package/dist/todo-QDXGRZZP.js +31 -0
  239. package/dist/todo-store-V3TTO5AH.js +12 -0
  240. package/dist/tool-preset-LU5BYEDZ.js +23 -0
  241. package/dist/tool-tiers-GD742YBN.js +23 -0
  242. package/dist/turndown.es-OI55K6C3.js +17373 -0
  243. package/dist/verification-attribution-EKG4CNUV.js +13 -0
  244. package/dist/volatile-git-MBTST7RA.js +15 -0
  245. package/dist/wave-checkpoint-XLIIHXQA.js +30 -0
  246. package/dist/web-cli-DUG4I35O.js +233 -0
  247. package/dist/web-search-3IK542AA.js +34 -0
  248. package/dist/workers/cpu-worker.js +45 -0
  249. package/dist/workflow-runner-HX7UBAYG.js +297 -0
  250. package/dist/worktree-baseline-O5NCU6M2.js +9 -0
  251. package/package.json +122 -0
  252. package/scripts/check-mobile-env.mjs +76 -0
  253. package/scripts/fetch-native-sqlite.js +392 -0
  254. package/scripts/prune-installed-wasms.js +45 -0
  255. package/scripts/tree-sitter-wasm-keep.js +40 -0
@@ -0,0 +1,1796 @@
1
+ #!/usr/bin/env -S node --expose-gc --max-old-space-size=4096
2
+ import { createRequire as __rivetCreateRequire } from 'node:module'; const require = __rivetCreateRequire(import.meta.url);
3
+ import {
4
+ parseFrozenSnapshotData
5
+ } from "./chunk-6S5FPUS7.js";
6
+ import {
7
+ normalizeOaiMessage,
8
+ normalizeOaiMessages
9
+ } from "./chunk-EXXICUIW.js";
10
+ import {
11
+ stableStringify
12
+ } from "./chunk-QARAGUXX.js";
13
+ import {
14
+ writeFileAtomicAsync,
15
+ writeFileAtomicSync
16
+ } from "./chunk-A5MNH4ER.js";
17
+ import {
18
+ init_paths,
19
+ sessionsDir
20
+ } from "./chunk-BCQXEZPJ.js";
21
+ import {
22
+ init_esm_shims
23
+ } from "./chunk-5GGOGLSN.js";
24
+
25
+ // src/agent/session-persist.ts
26
+ init_esm_shims();
27
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync5, unlinkSync as unlinkSync2, rmSync, readdirSync, statSync as statSync2 } from "fs";
28
+ init_paths();
29
+ import { isAbsolute, join as join5, relative, resolve } from "path";
30
+
31
+ // src/context/session-memory.ts
32
+ init_esm_shims();
33
+ import { existsSync, readFileSync } from "fs";
34
+ import { join } from "path";
35
+ import { createHash } from "crypto";
36
+
37
+ // src/validation.ts
38
+ init_esm_shims();
39
+ var SESSION_ID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
40
+ function isValidSessionId(id) {
41
+ return SESSION_ID_RE.test(id);
42
+ }
43
+ function assertValidSessionId(id) {
44
+ if (!isValidSessionId(id)) throw new Error(`Invalid sessionId: ${id}`);
45
+ }
46
+
47
+ // src/context/session-memory.ts
48
+ function memoryPath(dir, sessionId) {
49
+ assertValidSessionId(sessionId);
50
+ return join(dir, `${sessionId}.memory.json`);
51
+ }
52
+ function idFor(input) {
53
+ return createHash("sha256").update(`${input.createdAt}:${input.source}:${input.text}`).digest("hex").slice(0, 12);
54
+ }
55
+ function loadSessionMemory(dir, sessionId) {
56
+ const filePath = memoryPath(dir, sessionId);
57
+ if (!existsSync(filePath)) return { sessionId, entries: [] };
58
+ try {
59
+ return JSON.parse(readFileSync(filePath, "utf-8"));
60
+ } catch {
61
+ return { sessionId, entries: [] };
62
+ }
63
+ }
64
+ function appendSessionMemory(dir, sessionId, input) {
65
+ const state = loadSessionMemory(dir, sessionId);
66
+ const duplicate = state.entries.some((entry2) => entry2.text === input.text && entry2.source === input.source);
67
+ if (duplicate) return state;
68
+ const entry = { id: idFor(input), ...input };
69
+ const next = { sessionId, entries: [...state.entries, entry].slice(-50) };
70
+ writeFileAtomicSync(memoryPath(dir, sessionId), JSON.stringify(next, null, 2) + "\n");
71
+ return next;
72
+ }
73
+ function escapeXml(text) {
74
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
75
+ }
76
+ function buildSessionMemoryBlock(state) {
77
+ if (state.entries.length === 0) return "";
78
+ const entries = state.entries.map((entry) => `<entry id="${entry.id}" created_at="${entry.createdAt}" source="${entry.source}">${escapeXml(entry.text)}</entry>`);
79
+ return `<session-memory session_id="${state.sessionId}">
80
+ ${entries.join("\n")}
81
+ </session-memory>`;
82
+ }
83
+
84
+ // src/context/claim-store.ts
85
+ init_esm_shims();
86
+ import { appendFileSync, existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, statSync, unlinkSync } from "fs";
87
+ import { appendFile, rename, writeFile } from "fs/promises";
88
+ import { join as join3 } from "path";
89
+
90
+ // src/context/claims.ts
91
+ init_esm_shims();
92
+ import { createHash as createHash2 } from "crypto";
93
+ function normalizeClaimText(text) {
94
+ return text.trim().replace(/\s+/g, " ").toLowerCase();
95
+ }
96
+ function claimIdFor(proposal) {
97
+ return createHash2("sha256").update(JSON.stringify({
98
+ kind: proposal.kind,
99
+ scope: proposal.scope,
100
+ text: normalizeClaimText(proposal.text),
101
+ sessionId: proposal.source.sessionId
102
+ })).digest("hex").slice(0, 12);
103
+ }
104
+ function kindFromAnchor(anchor) {
105
+ if (anchor.kind === "user_constraint") return "user_constraint";
106
+ if (anchor.kind === "user_preference") return "user_preference";
107
+ if (anchor.kind === "decision") return "decision";
108
+ if (anchor.kind === "verification") return "verification_fact";
109
+ if (anchor.kind === "error") return "failure_pattern";
110
+ return "file_observation";
111
+ }
112
+ function confidenceFromAnchor(anchor) {
113
+ if (anchor.kind === "user_constraint") return 0.9;
114
+ if (anchor.kind === "decision") return 0.82;
115
+ if (anchor.kind === "verification") return 0.88;
116
+ return 0.7;
117
+ }
118
+ function claimProposalFromAnchor(anchor, meta) {
119
+ return {
120
+ kind: kindFromAnchor(anchor),
121
+ scope: "session",
122
+ text: anchor.text,
123
+ confidence: confidenceFromAnchor(anchor),
124
+ fitness: anchor.salience,
125
+ source: {
126
+ actor: meta.actor,
127
+ sessionId: meta.sessionId,
128
+ turn: meta.turn,
129
+ eventId: meta.eventId
130
+ },
131
+ evidence: [{
132
+ id: `${meta.eventId}:anchor`,
133
+ kind: meta.actor === "user" ? "user_message" : "assistant_message",
134
+ summary: anchor.text,
135
+ createdAt: meta.createdAt
136
+ }],
137
+ createdAt: meta.createdAt,
138
+ tags: ["anchor", anchor.kind]
139
+ };
140
+ }
141
+ function createClaimFromProposal(proposal) {
142
+ return {
143
+ id: claimIdFor(proposal),
144
+ kind: proposal.kind,
145
+ scope: proposal.scope,
146
+ status: "active",
147
+ text: proposal.text,
148
+ confidence: proposal.confidence,
149
+ fitness: proposal.fitness,
150
+ source: proposal.source,
151
+ evidence: [...proposal.evidence],
152
+ consumers: [],
153
+ counterevidence: [],
154
+ createdAt: proposal.createdAt,
155
+ lastUsedAt: proposal.createdAt,
156
+ expiresAt: proposal.expiresAt,
157
+ tags: [...proposal.tags]
158
+ };
159
+ }
160
+ function isPromptEligibleClaim(claim, now = Date.now()) {
161
+ if (claim.expiresAt !== void 0 && claim.expiresAt <= now) return false;
162
+ return claim.status === "active" || claim.status === "durable_candidate" || claim.status === "durable";
163
+ }
164
+ function checkpointClaims(claims, now = Date.now(), lastEventSeq) {
165
+ const alive = claims.filter((c) => {
166
+ if (c.status === "stale" || c.status === "quarantined") return false;
167
+ if (c.expiresAt !== void 0 && c.expiresAt <= now) return false;
168
+ return true;
169
+ });
170
+ return {
171
+ version: 1,
172
+ createdAt: now,
173
+ ...lastEventSeq !== void 0 ? { lastEventSeq } : {},
174
+ claims: alive
175
+ };
176
+ }
177
+ function loadClaimSnapshot(snapshot, now = Date.now()) {
178
+ if (snapshot.version !== 1) return [];
179
+ return snapshot.claims.map((claim) => ({
180
+ ...claim,
181
+ lastUsedAt: now
182
+ }));
183
+ }
184
+
185
+ // src/context/promotion.ts
186
+ init_esm_shims();
187
+ import { existsSync as existsSync2 } from "fs";
188
+ import { join as join2 } from "path";
189
+ function canRecallClaim(claim, cwd) {
190
+ if (!cwd) return true;
191
+ const filePaths = claim.evidence.filter((e) => e.path !== void 0).map((e) => e.path);
192
+ if (filePaths.length === 0) return true;
193
+ return filePaths.some((p) => existsSync2(join2(cwd, p)));
194
+ }
195
+ function evaluatePromotion(claim, now = Date.now()) {
196
+ if (!isPromptEligibleClaim(claim, now)) return null;
197
+ if (claim.counterevidence.length > 0) return null;
198
+ if (claim.status === "active") {
199
+ if (new Set(claim.consumers.map((c) => c.id)).size < 3) return null;
200
+ return "durable_candidate";
201
+ }
202
+ if (claim.status === "durable_candidate") {
203
+ const age = now - claim.createdAt;
204
+ if (age < 10 * 6e4) return null;
205
+ if (new Set(claim.consumers.map((c) => c.id)).size < 5) return null;
206
+ return "durable";
207
+ }
208
+ return null;
209
+ }
210
+ function claimHasFileEvidence(claim, path) {
211
+ if (claim.kind !== "file_observation" && claim.kind !== "verification_fact") return false;
212
+ return claim.evidence.some((evidence) => evidence.path === path);
213
+ }
214
+ function countClaimsByStatus(claims) {
215
+ return claims.reduce((counts, c) => {
216
+ if (c.status === "active") return { ...counts, active: counts.active + 1 };
217
+ if (c.status === "stale") return { ...counts, stale: counts.stale + 1 };
218
+ if (c.status === "conflicted") return { ...counts, conflicted: counts.conflicted + 1 };
219
+ if (c.status === "durable") return { ...counts, durable: counts.durable + 1 };
220
+ if (c.status === "durable_candidate") return { ...counts, durableCandidate: counts.durableCandidate + 1 };
221
+ if (c.status === "quarantined") return { ...counts, quarantined: counts.quarantined + 1 };
222
+ return counts;
223
+ }, { active: 0, stale: 0, conflicted: 0, durable: 0, durableCandidate: 0, quarantined: 0, recallBlocked: 0 });
224
+ }
225
+
226
+ // src/context/claim-store.ts
227
+ var MAX_CONSUMERS_PER_CLAIM = 50;
228
+ var MAX_ACTIVE_CLAIMS = 50;
229
+ var DEFAULT_CHECKPOINT_EVERY_EVENTS = 500;
230
+ var DEFAULT_MAX_TRANSIENT_WRITE_RETRIES = 8;
231
+ var ContextClaimStore = class _ContextClaimStore {
232
+ path;
233
+ sessionId;
234
+ cachedEvents = null;
235
+ lastFileSize = -1;
236
+ cachedClaims = null;
237
+ lastProcessedLineCount = 0;
238
+ snapshotPath;
239
+ // ⚠️ Single-writer assumption: seq is per-instance, not coordinated across processes.
240
+ // If multiple processes append to the same .claims.jsonl concurrently, seq may collide.
241
+ // This is acceptable because claim-store is session-scoped and each session has exactly one writer.
242
+ nextSeq = 1;
243
+ checkpointing = false;
244
+ checkpointEveryEvents;
245
+ maxTransientWriteRetries;
246
+ constructor(dir, sessionId, options = {}) {
247
+ assertValidSessionId(sessionId);
248
+ this.sessionId = sessionId;
249
+ this.checkpointEveryEvents = options.checkpointEveryEvents ?? DEFAULT_CHECKPOINT_EVERY_EVENTS;
250
+ this.maxTransientWriteRetries = options.maxTransientWriteRetries ?? DEFAULT_MAX_TRANSIENT_WRITE_RETRIES;
251
+ mkdirSync(dir, { recursive: true });
252
+ this.path = join3(dir, `${this.sessionId}.claims.jsonl`);
253
+ this.snapshotPath = join3(dir, `${this.sessionId}.claims.snapshot.json`);
254
+ }
255
+ get eventCount() {
256
+ return this.readEvents().length;
257
+ }
258
+ appendEvent(event) {
259
+ if (!this.cachedEvents) {
260
+ if (existsSync3(this.path)) this.readEvents();
261
+ const checkpointSnapshot = this.loadFromCheckpoint();
262
+ if (checkpointSnapshot) this.nextSeq = Math.max(this.nextSeq, checkpointSnapshot.lastEventSeq + 1);
263
+ if (!this.cachedEvents) this.cachedEvents = [];
264
+ if (this.lastFileSize < 0) this.lastFileSize = 0;
265
+ }
266
+ const withSeq = { ...event, seq: event.seq ?? this.nextSeq };
267
+ const line = JSON.stringify(withSeq) + "\n";
268
+ const bytes = Buffer.byteLength(line);
269
+ this.pendingLines.push(line);
270
+ this.pendingBytes += bytes;
271
+ this.kickWriteChain();
272
+ this.nextSeq = Math.max(this.nextSeq, (withSeq.seq ?? 0) + 1);
273
+ this.cachedEvents.push(withSeq);
274
+ this.lastFileSize += bytes;
275
+ if (!this.checkpointing && this.checkpointEveryEvents !== void 0 && this.checkpointEveryEvents > 0 && this.cachedEvents !== null && this.cachedEvents.length >= this.checkpointEveryEvents) {
276
+ this.checkpointQueued = true;
277
+ this.kickWriteChain();
278
+ }
279
+ }
280
+ // ── 写链(claims.jsonl 唯一写者)─────────────────────────────────────
281
+ pendingLines = [];
282
+ /** 在途未写字节数:readEvents 外部修改检测的「磁盘合法滞后」基线。 */
283
+ pendingBytes = 0;
284
+ writeChain = { running: false, again: false };
285
+ checkpointQueued = false;
286
+ /** 重试不会改变结果的写入错误码(路径不存在 / 不是目录 / 只读文件系统)。 */
287
+ static PERMANENT_WRITE_ERROR_CODES = /* @__PURE__ */ new Set([
288
+ "ENOENT",
289
+ "ENOTDIR",
290
+ "EROFS",
291
+ "EISDIR"
292
+ ]);
293
+ /** 撞上永久性错误后置起:停止自动重试。继续重试只会留下永不终止的 250ms
294
+ * 定时器链,使事件循环永远非空——node --test 因此既不打印汇总也不退出
295
+ * (实测:context-injection.test.ts 的 finally 删掉本 store 的目录后整批挂死)。
296
+ * 新 kick(新数据 / flushWrites)时清位,允许环境恢复后再试。 */
297
+ permanentWriteFailure = false;
298
+ /** 最近一次写失败(诊断用,停链时读取输出现场日志)。 */
299
+ lastWriteError = null;
300
+ /** 短暂性错误(EACCES/EPERM/EBUSY——AV/EDR 秒级锁会自愈,及未知码)的连续
301
+ * 失败计数:成功写入即清零;达 maxTransientWriteRetries 才转永久停链。
302
+ * 一次 EACCES 即停链会让锁释放后滞留行永久留在内存(agent-16)。 */
303
+ transientWriteRetries = 0;
304
+ /** 停链诊断「每实例至多一次」的独立闸:不复用 lastWriteError 判定——
305
+ * checkpoint 段异常(kickWriteChain 的 catch)也会赋 lastWriteError,
306
+ * 复用会让「首次真停链」静默无日志。 */
307
+ stallLogged = false;
308
+ /** 诊断读取点:最近一次的写失败(写链停链原因 / checkpoint 段异常),
309
+ * 供排障与测试断言使用(lastWriteError 的真实消费点)。 */
310
+ getLastWriteError() {
311
+ return this.lastWriteError;
312
+ }
313
+ /** 写链停链的现场记录:置状态 + 首次停链输出一次诊断(写链停了 = 滞留行留
314
+ * 在内存,值得在日志里可见;后续重试失败不再重复打)。 */
315
+ stallWriteChain(err) {
316
+ const error = err instanceof Error ? err : new Error(String(err));
317
+ this.permanentWriteFailure = true;
318
+ this.lastWriteError = error;
319
+ if (!this.stallLogged) {
320
+ this.stallLogged = true;
321
+ console.error(
322
+ `[claim-store] \u5199\u94FE\u505C\u94FE\uFF08${error.message}\uFF09\u2014\u2014\u6EDE\u7559\u884C\u4FDD\u7559\u5728\u5185\u5B58\uFF0C\u73AF\u5883\u6062\u590D\u540E\u7531\u65B0\u4E8B\u4EF6 / flushWrites \u91CD\u8BD5`
323
+ );
324
+ }
325
+ }
326
+ kickWriteChain() {
327
+ if (this.writeChain.running) {
328
+ this.writeChain.again = true;
329
+ return;
330
+ }
331
+ this.permanentWriteFailure = false;
332
+ this.transientWriteRetries = 0;
333
+ queueMicrotask(() => {
334
+ if (this.writeChain.running) return;
335
+ this.writeChain.running = true;
336
+ void this.runWriteChain().catch((err) => {
337
+ this.lastWriteError = err instanceof Error ? err : new Error(String(err));
338
+ });
339
+ });
340
+ }
341
+ async runWriteChain() {
342
+ try {
343
+ for (; ; ) {
344
+ this.writeChain.again = false;
345
+ const lines = this.pendingLines;
346
+ const wantCheckpoint = this.checkpointQueued;
347
+ this.checkpointQueued = false;
348
+ if (lines.length === 0 && !wantCheckpoint) break;
349
+ if (lines.length > 0) {
350
+ this.pendingLines = [];
351
+ const text = lines.join("");
352
+ try {
353
+ await appendFile(this.path, text, "utf-8");
354
+ this.pendingBytes -= Buffer.byteLength(text);
355
+ this.transientWriteRetries = 0;
356
+ } catch (err) {
357
+ this.pendingLines = [...lines, ...this.pendingLines];
358
+ const code = err.code ?? "";
359
+ if (_ContextClaimStore.PERMANENT_WRITE_ERROR_CODES.has(code)) {
360
+ this.stallWriteChain(err);
361
+ return;
362
+ }
363
+ this.transientWriteRetries++;
364
+ if (this.transientWriteRetries >= this.maxTransientWriteRetries) {
365
+ this.stallWriteChain(err);
366
+ return;
367
+ }
368
+ await new Promise((r) => setTimeout(r, 250));
369
+ continue;
370
+ }
371
+ }
372
+ if (wantCheckpoint) await this.checkpointOnChain();
373
+ if (!this.writeChain.again && this.pendingLines.length === 0 && !this.checkpointQueued) break;
374
+ }
375
+ } finally {
376
+ this.writeChain.running = false;
377
+ this.writeChain.again = false;
378
+ if (!this.permanentWriteFailure && (this.pendingLines.length > 0 || this.checkpointQueued)) {
379
+ this.kickWriteChain();
380
+ }
381
+ }
382
+ }
383
+ /** checkpoint 的链内异步版:快照由内存态派生(cachedEvents/cachedClaims 在
384
+ * appendEvent 时同步更新),文件写全部异步,截断与后续 append 由链保序。 */
385
+ async checkpointOnChain(now = Date.now()) {
386
+ this.checkpointing = true;
387
+ try {
388
+ const snapshot = checkpointClaims(this.listClaims(), now, this.maxEventSeq(this.readEvents()));
389
+ const tmpSnapshot = `${this.snapshotPath}.tmp`;
390
+ await writeFile(tmpSnapshot, JSON.stringify(snapshot, null, 2) + "\n", "utf-8");
391
+ await rename(tmpSnapshot, this.snapshotPath);
392
+ const tmpLog = `${this.path}.tmp`;
393
+ await writeFile(tmpLog, "", "utf-8");
394
+ await rename(tmpLog, this.path);
395
+ this.cachedEvents = [];
396
+ this.cachedClaims = loadClaimSnapshot(snapshot, now);
397
+ this.lastProcessedLineCount = 0;
398
+ this.lastFileSize = 0;
399
+ this.pendingBytes = 0;
400
+ } finally {
401
+ this.checkpointing = false;
402
+ }
403
+ }
404
+ /** 等写链排空(测试/收口用)。 */
405
+ async flushWrites(timeoutMs = 1e4) {
406
+ this.kickWriteChain();
407
+ const deadline = Date.now() + timeoutMs;
408
+ let requeued = false;
409
+ for (; ; ) {
410
+ if (!this.writeChain.running && this.pendingLines.length === 0 && !this.checkpointQueued) return;
411
+ if (!this.writeChain.running && (this.pendingLines.length > 0 || this.checkpointQueued)) {
412
+ if (requeued) return;
413
+ requeued = true;
414
+ this.kickWriteChain();
415
+ }
416
+ if (Date.now() > deadline) return;
417
+ await new Promise((r) => setTimeout(r, 10));
418
+ }
419
+ }
420
+ propose(proposal) {
421
+ const claim = createClaimFromProposal(proposal);
422
+ const existing = this.listClaims().find((current) => current.id === claim.id);
423
+ if (existing) return existing;
424
+ this.appendEvent({
425
+ type: "claim_proposed",
426
+ eventId: `${proposal.source.eventId}:claim:${claim.id}`,
427
+ createdAt: proposal.createdAt,
428
+ claim
429
+ });
430
+ this.evictExcessActiveClaims();
431
+ return claim;
432
+ }
433
+ updateClaimStatus(id, status, reason) {
434
+ const current = this.listClaims().find((claim) => claim.id === id);
435
+ if (!current) return null;
436
+ this.appendEvent({
437
+ type: "claim_status_changed",
438
+ eventId: `${id}:status:${status}:${Date.now()}`,
439
+ createdAt: Date.now(),
440
+ claimId: id,
441
+ status,
442
+ reason
443
+ });
444
+ return this.listClaims().find((claim) => claim.id === id) ?? null;
445
+ }
446
+ recordClaimUsed(id, input) {
447
+ const current = this.listClaims().find((claim) => claim.id === id);
448
+ if (!current) return null;
449
+ this.appendEvent({
450
+ type: "claim_used",
451
+ eventId: `${id}:used:${input.consumerId}:${input.usedAt}`,
452
+ createdAt: input.usedAt,
453
+ claimId: id,
454
+ consumerId: input.consumerId,
455
+ consumerKind: input.consumerKind
456
+ });
457
+ return this.listClaims().find((claim) => claim.id === id) ?? null;
458
+ }
459
+ boostFitness(id, delta, cap) {
460
+ const claim = this.listClaims().find((c) => c.id === id);
461
+ if (!claim) return null;
462
+ const newFitness = Math.min(claim.fitness + delta, cap);
463
+ this.appendEvent({
464
+ type: "claim_boosted",
465
+ eventId: `${id}:boost:${Date.now()}`,
466
+ createdAt: Date.now(),
467
+ claimId: id,
468
+ fitness: newFitness
469
+ });
470
+ return { ...claim, fitness: newFitness };
471
+ }
472
+ listClaims(filter = {}) {
473
+ return this.projectClaims().filter((claim) => {
474
+ if (filter.status && !filter.status.includes(claim.status)) return false;
475
+ if (filter.kind && !filter.kind.includes(claim.kind)) return false;
476
+ if (filter.scope && !filter.scope.includes(claim.scope)) return false;
477
+ return true;
478
+ });
479
+ }
480
+ listActiveClaims(now = Date.now()) {
481
+ return this.listClaims().filter((claim) => isPromptEligibleClaim(claim, now));
482
+ }
483
+ listClaimsByFileEvidence(path) {
484
+ return this.listClaims().filter((claim) => claimHasFileEvidence(claim, path));
485
+ }
486
+ getStatusCounts() {
487
+ return countClaimsByStatus(this.listClaims());
488
+ }
489
+ markClaimsStaleForFile(path, reason) {
490
+ const changed = [];
491
+ for (const claim of this.listClaimsByFileEvidence(path)) {
492
+ if (claim.status === "stale" || claim.status === "quarantined") continue;
493
+ const updated = this.updateClaimStatus(claim.id, "stale", reason);
494
+ if (updated) changed.push(updated);
495
+ }
496
+ return changed;
497
+ }
498
+ promoteEligibleClaims(now = Date.now(), cwd) {
499
+ const promoted = [];
500
+ for (const claim of this.listClaims()) {
501
+ const next = evaluatePromotion(claim, now);
502
+ if (!next) continue;
503
+ if (!canRecallClaim(claim, cwd)) {
504
+ this.updateClaimStatus(claim.id, "stale", "recall-gate: evidence files no longer exist");
505
+ continue;
506
+ }
507
+ const updated = this.updateClaimStatus(claim.id, next, "promotion threshold met");
508
+ if (updated) promoted.push(updated);
509
+ }
510
+ this.evictExcessActiveClaims();
511
+ return promoted;
512
+ }
513
+ evictExcessActiveClaims() {
514
+ const evictable = this.listActiveClaims().filter((c) => c.status !== "durable");
515
+ if (evictable.length <= MAX_ACTIVE_CLAIMS) return;
516
+ const toEvict = [...evictable].sort((a, b) => a.createdAt - b.createdAt).slice(0, evictable.length - MAX_ACTIVE_CLAIMS);
517
+ for (const claim of toEvict) {
518
+ this.updateClaimStatus(claim.id, "stale", "evicted-overflow");
519
+ }
520
+ }
521
+ exportSession() {
522
+ const disk = existsSync3(this.path) ? readFileSync2(this.path, "utf-8") : "";
523
+ return disk + this.pendingLines.join("");
524
+ }
525
+ /**
526
+ * Checkpoint: write current claims state as a snapshot, then truncate JSONL.
527
+ * Follows Redis 7.0 Base+Incr pattern:
528
+ * - Snapshot = full projected state (base)
529
+ * - JSONL = incremental events after snapshot (incr)
530
+ * - Load = read snapshot + replay incr events
531
+ */
532
+ checkpoint(now = Date.now()) {
533
+ if (this.writeChain.running) {
534
+ this.checkpointQueued = true;
535
+ this.kickWriteChain();
536
+ const snapshot = checkpointClaims(this.listClaims(), now, this.maxEventSeq(this.readEvents()));
537
+ return { snapshotPath: this.snapshotPath, claimCount: snapshot.claims.length, truncatedPath: this.path };
538
+ }
539
+ if (this.pendingLines.length > 0) {
540
+ try {
541
+ appendFileSync(this.path, this.pendingLines.join(""), "utf-8");
542
+ this.pendingLines = [];
543
+ this.pendingBytes = 0;
544
+ } catch {
545
+ }
546
+ }
547
+ this.checkpointing = true;
548
+ try {
549
+ const snapshot = checkpointClaims(this.listClaims(), now, this.maxEventSeq(this.readEvents()));
550
+ writeFileAtomicSync(this.snapshotPath, JSON.stringify(snapshot, null, 2) + "\n");
551
+ writeFileAtomicSync(this.path, "");
552
+ this.cachedEvents = [];
553
+ this.cachedClaims = loadClaimSnapshot(snapshot, now);
554
+ this.lastProcessedLineCount = 0;
555
+ this.lastFileSize = 0;
556
+ return { snapshotPath: this.snapshotPath, claimCount: snapshot.claims.length, truncatedPath: this.path };
557
+ } finally {
558
+ this.checkpointing = false;
559
+ }
560
+ }
561
+ /**
562
+ * Load claims from checkpoint snapshot. Incremental JSONL events are replayed
563
+ * by projectClaims() after this base state is loaded.
564
+ */
565
+ loadFromCheckpoint(now = Date.now()) {
566
+ if (!existsSync3(this.snapshotPath)) return null;
567
+ try {
568
+ const raw = readFileSync2(this.snapshotPath, "utf-8");
569
+ const snapshot = JSON.parse(raw);
570
+ return {
571
+ claims: loadClaimSnapshot(snapshot, now),
572
+ // Old snapshots predate watermarks and were always paired with a
573
+ // truncated JSONL in production. Treat them as base state only.
574
+ lastEventSeq: snapshot.lastEventSeq ?? 0
575
+ };
576
+ } catch {
577
+ return null;
578
+ }
579
+ }
580
+ /** Delete checkpoint snapshot file (for testing or cleanup). */
581
+ deleteCheckpoint() {
582
+ if (existsSync3(this.snapshotPath)) {
583
+ unlinkSync(this.snapshotPath);
584
+ }
585
+ }
586
+ static loadDurableClaims(dir, sessionId) {
587
+ if (!existsSync3(join3(dir, `${sessionId}.claims.jsonl`)) && !existsSync3(join3(dir, `${sessionId}.claims.snapshot.json`))) return [];
588
+ const store = new _ContextClaimStore(dir, sessionId);
589
+ return store.listClaims().filter((c) => c.status === "durable");
590
+ }
591
+ readEvents() {
592
+ if (this.cachedEvents) {
593
+ if (!existsSync3(this.path)) {
594
+ return this.lastFileSize === this.pendingBytes ? this.cachedEvents : [];
595
+ }
596
+ const size = statSync(this.path).size;
597
+ if (size === this.lastFileSize - this.pendingBytes) return this.cachedEvents;
598
+ } else if (!existsSync3(this.path)) {
599
+ return [];
600
+ }
601
+ const content = readFileSync2(this.path, "utf-8");
602
+ this.lastFileSize = Buffer.byteLength(content);
603
+ const events = content.split("\n").filter((line) => line.trim().length > 0).flatMap((line) => {
604
+ try {
605
+ return [JSON.parse(line)];
606
+ } catch {
607
+ return [];
608
+ }
609
+ });
610
+ this.cachedEvents = events;
611
+ this.nextSeq = Math.max(1, this.maxEventSeq(events) + 1);
612
+ return events;
613
+ }
614
+ maxEventSeq(events) {
615
+ return events.reduce((max, event, index) => Math.max(max, event.seq ?? index + 1), 0);
616
+ }
617
+ projectClaims() {
618
+ const events = this.readEvents();
619
+ if (this.cachedClaims && this.lastProcessedLineCount === events.length) {
620
+ return this.cachedClaims;
621
+ }
622
+ if (this.cachedClaims && this.lastProcessedLineCount < events.length) {
623
+ const newEvents = events.slice(this.lastProcessedLineCount);
624
+ const map = new Map(this.cachedClaims.map((c) => [c.id, c]));
625
+ this.applyEventsToMap(map, newEvents);
626
+ this.cachedClaims = [...map.values()];
627
+ this.lastProcessedLineCount = events.length;
628
+ return this.cachedClaims;
629
+ }
630
+ const checkpointSnapshot = this.loadFromCheckpoint();
631
+ if (checkpointSnapshot) {
632
+ const claims2 = new Map(checkpointSnapshot.claims.map((c) => [c.id, c]));
633
+ this.nextSeq = Math.max(this.nextSeq, checkpointSnapshot.lastEventSeq + 1, this.maxEventSeq(events) + 1);
634
+ const replayEvents = events.filter((event, index) => (event.seq ?? index + 1) > checkpointSnapshot.lastEventSeq);
635
+ this.applyEventsToMap(claims2, replayEvents);
636
+ this.cachedClaims = [...claims2.values()];
637
+ this.lastProcessedLineCount = events.length;
638
+ return this.cachedClaims;
639
+ }
640
+ const claims = /* @__PURE__ */ new Map();
641
+ this.applyEventsToMap(claims, events);
642
+ this.cachedClaims = [...claims.values()];
643
+ this.lastProcessedLineCount = events.length;
644
+ return this.cachedClaims;
645
+ }
646
+ applyEventsToMap(claims, events) {
647
+ for (const event of events) {
648
+ if (event.type === "claim_proposed") {
649
+ if (!claims.has(event.claim.id)) {
650
+ claims.set(event.claim.id, event.claim);
651
+ }
652
+ continue;
653
+ }
654
+ if (event.type === "claim_status_changed") {
655
+ const claim = claims.get(event.claimId);
656
+ if (!claim) continue;
657
+ const counterevidence = event.status === "active" ? claim.counterevidence : [...claim.counterevidence, {
658
+ id: event.eventId,
659
+ kind: "tool_result",
660
+ summary: event.reason,
661
+ createdAt: event.createdAt
662
+ }];
663
+ claims.set(event.claimId, { ...claim, status: event.status, counterevidence });
664
+ continue;
665
+ }
666
+ if (event.type === "claim_used") {
667
+ const claim = claims.get(event.claimId);
668
+ if (!claim) continue;
669
+ const newConsumers = [...claim.consumers, {
670
+ id: event.consumerId,
671
+ kind: event.consumerKind,
672
+ usedAt: event.createdAt
673
+ }];
674
+ const cappedConsumers = newConsumers.length > MAX_CONSUMERS_PER_CLAIM ? newConsumers.slice(-MAX_CONSUMERS_PER_CLAIM) : newConsumers;
675
+ claims.set(event.claimId, {
676
+ ...claim,
677
+ lastUsedAt: event.createdAt,
678
+ consumers: cappedConsumers
679
+ });
680
+ continue;
681
+ }
682
+ if (event.type === "claim_boosted") {
683
+ const claim = claims.get(event.claimId);
684
+ if (!claim) continue;
685
+ claims.set(event.claimId, { ...claim, fitness: event.fitness });
686
+ continue;
687
+ }
688
+ }
689
+ }
690
+ };
691
+
692
+ // src/agent/checksum.ts
693
+ init_esm_shims();
694
+ import { createHash as createHash3 } from "crypto";
695
+ function computeLineChecksum(jsonLine) {
696
+ const hash = createHash3("sha256").update(jsonLine).digest("hex");
697
+ return hash.slice(0, 16);
698
+ }
699
+ function appendChecksum(jsonLine) {
700
+ const checksum = computeLineChecksum(jsonLine);
701
+ return `${jsonLine}|${checksum}`;
702
+ }
703
+ function verifyAndExtract(line) {
704
+ const trimmed = line.trim();
705
+ if (!trimmed) {
706
+ return { valid: false, json: "", isLegacy: false, error: "Empty line" };
707
+ }
708
+ const lastPipe = trimmed.lastIndexOf("|");
709
+ if (lastPipe === -1) {
710
+ return { valid: true, json: trimmed, isLegacy: true };
711
+ }
712
+ const jsonPart = trimmed.slice(0, lastPipe);
713
+ const storedChecksum = trimmed.slice(lastPipe + 1);
714
+ if (!/^[0-9a-f]{16}$/.test(storedChecksum)) {
715
+ return { valid: true, json: trimmed, isLegacy: true };
716
+ }
717
+ try {
718
+ JSON.parse(jsonPart);
719
+ } catch {
720
+ return { valid: true, json: trimmed, isLegacy: true };
721
+ }
722
+ const computedChecksum = computeLineChecksum(jsonPart);
723
+ if (computedChecksum !== storedChecksum) {
724
+ return {
725
+ valid: false,
726
+ json: jsonPart,
727
+ isLegacy: false,
728
+ error: `Checksum mismatch: expected ${computedChecksum}, got ${storedChecksum}`
729
+ };
730
+ }
731
+ return { valid: true, json: jsonPart, isLegacy: false };
732
+ }
733
+ function verifyLines(lines) {
734
+ let invalidCount = 0;
735
+ let legacyCount = 0;
736
+ const validLines = [];
737
+ for (const line of lines) {
738
+ const result = verifyAndExtract(line);
739
+ if (result.valid) {
740
+ validLines.push(result.json);
741
+ if (result.isLegacy) legacyCount++;
742
+ } else {
743
+ invalidCount++;
744
+ }
745
+ }
746
+ return { validLines, invalidCount, legacyCount };
747
+ }
748
+
749
+ // src/agent/session-transcript-codec.ts
750
+ init_esm_shims();
751
+ import { constants, zstdCompressSync, zstdDecompressSync } from "zlib";
752
+ var ZSTD_MAGIC_LE = 4247762216;
753
+ var CHECKSUM_OPTIONS = {
754
+ params: { [constants.ZSTD_c_checksumFlag]: 1 }
755
+ };
756
+ function scanZstdFrames(buffer) {
757
+ const frames = [];
758
+ let offset = 0;
759
+ while (offset < buffer.length) {
760
+ const start = offset;
761
+ if (buffer.length - offset < 4) return { frames, tornStart: start };
762
+ if (buffer.readUInt32LE(offset) !== ZSTD_MAGIC_LE) {
763
+ throw new Error(`corrupt session transcript: invalid frame magic at byte ${offset}`);
764
+ }
765
+ offset += 4;
766
+ if (offset === buffer.length) return { frames, tornStart: start };
767
+ const descriptor = buffer.readUInt8(offset);
768
+ offset += 1;
769
+ if ((descriptor & 24) !== 0) {
770
+ throw new Error(`corrupt session transcript: reserved frame-header bit at byte ${offset - 1}`);
771
+ }
772
+ const contentSizeFlag = descriptor >>> 6;
773
+ const singleSegment = (descriptor & 32) !== 0;
774
+ const checksum = (descriptor & 4) !== 0;
775
+ const dictionaryFlag = descriptor & 3;
776
+ const dictionaryBytes = dictionaryFlag === 3 ? 4 : dictionaryFlag;
777
+ const contentSizeBytes = contentSizeFlag === 0 ? singleSegment ? 1 : 0 : 1 << contentSizeFlag;
778
+ const remainingHeaderBytes = (singleSegment ? 0 : 1) + dictionaryBytes + contentSizeBytes;
779
+ if (buffer.length - offset < remainingHeaderBytes) return { frames, tornStart: start };
780
+ offset += remainingHeaderBytes;
781
+ for (; ; ) {
782
+ if (buffer.length - offset < 3) return { frames, tornStart: start };
783
+ const blockHeader = buffer.readUIntLE(offset, 3);
784
+ offset += 3;
785
+ const lastBlock = (blockHeader & 1) !== 0;
786
+ const blockType = blockHeader >>> 1 & 3;
787
+ const blockSize = blockHeader >>> 3;
788
+ if (blockType === 3) {
789
+ throw new Error(`corrupt session transcript: reserved block type at byte ${offset - 3}`);
790
+ }
791
+ const payloadBytes = blockType === 1 ? 1 : blockSize;
792
+ if (buffer.length - offset < payloadBytes) return { frames, tornStart: start };
793
+ offset += payloadBytes;
794
+ if (lastBlock) break;
795
+ }
796
+ if (checksum) {
797
+ if (buffer.length - offset < 4) return { frames, tornStart: start };
798
+ offset += 4;
799
+ }
800
+ frames.push({ start, end: offset });
801
+ }
802
+ return { frames };
803
+ }
804
+ function isZstdFrameStream(buffer) {
805
+ return buffer.length >= 4 && buffer.readUInt32LE(0) === ZSTD_MAGIC_LE;
806
+ }
807
+ function encodeBatch(text) {
808
+ if (text.length === 0) return Buffer.alloc(0);
809
+ return zstdCompressSync(Buffer.from(text, "utf-8"), CHECKSUM_OPTIONS);
810
+ }
811
+ function decodeTranscriptText(buffer) {
812
+ if (buffer.length === 0) return "";
813
+ if (!isZstdFrameStream(buffer)) return buffer.toString("utf-8");
814
+ const { frames } = scanZstdFrames(buffer);
815
+ if (frames.length === 0) return "";
816
+ const parts = [];
817
+ for (const frame of frames) {
818
+ const plain = zstdDecompressSync(buffer.subarray(frame.start, frame.end));
819
+ parts.push(plain.toString("utf-8"));
820
+ }
821
+ return parts.join("");
822
+ }
823
+
824
+ // src/agent/session-batch-writer.ts
825
+ init_esm_shims();
826
+ import { appendFileSync as appendFileSync2, copyFileSync, existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
827
+ import { appendFile as appendFile2, open } from "fs/promises";
828
+ import { join as join4 } from "path";
829
+ var SessionBatchWriter = class {
830
+ constructor(filePath, backupDirProvider) {
831
+ this.filePath = filePath;
832
+ this.backupDirProvider = backupDirProvider;
833
+ }
834
+ filePath;
835
+ backupDirProvider;
836
+ /** Write-behind batch: JSONL text waiting for the next flush. */
837
+ pendingBuffer = "";
838
+ flushTimer = null;
839
+ flushPromise = null;
840
+ /** Set once the transcript file is known to be zstd-frame format. */
841
+ codecReady = false;
842
+ /**
843
+ * Queue a JSONL line into the batch. The batch is flushed by a 200ms timer,
844
+ * an explicit flush barrier, or compaction — whichever comes first.
845
+ *
846
+ * Exception: the first line of a brand-new session flushes synchronously so
847
+ * the session file exists immediately, and a crash within the first 200ms
848
+ * cannot lose the entire session.
849
+ */
850
+ enqueueLine(line) {
851
+ this.pendingBuffer += line;
852
+ if (this.flushTimer === null) {
853
+ if (!this.codecReady && !existsSync4(this.filePath)) {
854
+ this.flushSync();
855
+ return;
856
+ }
857
+ this.flushTimer = setTimeout(() => {
858
+ this.flushTimer = null;
859
+ void this.flush().catch((err) => {
860
+ console.error("[session-persist] batch flush failed:", err);
861
+ });
862
+ }, 200);
863
+ this.flushTimer.unref?.();
864
+ }
865
+ }
866
+ /**
867
+ * Flush barrier: cancel the timer, drain the pending batch into one
868
+ * checksummed zstd frame append + datasync. Concurrent callers share the
869
+ * in-flight drain promise; a drain that arrives while one is already in
870
+ * flight still drains whatever queued in the meantime (the awaited promise
871
+ * only covers the batch it started with).
872
+ *
873
+ * On write failure the batch is put back at the head of the buffer so the
874
+ * next flush / shutdown drain retries it — a failed append must not
875
+ * permanently drop queued lines (ENOSPC etc.).
876
+ */
877
+ async flush() {
878
+ if (this.flushTimer !== null) {
879
+ clearTimeout(this.flushTimer);
880
+ this.flushTimer = null;
881
+ }
882
+ if (this.flushPromise !== null) return this.flushPromise;
883
+ this.flushPromise = this.drainPending().finally(() => {
884
+ this.flushPromise = null;
885
+ });
886
+ return this.flushPromise;
887
+ }
888
+ async drainPending() {
889
+ while (this.pendingBuffer.length > 0) {
890
+ const text = this.pendingBuffer;
891
+ this.pendingBuffer = "";
892
+ try {
893
+ await this.writeBatch(text);
894
+ } catch (err) {
895
+ this.pendingBuffer = text + this.pendingBuffer;
896
+ throw err;
897
+ }
898
+ }
899
+ }
900
+ /** Synchronous flush variant for the sync compaction paths. */
901
+ flushSync() {
902
+ if (this.flushTimer !== null) {
903
+ clearTimeout(this.flushTimer);
904
+ this.flushTimer = null;
905
+ }
906
+ const text = this.pendingBuffer;
907
+ this.pendingBuffer = "";
908
+ if (text.length > 0) {
909
+ this.ensureCodecFormat();
910
+ const frame = encodeBatch(text);
911
+ if (frame.length > 0) appendFileSync2(this.filePath, frame, { mode: 384 });
912
+ }
913
+ }
914
+ /**
915
+ * Merge the on-disk transcript text with lines still queued in the batch —
916
+ * in-process readers must see un-flushed lines (append followed by load
917
+ * without a flush is valid).
918
+ */
919
+ mergePending(onDiskText) {
920
+ return this.pendingBuffer.length > 0 ? onDiskText + this.pendingBuffer : onDiskText;
921
+ }
922
+ async writeBatch(text) {
923
+ this.ensureCodecFormat();
924
+ const frame = encodeBatch(text);
925
+ if (frame.length === 0) return;
926
+ await appendFile2(this.filePath, frame, { mode: 384 });
927
+ await this.fdatasyncQuiet();
928
+ }
929
+ /**
930
+ * One-time migration: on the first write, a legacy plain-text transcript is
931
+ * backed up and transcoded to a single zstd frame so subsequent appends can
932
+ * stay frame-aligned. New files and already-compressed files are untouched.
933
+ */
934
+ ensureCodecFormat() {
935
+ if (this.codecReady) return;
936
+ this.codecReady = true;
937
+ if (!existsSync4(this.filePath)) return;
938
+ const head = readFileSync3(this.filePath);
939
+ if (isZstdFrameStream(head)) return;
940
+ try {
941
+ copyFileSync(this.filePath, join4(this.backupDirProvider(), this.filePath.split("/").pop() + ".pre-zstd"));
942
+ } catch {
943
+ }
944
+ const frame = encodeBatch(head.toString("utf-8"));
945
+ writeFileAtomicSync(this.filePath, frame.length > 0 ? frame : Buffer.alloc(0));
946
+ }
947
+ /** Force appended data to disk without blocking the event loop. */
948
+ async fdatasyncQuiet() {
949
+ try {
950
+ const fh = await open(this.filePath, "a");
951
+ try {
952
+ await fh.datasync();
953
+ } finally {
954
+ await fh.close();
955
+ }
956
+ } catch {
957
+ }
958
+ }
959
+ };
960
+
961
+ // src/agent/session-metadata.ts
962
+ init_esm_shims();
963
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
964
+ var SessionMetadataStore = class {
965
+ constructor(metadataPath) {
966
+ this.metadataPath = metadataPath;
967
+ }
968
+ metadataPath;
969
+ /** In-memory cache: null = not loaded, undefined = no file on disk. */
970
+ cached = null;
971
+ /** Set when in-memory metadata diverges from disk. */
972
+ dirty = false;
973
+ load() {
974
+ if (this.cached !== null) return this.cached;
975
+ if (!existsSync5(this.metadataPath)) {
976
+ this.cached = void 0;
977
+ return void 0;
978
+ }
979
+ try {
980
+ this.cached = JSON.parse(readFileSync4(this.metadataPath, "utf-8"));
981
+ } catch {
982
+ this.cached = void 0;
983
+ }
984
+ return this.cached;
985
+ }
986
+ /** Synchronously durable write (initMetadata / external writers). */
987
+ write(metadata) {
988
+ this.cached = metadata;
989
+ this.dirty = false;
990
+ writeFileAtomicSync(this.metadataPath, JSON.stringify(metadata, null, 2) + "\n");
991
+ }
992
+ /**
993
+ * Upsert fields in memory only. Merge semantics: sessionId/createdAt win
994
+ * from explicit arguments, updatedAt always advances, tokenUsage merges
995
+ * nested instead of replacing. Disk write rides the batch flush cadence.
996
+ */
997
+ update(patch, sessionId) {
998
+ const existing = this.load();
999
+ const merged = {
1000
+ compactEvents: existing?.compactEvents ?? [],
1001
+ ...existing,
1002
+ ...patch,
1003
+ // These must win over ...existing/...patch — place them last:
1004
+ // - sessionId is authoritative from the caller
1005
+ // - createdAt is set once at creation and preserved thereafter
1006
+ // - updatedAt always advances to now (the whole point of the field;
1007
+ // spreading ...existing after it would freeze it at creation time)
1008
+ sessionId,
1009
+ createdAt: existing?.createdAt ?? Date.now(),
1010
+ updatedAt: Date.now(),
1011
+ // Preserve nested objects by merging, not replacing
1012
+ tokenUsage: existing?.tokenUsage || patch.tokenUsage ? { prompt: 0, completion: 0, total: 0, ...existing?.tokenUsage, ...patch.tokenUsage } : void 0
1013
+ };
1014
+ this.cached = merged;
1015
+ this.dirty = true;
1016
+ }
1017
+ /** Persist in-memory metadata when dirty (batch-flush cadence). */
1018
+ flush() {
1019
+ if (!this.dirty) return;
1020
+ try {
1021
+ writeFileAtomicSync(this.metadataPath, JSON.stringify(this.cached ?? {}, null, 2) + "\n");
1022
+ } catch (err) {
1023
+ this.dirty = true;
1024
+ throw err;
1025
+ }
1026
+ this.dirty = false;
1027
+ }
1028
+ };
1029
+
1030
+ // src/agent/session-persist.ts
1031
+ init_paths();
1032
+ function legacyMessageToOaiMessages(message) {
1033
+ if (typeof message.content === "string") {
1034
+ return [{ role: message.role, content: message.content }];
1035
+ }
1036
+ if (message.role === "user") {
1037
+ const text2 = message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
1038
+ const toolMessages = message.content.filter((block) => block.type === "tool_result").map((block) => ({ role: "tool", tool_call_id: block.tool_use_id, content: block.content }));
1039
+ return [
1040
+ ...text2 ? [{ role: "user", content: text2 }] : [],
1041
+ ...toolMessages
1042
+ ];
1043
+ }
1044
+ const text = message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
1045
+ const reasoning = message.content.filter((block) => block.type === "thinking").map((block) => block.thinking).join("");
1046
+ const toolCalls = message.content.filter((block) => block.type === "tool_use").map((block) => ({
1047
+ id: block.id,
1048
+ type: "function",
1049
+ function: { name: block.name, arguments: stableStringify(block.input) }
1050
+ }));
1051
+ const assistant = {
1052
+ role: "assistant",
1053
+ content: text || (toolCalls.length === 0 ? "" : null),
1054
+ ...reasoning ? { reasoning_content: reasoning } : {},
1055
+ ...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
1056
+ };
1057
+ return [assistant];
1058
+ }
1059
+ function getSessionDir(cwd) {
1060
+ return sessionsDir(cwd);
1061
+ }
1062
+ function ensureDir(dir) {
1063
+ if (!existsSync6(dir)) {
1064
+ mkdirSync2(dir, { recursive: true });
1065
+ }
1066
+ }
1067
+ var MAX_SESSION_MESSAGE_JSON_CHARS = 1e5;
1068
+ function truncateString(value, maxChars) {
1069
+ if (value.length <= maxChars) return value;
1070
+ const marker = `
1071
+ <session-message-truncated original_chars="${value.length}" kept_chars="${maxChars}" />`;
1072
+ const keep = Math.max(0, maxChars - marker.length);
1073
+ return value.slice(0, keep) + marker;
1074
+ }
1075
+ function capJsonValue(value, maxChars) {
1076
+ if (typeof value === "string") return truncateString(value, maxChars);
1077
+ if (Array.isArray(value)) return value.map((item) => capJsonValue(item, maxChars));
1078
+ if (value && typeof value === "object") {
1079
+ const capped = {};
1080
+ for (const [key, child] of Object.entries(value)) {
1081
+ capped[key] = capJsonValue(child, maxChars);
1082
+ }
1083
+ return capped;
1084
+ }
1085
+ return value;
1086
+ }
1087
+ function serializeSessionMessage(message, maxChars = MAX_SESSION_MESSAGE_JSON_CHARS) {
1088
+ return serializeSessionJsonValue(message, maxChars, () => ({
1089
+ role: message.role,
1090
+ content: truncateString(JSON.stringify(message), maxChars)
1091
+ }));
1092
+ }
1093
+ function serializeOaiSessionMessage(message, maxChars = MAX_SESSION_MESSAGE_JSON_CHARS) {
1094
+ const normalized = normalizeOaiMessage(message);
1095
+ return serializeSessionJsonValue(normalized, maxChars, () => ({
1096
+ role: normalized.role,
1097
+ content: truncateString(JSON.stringify(normalized), maxChars),
1098
+ ...normalized.role === "tool" ? { tool_call_id: normalized.tool_call_id } : {}
1099
+ }));
1100
+ }
1101
+ function serializeSessionJsonValue(message, maxChars, fallback) {
1102
+ let json = JSON.stringify(message);
1103
+ if (json.length <= maxChars) return json;
1104
+ const capped = capJsonValue(message, Math.max(1e3, Math.floor(maxChars * 0.8)));
1105
+ json = JSON.stringify(capped);
1106
+ if (json.length <= maxChars) return json;
1107
+ return JSON.stringify(fallback());
1108
+ }
1109
+ function isOaiMessage(value) {
1110
+ if (!value || typeof value !== "object") return false;
1111
+ const msg = value;
1112
+ if (msg.role === "system") return typeof msg.content === "string";
1113
+ if (msg.role === "user") return typeof msg.content === "string";
1114
+ if (msg.role === "assistant") return typeof msg.content === "string" || msg.content === null;
1115
+ if (msg.role === "tool") return typeof msg.tool_call_id === "string" && typeof msg.content === "string";
1116
+ return false;
1117
+ }
1118
+ function parseSessionLine(line) {
1119
+ const parsed = JSON.parse(line);
1120
+ if (parsed.type === "compact_start" || parsed.type === "compact_end" || parsed.type === "model_switch") {
1121
+ return null;
1122
+ }
1123
+ return parsed;
1124
+ }
1125
+ function readHistoricalTranscript(filePath) {
1126
+ if (!existsSync6(filePath)) return [];
1127
+ let lines;
1128
+ try {
1129
+ lines = decodeTranscriptText(readFileSync5(filePath)).trim().split("\n").filter(Boolean);
1130
+ } catch {
1131
+ return [];
1132
+ }
1133
+ const messages = [];
1134
+ for (const line of verifyLines(lines).validLines) {
1135
+ try {
1136
+ const parsed = parseSessionLine(line);
1137
+ if (parsed && (parsed.role === "user" || parsed.role === "assistant") && typeof parsed.content === "string") {
1138
+ messages.push({ role: parsed.role, content: parsed.content });
1139
+ }
1140
+ } catch {
1141
+ }
1142
+ }
1143
+ return messages;
1144
+ }
1145
+ var SessionPersist = class _SessionPersist {
1146
+ filePath;
1147
+ metadataPath;
1148
+ frozenPath;
1149
+ sessionId;
1150
+ cwd;
1151
+ /** Write-behind batch writer (zstd frames + 200ms batching + torn recovery). */
1152
+ batchWriter;
1153
+ /** Metadata store (in-memory cache + batch-flush cadence). */
1154
+ metaStore;
1155
+ /** Public getter for testing file-path-dependent integrations. */
1156
+ getFilePath() {
1157
+ return this.filePath;
1158
+ }
1159
+ constructor(sessionId, cwd) {
1160
+ assertValidSessionId(sessionId);
1161
+ this.cwd = cwd;
1162
+ ensureDir(getSessionDir(cwd));
1163
+ this.sessionId = sessionId;
1164
+ this.filePath = join5(getSessionDir(cwd), `${sessionId}.jsonl`);
1165
+ this.metadataPath = join5(getSessionDir(cwd), `${sessionId}.meta.json`);
1166
+ this.frozenPath = join5(getSessionDir(cwd), `${sessionId}.frozen.json`);
1167
+ this.batchWriter = new SessionBatchWriter(this.filePath, () => this.getBackupDir());
1168
+ this.metaStore = new SessionMetadataStore(this.metadataPath);
1169
+ }
1170
+ getBackupDir() {
1171
+ const dir = join5(getSessionDir(this.cwd), this.sessionId, "backups");
1172
+ ensureDir(dir);
1173
+ return dir;
1174
+ }
1175
+ /** Append a single message to the session file (checksummed batch path). */
1176
+ async append(message) {
1177
+ const line = serializeSessionMessage(message) + "\n";
1178
+ this.batchWriter.enqueueLine(line);
1179
+ }
1180
+ /** Flush barrier: drain the write-behind batch into a zstd frame on disk. */
1181
+ async flushSessionBuffer() {
1182
+ await this.batchWriter.flush();
1183
+ this.metaStore.flush();
1184
+ }
1185
+ /** Read the transcript file as JSONL text (zstd frames or legacy passthrough). */
1186
+ readTranscriptText() {
1187
+ const onDisk = existsSync6(this.filePath) ? decodeTranscriptText(readFileSync5(this.filePath)) : "";
1188
+ return this.batchWriter.mergePending(onDisk);
1189
+ }
1190
+ /** Load all messages from the session file (with checksum validation) */
1191
+ load() {
1192
+ return this.loadWithChecksum();
1193
+ }
1194
+ /** Append an OpenAI-native message with checksum (queued into the batch).
1195
+ *
1196
+ * `flush: true` drains the batch after enqueue — used for durability-critical
1197
+ * records (assistant tool_calls / tool results / user turns) so a hard crash
1198
+ * cannot lose a completed tool result while its disk side effect survives.
1199
+ */
1200
+ async appendOaiWithChecksum(message, options) {
1201
+ const json = serializeOaiSessionMessage(message);
1202
+ const line = appendChecksum(json) + "\n";
1203
+ this.batchWriter.enqueueLine(line);
1204
+ if (options?.flush) await this.batchWriter.flush();
1205
+ }
1206
+ /**
1207
+ * Append a model-switch event to the session transcript.
1208
+ *
1209
+ * Written as a checksummed `type: 'model_switch'` line so it survives
1210
+ * checksum verification, but parseSessionLine skips it on replay (same
1211
+ * pattern as compact_start/compact_end) — it's an audit breadcrumb, not
1212
+ * part of the conversation history. Lets a session JSONL show exactly
1213
+ * when/what the model changed mid-session.
1214
+ */
1215
+ appendModelSwitch(event) {
1216
+ const line = appendChecksum(JSON.stringify({
1217
+ type: "model_switch",
1218
+ t: Date.now(),
1219
+ from: event.from,
1220
+ to: event.to,
1221
+ provider: event.provider
1222
+ })) + "\n";
1223
+ this.batchWriter.enqueueLine(line);
1224
+ }
1225
+ /** Load messages in OpenAI-native format, migrating legacy rows on read. */
1226
+ loadOai() {
1227
+ const content = this.readTranscriptText();
1228
+ const lines = content.trim().split("\n").filter(Boolean);
1229
+ const { validLines } = verifyLines(lines);
1230
+ const messages = [];
1231
+ for (const line of validLines) {
1232
+ try {
1233
+ const parsed = parseSessionLine(line);
1234
+ if (!parsed) continue;
1235
+ if (isOaiMessage(parsed)) {
1236
+ messages.push(parsed);
1237
+ } else {
1238
+ messages.push(...legacyMessageToOaiMessages(parsed).map((message) => JSON.parse(JSON.stringify(message))));
1239
+ }
1240
+ } catch {
1241
+ }
1242
+ }
1243
+ const normalized = normalizeOaiMessages(messages);
1244
+ const { messages: repaired, hadOrphans, strippedWriteTool } = this.repairOrphanToolCalls(normalized);
1245
+ if (hadOrphans) {
1246
+ repaired.unshift({
1247
+ role: "system",
1248
+ content: strippedWriteTool ? "<system-reminder>The previous session was interrupted mid-turn. A write/edit tool call from the last assistant message was stripped because its result was not recorded \u2014 but the interruption may have happened either before OR after the file was actually written, so the file may or may not already contain the intended changes. Do NOT blindly re-run the write. First verify the file's current state with read_file or grep, then only write what is still missing. This is host-process interruption recovery, NOT a tool malfunction \u2014 the write tools remain fully functional; keep using them normally instead of bash workarounds.</system-reminder>" : "<system-reminder>The previous session was interrupted mid-turn. Some tool calls from the last assistant message were stripped because their results were not recorded. Re-run any read-only/search steps you still need; verify state before assuming any side effects took hold.</system-reminder>"
1249
+ });
1250
+ }
1251
+ return repaired.map(normalizeOaiMessage);
1252
+ }
1253
+ /** Tools whose orphan recovery must be non-destructive: the file may already
1254
+ * hold the intended changes, so the model must verify before re-writing. */
1255
+ static WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
1256
+ "write_file",
1257
+ "edit_file",
1258
+ "hash_edit",
1259
+ "ast_edit",
1260
+ "apply_patch"
1261
+ ]);
1262
+ /** 压#7: Remove orphan tool_use/tool_result pairs left by corrupted/missing lines.
1263
+ *
1264
+ * Orphan tool_use entries occur when the stream delivered a complete
1265
+ * tool_calls block but the process was killed before the tool executed
1266
+ * (force-kill, power loss, or stream error after tool_use commit). The tool
1267
+ * was NEVER run — any files it would have created/modified do NOT exist.
1268
+ * Returns whether any orphans were stripped so the caller can warn the model
1269
+ * not to assume side effects from the removed tools, plus whether any stripped
1270
+ * orphan was a write/edit tool (so the warning can be non-destructive: the
1271
+ * file may already hold the change, verify before re-running). */
1272
+ repairOrphanToolCalls(messages) {
1273
+ const toolCallIds = /* @__PURE__ */ new Set();
1274
+ const toolResultIndices = /* @__PURE__ */ new Map();
1275
+ for (let i = 0; i < messages.length; i++) {
1276
+ const msg = messages[i];
1277
+ if (msg.role === "assistant" && msg.tool_calls) {
1278
+ for (const tc of msg.tool_calls) {
1279
+ if (tc.id) toolCallIds.add(tc.id);
1280
+ }
1281
+ }
1282
+ if (msg.role === "tool" && msg.tool_call_id) {
1283
+ toolResultIndices.set(msg.tool_call_id, i);
1284
+ }
1285
+ }
1286
+ const orphanResultIdx = /* @__PURE__ */ new Set();
1287
+ for (const [id, idx] of toolResultIndices) {
1288
+ if (!toolCallIds.has(id)) orphanResultIdx.add(idx);
1289
+ }
1290
+ const result = [];
1291
+ let hadOrphans = false;
1292
+ let strippedWriteTool = false;
1293
+ const noteStripped = (tc) => {
1294
+ if (_SessionPersist.WRITE_TOOL_NAMES.has(tc.function?.name ?? "")) strippedWriteTool = true;
1295
+ };
1296
+ for (let i = 0; i < messages.length; i++) {
1297
+ const msg = messages[i];
1298
+ if (orphanResultIdx.has(i)) {
1299
+ hadOrphans = true;
1300
+ continue;
1301
+ }
1302
+ if (msg.role === "assistant" && msg.tool_calls) {
1303
+ const valid = msg.tool_calls.filter((tc) => tc.id && toolResultIndices.has(tc.id));
1304
+ const orphaned = msg.tool_calls.filter((tc) => !(tc.id && toolResultIndices.has(tc.id)));
1305
+ if (valid.length === 0 && !msg.content) {
1306
+ hadOrphans = true;
1307
+ orphaned.forEach(noteStripped);
1308
+ continue;
1309
+ }
1310
+ if (valid.length !== msg.tool_calls.length) {
1311
+ hadOrphans = true;
1312
+ orphaned.forEach(noteStripped);
1313
+ result.push({ ...msg, tool_calls: valid });
1314
+ continue;
1315
+ }
1316
+ }
1317
+ result.push(msg);
1318
+ }
1319
+ return { messages: result, hadOrphans, strippedWriteTool };
1320
+ }
1321
+ /** Audit breadcrumb types: skipped on replay (parseSessionLine), preserved across rewrites. */
1322
+ static AUDIT_LINE_TYPES = /* @__PURE__ */ new Set(["compact_start", "compact_end", "model_switch"]);
1323
+ /**
1324
+ * Collect audit breadcrumb lines from the current file, re-checksummed for a
1325
+ * full rewrite. Rewrites (compact / compactOai / compactOaiAsync) regenerate
1326
+ * the file from in-memory messages only, and audit lines never enter memory
1327
+ * (parseSessionLine skips them on replay) — without this, the first rewrite
1328
+ * after appendModelSwitch silently destroys the audit trail.
1329
+ */
1330
+ collectAuditLines() {
1331
+ if (!existsSync6(this.filePath)) return [];
1332
+ try {
1333
+ const lines = this.readTranscriptText().trim().split("\n").filter(Boolean);
1334
+ const { validLines } = verifyLines(lines);
1335
+ const audit = [];
1336
+ for (const json of validLines) {
1337
+ try {
1338
+ const parsed = JSON.parse(json);
1339
+ if (typeof parsed.type === "string" && _SessionPersist.AUDIT_LINE_TYPES.has(parsed.type)) {
1340
+ audit.push(appendChecksum(json));
1341
+ }
1342
+ } catch {
1343
+ }
1344
+ }
1345
+ return audit;
1346
+ } catch {
1347
+ return [];
1348
+ }
1349
+ }
1350
+ /** Compact the session file with the given messages (with checksums) */
1351
+ compact(messages) {
1352
+ this.batchWriter.flushSync();
1353
+ const audit = this.collectAuditLines();
1354
+ const content = [...audit, ...messages.map((m) => appendChecksum(serializeSessionMessage(m)))].join("\n") + "\n";
1355
+ writeFileAtomicSync(this.filePath, encodeBatch(content));
1356
+ }
1357
+ /** Compact the session file with OAI-format messages */
1358
+ compactOai(messages) {
1359
+ this.batchWriter.flushSync();
1360
+ const audit = this.collectAuditLines();
1361
+ const content = [...audit, ...messages.map((m) => appendChecksum(serializeOaiSessionMessage(m)))].join("\n") + "\n";
1362
+ writeFileAtomicSync(this.filePath, encodeBatch(content));
1363
+ }
1364
+ /** Async atomic compaction — avoids blocking the agent loop on full rewrites (S13). */
1365
+ async compactOaiAsync(messages) {
1366
+ await this.flushSessionBuffer();
1367
+ const audit = this.collectAuditLines();
1368
+ const content = [...audit, ...messages.map((m) => appendChecksum(serializeOaiSessionMessage(m)))].join("\n") + "\n";
1369
+ await writeFileAtomicAsync(this.filePath, encodeBatch(content));
1370
+ }
1371
+ /** Delete the session file */
1372
+ delete() {
1373
+ try {
1374
+ unlinkSync2(this.filePath);
1375
+ } catch {
1376
+ }
1377
+ }
1378
+ /**
1379
+ * 带校验和的 append(入队批处理)
1380
+ */
1381
+ async appendWithChecksum(message) {
1382
+ const json = serializeSessionMessage(message);
1383
+ const line = appendChecksum(json) + "\n";
1384
+ this.batchWriter.enqueueLine(line);
1385
+ }
1386
+ /**
1387
+ * 带校验和的 load(向后兼容)
1388
+ */
1389
+ loadWithChecksum() {
1390
+ const content = this.readTranscriptText();
1391
+ const lines = content.trim().split("\n").filter(Boolean);
1392
+ const { validLines, invalidCount } = verifyLines(lines);
1393
+ if (invalidCount > 0) {
1394
+ }
1395
+ return validLines.map((line) => {
1396
+ try {
1397
+ const parsed = parseSessionLine(line);
1398
+ if (!parsed) return null;
1399
+ return parsed;
1400
+ } catch {
1401
+ return null;
1402
+ }
1403
+ }).filter(Boolean);
1404
+ }
1405
+ /**
1406
+ * 写入 compact 开始标记
1407
+ */
1408
+ /**
1409
+ * 写入 compact 结束标记
1410
+ */
1411
+ /**
1412
+ * 检测 incomplete compact
1413
+ * @returns 是否检测到 incomplete compact
1414
+ */
1415
+ /** Get the session file path */
1416
+ getPath() {
1417
+ return this.filePath;
1418
+ }
1419
+ writeMetadata(metadata) {
1420
+ this.metaStore.write(metadata);
1421
+ this.refreshListCacheEntry();
1422
+ }
1423
+ /** Upsert specific metadata fields without overwriting others */
1424
+ updateMetadata(patch) {
1425
+ this.metaStore.update(patch, this.sessionId);
1426
+ this.refreshListCacheEntry();
1427
+ }
1428
+ /**
1429
+ * 把本会话的最新 metadata 增量并入会话列表缓存(纯内存,无磁盘 I/O),
1430
+ * 而不是整表失效。此前每次 append 都 invalidateListCache——append 监听器
1431
+ * 每条消息都会走 updateMetadata,而 loadPrevHandoff 在每个 LLM round 都读
1432
+ * 列表,活跃会话期间缓存命中率恒为 0,每轮全量 readdirSync + 逐会话重读。
1433
+ * 增量 upsert 保持同样的新鲜度语义:新建/更新立即可见、updatedAt 排序即时
1434
+ * 重排;TTL 不动,外部进程的文件变化仍按原 60s 窗口被吸收。
1435
+ */
1436
+ refreshListCacheEntry() {
1437
+ const meta = this.metaStore.load();
1438
+ if (meta === void 0) return;
1439
+ _SessionPersist.upsertListCacheEntry(this.cwd, _SessionPersist.buildListEntry(this.sessionId, meta));
1440
+ }
1441
+ /** Initialize metadata for a new session if not already present */
1442
+ initMetadata(init) {
1443
+ if (existsSync6(this.metadataPath)) return;
1444
+ this.writeMetadata({
1445
+ sessionId: this.sessionId,
1446
+ createdAt: Date.now(),
1447
+ updatedAt: Date.now(),
1448
+ compactEvents: [],
1449
+ status: "active",
1450
+ turnCount: 0,
1451
+ toolCallCount: 0,
1452
+ tokenUsage: { prompt: 0, completion: 0, total: 0 },
1453
+ ...init
1454
+ });
1455
+ }
1456
+ loadMetadata() {
1457
+ return this.metaStore.load();
1458
+ }
1459
+ /**
1460
+ * 冻结前缀快照落盘(`<id>.frozen.json`)——resume 缓存继承的写侧。
1461
+ * 由 PromptEngine 的 onFrozenSnapshotCommit 钩子在每个 user 边界触发(低频)。
1462
+ */
1463
+ writeFrozenSnapshot(data) {
1464
+ writeFileAtomicSync(this.frozenPath, JSON.stringify(data));
1465
+ }
1466
+ /**
1467
+ * 读回冻结前缀快照(resume 缓存继承的读侧)。
1468
+ * 不存在 / 坏 JSON / 形状校验失败一律 undefined——调用方降级为全量重建。
1469
+ */
1470
+ readFrozenSnapshot() {
1471
+ if (!existsSync6(this.frozenPath)) return void 0;
1472
+ try {
1473
+ return parseFrozenSnapshotData(JSON.parse(readFileSync5(this.frozenPath, "utf-8")));
1474
+ } catch {
1475
+ return void 0;
1476
+ }
1477
+ }
1478
+ loadMemory() {
1479
+ return loadSessionMemory(getSessionDir(this.cwd), this.sessionId);
1480
+ }
1481
+ appendMemory(input) {
1482
+ return appendSessionMemory(getSessionDir(this.cwd), this.sessionId, input);
1483
+ }
1484
+ buildMemoryBlock() {
1485
+ return buildSessionMemoryBlock(this.loadMemory());
1486
+ }
1487
+ getSessionMemoryState() {
1488
+ const memory = this.loadMemory();
1489
+ if (memory.entries.length === 0) return void 0;
1490
+ const block = buildSessionMemoryBlock(memory);
1491
+ return {
1492
+ path: join5(getSessionDir(this.cwd), `${this.sessionId}.memory.json`),
1493
+ lastSummarizedRoundIndex: -1,
1494
+ lastUpdatedAt: memory.entries[memory.entries.length - 1]?.createdAt ?? Date.now(),
1495
+ digest: block.length > 200 ? block.slice(0, 197) + "..." : block,
1496
+ stale: false,
1497
+ tokenEstimate: block.length
1498
+ };
1499
+ }
1500
+ /** Create a claim store for the current session. */
1501
+ createClaimStore() {
1502
+ return new ContextClaimStore(getSessionDir(this.cwd), this.sessionId);
1503
+ }
1504
+ /** Load durable claims from the most recent previous main session.
1505
+ * listMainSessions 排除 worker-/带点伪 id(`<id>.claims`)并按 updatedAt 降序——
1506
+ * listSessions + sort().pop() 会选中伪条目(恒返回 [])或非最近会话。 */
1507
+ loadPreviousDurableClaims() {
1508
+ const previous = _SessionPersist.listMainSessions(this.cwd).find((s) => s.id !== this.sessionId);
1509
+ return previous ? ContextClaimStore.loadDurableClaims(getSessionDir(this.cwd), previous.id) : [];
1510
+ }
1511
+ /** Inject durable claims from previous session into a claim store with confidence decay.
1512
+ * A4: cross-session pollution gate — only inject claims that intersect with current
1513
+ * project files AND were created within 7 days. */
1514
+ injectDurableClaims(store, cwd) {
1515
+ const now = Date.now();
1516
+ const TTL_MS = 7 * 24 * 60 * 60 * 1e3;
1517
+ const durableClaims = this.loadPreviousDurableClaims();
1518
+ for (const claim of durableClaims) {
1519
+ if (now - claim.createdAt > TTL_MS) continue;
1520
+ if (cwd) {
1521
+ const fileEvidence = claim.evidence.filter((e) => e.path);
1522
+ if (fileEvidence.length > 0) {
1523
+ const hasRelevantFile = fileEvidence.some((e) => {
1524
+ const abs = resolve(cwd, e.path);
1525
+ const rel = relative(cwd, abs);
1526
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
1527
+ });
1528
+ if (!hasRelevantFile) continue;
1529
+ }
1530
+ }
1531
+ store.propose({
1532
+ kind: claim.kind,
1533
+ scope: claim.scope,
1534
+ text: claim.text,
1535
+ confidence: claim.confidence * 0.9,
1536
+ fitness: claim.fitness,
1537
+ source: { ...claim.source, eventId: `resume:${claim.id}` },
1538
+ evidence: claim.evidence,
1539
+ createdAt: Date.now(),
1540
+ tags: [...claim.tags, "resumed"]
1541
+ });
1542
+ }
1543
+ }
1544
+ /** Write structured handoff text for this session. */
1545
+ writeHandoff(text) {
1546
+ writeFileAtomicSync(this.getHandoffPath(), text);
1547
+ }
1548
+ /** 会话归档交接文档路径(`<id>.handoff.md`)——loadPrevHandoff 注入管线认的位置。 */
1549
+ getHandoffPath() {
1550
+ return join5(getSessionDir(this.cwd), `${this.sessionId}.handoff.md`);
1551
+ }
1552
+ /**
1553
+ * Load the most relevant previous session's handoff text.
1554
+ * Routes by domain if both sessions have a domain tag; otherwise falls back
1555
+ * to the most recently updated session. Returns null if none found.
1556
+ */
1557
+ static loadPrevHandoff(cwd, currentSessionId, currentDomain) {
1558
+ const sessions = _SessionPersist.listSessionsWithMetadata(cwd).filter((s) => s.id !== currentSessionId);
1559
+ if (sessions.length === 0) return null;
1560
+ let candidates = sessions;
1561
+ if (currentDomain) {
1562
+ const sameDomain = sessions.filter((s) => s.domain === currentDomain);
1563
+ if (sameDomain.length > 0) candidates = sameDomain;
1564
+ }
1565
+ const prev = candidates[0];
1566
+ if (!prev) return null;
1567
+ const handoffPath = join5(getSessionDir(cwd), `${prev.id}.handoff.md`);
1568
+ if (!existsSync6(handoffPath)) return null;
1569
+ try {
1570
+ return readFileSync5(handoffPath, "utf-8");
1571
+ } catch {
1572
+ return null;
1573
+ }
1574
+ }
1575
+ /** List all session files */
1576
+ static listSessions(cwd) {
1577
+ const dir = getSessionDir(cwd);
1578
+ ensureDir(dir);
1579
+ try {
1580
+ return readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => f.replace(".jsonl", ""));
1581
+ } catch {
1582
+ return [];
1583
+ }
1584
+ }
1585
+ /**
1586
+ * Cache for listSessionsWithMetadata — avoids re-reading hundreds of session
1587
+ * meta files on every user boundary when cross-session handoff is requested.
1588
+ * TTL: 60s. Writes upsert the session's own entry incrementally
1589
+ * (refreshListCacheEntry) so the hot append path never cold-rows the cache.
1590
+ * Keyed by cwd since sessions are per-project.
1591
+ */
1592
+ static _listCache = /* @__PURE__ */ new Map();
1593
+ static LIST_CACHE_TTL_MS = 6e4;
1594
+ static invalidateListCache() {
1595
+ _SessionPersist._listCache.clear();
1596
+ }
1597
+ /** 移除单会话的缓存条目(硬删链——文件删除后条目不得在重建中复活)。 */
1598
+ static removeListCacheEntry(id) {
1599
+ for (const cached of _SessionPersist._listCache.values()) {
1600
+ const idx = cached.data.findIndex((e) => e.id === id);
1601
+ if (idx >= 0) cached.data.splice(idx, 1);
1602
+ }
1603
+ }
1604
+ /** 统一的列表条目形状——全量构建与增量 upsert 共用,防两处漂移。 */
1605
+ static buildListEntry(id, meta) {
1606
+ return {
1607
+ id,
1608
+ sessionId: id,
1609
+ createdAt: meta?.createdAt ?? 0,
1610
+ updatedAt: meta?.updatedAt ?? 0,
1611
+ compactEvents: meta?.compactEvents ?? [],
1612
+ ...meta
1613
+ };
1614
+ }
1615
+ /**
1616
+ * 缓存命中时把单会话条目原位并入列表。不替换数组、不刷新 ts:
1617
+ * 命中路径的契约本来就是"同一 cwd 的多次 list 返回同一数组",写入原位生效
1618
+ * 即是该契约下的新鲜度语义;ts 不动则 TTL 仍按首次构建计时,外部进程的
1619
+ * 文件变化照旧在 60s 窗口内被吸收。
1620
+ */
1621
+ static upsertListCacheEntry(cwd, entry) {
1622
+ const cached = _SessionPersist._listCache.get(cwd);
1623
+ if (!cached) return;
1624
+ const idx = cached.data.findIndex((e) => e.id === entry.id);
1625
+ if (idx >= 0) cached.data[idx] = { ...cached.data[idx], ...entry };
1626
+ else cached.data.push(entry);
1627
+ cached.data.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
1628
+ }
1629
+ /** List sessions with metadata, sorted by updatedAt descending (most recent first) */
1630
+ static listSessionsWithMetadata(cwd) {
1631
+ const now = Date.now();
1632
+ const cached = _SessionPersist._listCache.get(cwd);
1633
+ if (cached && now - cached.ts < _SessionPersist.LIST_CACHE_TTL_MS) {
1634
+ return cached.data;
1635
+ }
1636
+ const ids = _SessionPersist.listSessions(cwd);
1637
+ const results = [];
1638
+ for (const id of ids) {
1639
+ try {
1640
+ const p = new _SessionPersist(id, cwd);
1641
+ results.push(_SessionPersist.buildListEntry(id, p.loadMetadata()));
1642
+ } catch {
1643
+ }
1644
+ }
1645
+ results.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
1646
+ _SessionPersist._listCache.set(cwd, { ts: now, data: results });
1647
+ return results;
1648
+ }
1649
+ /**
1650
+ * Main user-facing sessions only: excludes worker sub-sessions (`worker-*`)
1651
+ * and non-transcript artifacts whose id carries a dotted suffix
1652
+ * (`<id>.claims`, `<id>.memory`, `<id>.snapshot`). Sorted by updatedAt desc.
1653
+ */
1654
+ static listMainSessions(cwd) {
1655
+ return _SessionPersist.listSessionsWithMetadata(cwd).filter((s) => !s.id.startsWith("worker-") && !s.id.includes("."));
1656
+ }
1657
+ /**
1658
+ * Resolve a user-supplied session reference (full id or short prefix) to a
1659
+ * single full session id. Exact match wins; otherwise prefix-match across
1660
+ * main sessions. Returns the resolved id, an ambiguous candidate list, or
1661
+ * null when nothing matches. The id = log id = resume id are the same value.
1662
+ */
1663
+ static resolveSessionId(cwd, ref) {
1664
+ const ref0 = ref.trim();
1665
+ if (!ref0) return null;
1666
+ const sessions = _SessionPersist.listMainSessions(cwd);
1667
+ const exact = sessions.find((s) => s.id === ref0);
1668
+ if (exact) return { id: exact.id };
1669
+ const matches = sessions.filter((s) => s.id.startsWith(ref0));
1670
+ if (matches.length === 1) return { id: matches[0].id };
1671
+ if (matches.length > 1) return { ambiguous: matches.map((s) => s.id) };
1672
+ return null;
1673
+ }
1674
+ /**
1675
+ * Render the session list for CLI `--list` and TUI `/sessions`. One row per
1676
+ * main session, numbered to match `listMainSessions` ordering so the index is
1677
+ * stable and aligned with prefix-based resume.
1678
+ */
1679
+ static formatSessionList(cwd, currentId) {
1680
+ const sessions = _SessionPersist.listMainSessions(cwd);
1681
+ if (sessions.length === 0) return "\u6CA1\u6709\u5386\u53F2\u4F1A\u8BDD\u3002";
1682
+ return sessions.map((s, i) => {
1683
+ const marker = s.id === currentId ? " \u2190 \u5F53\u524D" : "";
1684
+ const when = formatRelativeTime(s.updatedAt ?? 0);
1685
+ const turns = s.turnCount ?? 0;
1686
+ const model = s.model ?? "?";
1687
+ const domain = s.domain ? ` ${s.domain}` : "";
1688
+ const title = (s.title ?? "").replace(/\s+/g, " ").trim().slice(0, 50);
1689
+ return `${String(i + 1).padStart(2)}. ${s.id.slice(0, 8)} ${when} ${turns}\u8F6E ${model}${domain} ${title}${marker}`;
1690
+ }).join("\n");
1691
+ }
1692
+ };
1693
+ function formatExitSummary(meta, sessionId) {
1694
+ const turns = meta?.turnCount ?? 0;
1695
+ if (turns <= 0) return null;
1696
+ const short = sessionId.slice(0, 8);
1697
+ const title = (meta?.title ?? "").replace(/\s+/g, " ").trim().slice(0, 60);
1698
+ const head = `\u4F1A\u8BDD\u5DF2\u4FDD\u5B58: ${short} \xB7 ${turns}\u8F6E${title ? ` \xB7 \u201C${title}\u201D` : ""}`;
1699
+ return `\u2726 \u540E\u4F1A\u6709\u671F \u2014 \u661F\u8F68\u5DF2\u5B58\u6863
1700
+ ${head}
1701
+ \u6062\u590D: rivet --continue\uFF08\u6700\u8FD1\u4F1A\u8BDD\uFF09\u6216 rivet --resume ${short}
1702
+ \u7F13\u5B58\u6210\u672C\uFF1A\u5C3D\u5FEB\u56DE\u8FDE \u2248 \u7EE7\u627F\u51BB\u7ED3\u951A\u70B9\uFF08\u53EA\u8BFB\u7F13\u5B58\u4EF7\uFF09\uFF1B\u95F4\u9694\u8FC7\u4E45\u7F13\u5B58\u8FC7\u671F \u2192 \u5168\u91CF\u91CD\u5EFA\u4E00\u6B21\u524D\u7F00\uFF08\u957F\u4F1A\u8BDD\u6570\u5143\u7EA7\uFF09\u3002`;
1703
+ }
1704
+ function shouldAutoWriteHandoff(existingMtimeMs, sessionStartMs) {
1705
+ if (existingMtimeMs === null) return true;
1706
+ return existingMtimeMs <= sessionStartMs;
1707
+ }
1708
+ function formatRelativeTime(ts) {
1709
+ if (!ts) return "\u672A\u77E5";
1710
+ const diff = Date.now() - ts;
1711
+ if (diff < 0) return "\u521A\u521A";
1712
+ const sec = Math.floor(diff / 1e3);
1713
+ if (sec < 60) return "\u521A\u521A";
1714
+ const min = Math.floor(sec / 60);
1715
+ if (min < 60) return `${min}\u5206\u949F\u524D`;
1716
+ const hr = Math.floor(min / 60);
1717
+ if (hr < 24) return `${hr}\u5C0F\u65F6\u524D`;
1718
+ const day = Math.floor(hr / 24);
1719
+ if (day < 30) return `${day}\u5929\u524D`;
1720
+ const mon = Math.floor(day / 30);
1721
+ if (mon < 12) return `${mon}\u4E2A\u6708\u524D`;
1722
+ return `${Math.floor(mon / 12)}\u5E74\u524D`;
1723
+ }
1724
+ var MAX_SESSIONS = 50;
1725
+ function evictOldSessions(keepSessionId, cwd) {
1726
+ return evictOldSessionsInternal(getSessionDir(cwd), keepSessionId, MAX_SESSIONS);
1727
+ }
1728
+ function evictOldSessionsInternal(dir, keepSessionId, limit) {
1729
+ ensureDir(dir);
1730
+ let sessions;
1731
+ try {
1732
+ sessions = readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => f.replace(".jsonl", "")).filter((id) => !id.startsWith("worker-") && !id.includes("."));
1733
+ } catch {
1734
+ return [];
1735
+ }
1736
+ if (sessions.length <= limit) return [];
1737
+ const withMtime = sessions.map((id) => {
1738
+ let mtime = 0;
1739
+ try {
1740
+ mtime = statSync2(join5(dir, `${id}.jsonl`)).mtimeMs;
1741
+ } catch {
1742
+ }
1743
+ return { id, mtime };
1744
+ });
1745
+ withMtime.sort((a, b) => a.mtime - b.mtime);
1746
+ const toEvict = withMtime.filter(({ id }) => id !== keepSessionId).slice(0, sessions.length - limit).map(({ id }) => id);
1747
+ for (const id of toEvict) removeSessionFilesIn(dir, id);
1748
+ return toEvict;
1749
+ }
1750
+ function removeSessionFilesIn(dir, id) {
1751
+ try {
1752
+ unlinkSync2(join5(dir, `${id}.jsonl`));
1753
+ } catch {
1754
+ }
1755
+ try {
1756
+ unlinkSync2(join5(dir, `${id}.meta.json`));
1757
+ } catch {
1758
+ }
1759
+ try {
1760
+ unlinkSync2(join5(dir, `${id}.memory.json`));
1761
+ } catch {
1762
+ }
1763
+ try {
1764
+ unlinkSync2(join5(dir, `${id}.claims.jsonl`));
1765
+ } catch {
1766
+ }
1767
+ try {
1768
+ unlinkSync2(join5(dir, `${id}.frozen.json`));
1769
+ } catch {
1770
+ }
1771
+ try {
1772
+ rmSync(join5(dir, id), { recursive: true, force: true });
1773
+ } catch {
1774
+ }
1775
+ }
1776
+ function deleteSessionFiles(cwd, id) {
1777
+ removeSessionFilesIn(getSessionDir(cwd), id);
1778
+ SessionPersist.removeListCacheEntry(id);
1779
+ }
1780
+
1781
+ export {
1782
+ claimProposalFromAnchor,
1783
+ verifyAndExtract,
1784
+ decodeTranscriptText,
1785
+ getSessionDir,
1786
+ MAX_SESSION_MESSAGE_JSON_CHARS,
1787
+ serializeSessionMessage,
1788
+ serializeOaiSessionMessage,
1789
+ readHistoricalTranscript,
1790
+ SessionPersist,
1791
+ formatExitSummary,
1792
+ shouldAutoWriteHandoff,
1793
+ evictOldSessions,
1794
+ evictOldSessionsInternal,
1795
+ deleteSessionFiles
1796
+ };