@baique/pi-web-sky 0.1.55 → 0.1.57

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 (396) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +8 -4
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +2 -2
  5. package/.next/react-loadable-manifest.json +2 -2
  6. package/.next/required-server-files.js +3 -2
  7. package/.next/required-server-files.json +3 -2
  8. package/.next/routes-manifest.json +28 -0
  9. package/.next/server/app/_global-error/page.js +2 -2
  10. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/_global-error.html +1 -1
  12. package/.next/server/app/_global-error.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/server/app/_not-found/page.js +1 -1
  17. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/server/app/_not-found.html +1 -1
  19. package/.next/server/app/_not-found.rsc +4 -3
  20. package/.next/server/app/_not-found.segments/_full.segment.rsc +4 -3
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +4 -3
  22. package/.next/server/app/_not-found.segments/_tree.segment.rsc +3 -2
  23. package/.next/server/app/api/agent/[id]/bash-output/route.js +2 -2
  24. package/.next/server/app/api/agent/[id]/events/route.js +2 -2
  25. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  26. package/.next/server/app/api/agent/[id]/route.js +1 -1
  27. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  28. package/.next/server/app/api/agent/new/route.js +1 -1
  29. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  30. package/.next/server/app/api/agent/running/route.js +1 -1
  31. package/.next/server/app/api/agent/running/route.js.nft.json +1 -1
  32. package/.next/server/app/api/app-update/route.js +1 -1
  33. package/.next/server/app/api/auth/all-providers/route.js +1 -1
  34. package/.next/server/app/api/auth/api-key/[provider]/route.js +1 -1
  35. package/.next/server/app/api/auth/login/[provider]/route.js +1 -1
  36. package/.next/server/app/api/auth/logout/[provider]/route.js +1 -1
  37. package/.next/server/app/api/auth/providers/route.js +1 -1
  38. package/.next/server/app/api/board-assets/[...path]/route.js +2 -2
  39. package/.next/server/app/api/board-assets/route.js +2 -2
  40. package/.next/server/app/api/boards/[id]/add-session/route.js +1 -0
  41. package/.next/server/app/api/boards/[id]/add-session/route.js.nft.json +1 -0
  42. package/.next/server/app/api/boards/[id]/add-session/route_client-reference-manifest.js +1 -0
  43. package/.next/server/app/api/boards/[id]/reconcile/route.js +1 -1
  44. package/.next/server/app/api/boards/[id]/route.js +1 -1
  45. package/.next/server/app/api/boards/reorder/route.js +2 -2
  46. package/.next/server/app/api/boards/route.js +1 -1
  47. package/.next/server/app/api/cwd/browse/route.js +1 -1
  48. package/.next/server/app/api/cwd/validate/route.js +2 -2
  49. package/.next/server/app/api/default-cwd/route.js +2 -2
  50. package/.next/server/app/api/file-index/route.js +2 -2
  51. package/.next/server/app/api/files/[...path]/route.js +3 -3
  52. package/.next/server/app/api/git/diff/route.js +1 -1
  53. package/.next/server/app/api/git/status/route.js +1 -1
  54. package/.next/server/app/api/home/route.js +1 -1
  55. package/.next/server/app/api/mcp/route.js +2 -2
  56. package/.next/server/app/api/models/route.js +2 -2
  57. package/.next/server/app/api/models-config/catalog/route.js +1 -1
  58. package/.next/server/app/api/models-config/discover/route.js +1 -1
  59. package/.next/server/app/api/models-config/route.js +1 -1
  60. package/.next/server/app/api/models-config/test/route.js +1 -1
  61. package/.next/server/app/api/plugins/route.js +2 -2
  62. package/.next/server/app/api/project-trust/route.js +1 -1
  63. package/.next/server/app/api/project-trust/route.js.nft.json +1 -1
  64. package/.next/server/app/api/quota/route.js +1 -1
  65. package/.next/server/app/api/search/route.js +3 -3
  66. package/.next/server/app/api/sessions/[id]/auto-name/route.js +2 -2
  67. package/.next/server/app/api/sessions/[id]/auto-name/route.js.nft.json +1 -1
  68. package/.next/server/app/api/sessions/[id]/context/route.js +1 -1
  69. package/.next/server/app/api/sessions/[id]/context/route.js.nft.json +1 -1
  70. package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +2 -2
  71. package/.next/server/app/api/sessions/[id]/export/route.js +2 -2
  72. package/.next/server/app/api/sessions/[id]/minimap/route.js +1 -1
  73. package/.next/server/app/api/sessions/[id]/minimap/route.js.nft.json +1 -1
  74. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  75. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  76. package/.next/server/app/api/sessions/[id]/state/route.js +1 -1
  77. package/.next/server/app/api/sessions/[id]/state/route.js.nft.json +1 -1
  78. package/.next/server/app/api/sessions/[id]/todos/route.js +1 -1
  79. package/.next/server/app/api/sessions/[id]/todos/route.js.nft.json +1 -1
  80. package/.next/server/app/api/sessions/route.js +1 -1
  81. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  82. package/.next/server/app/api/sessions/summary/route.js +2 -2
  83. package/.next/server/app/api/skills/check/route.js +1 -1
  84. package/.next/server/app/api/skills/install/route.js +2 -2
  85. package/.next/server/app/api/skills/route.js +1 -1
  86. package/.next/server/app/api/skills/search/route.js +1 -1
  87. package/.next/server/app/api/skills/update/route.js +1 -1
  88. package/.next/server/app/api/subagents/[id]/route.js +1 -0
  89. package/.next/server/app/api/subagents/[id]/route.js.nft.json +1 -0
  90. package/.next/server/app/api/subagents/[id]/route_client-reference-manifest.js +1 -0
  91. package/.next/server/app/api/subagents/profiles/route.js +30 -0
  92. package/.next/server/app/api/subagents/profiles/route.js.nft.json +1 -0
  93. package/.next/server/app/api/subagents/profiles/route_client-reference-manifest.js +1 -0
  94. package/.next/server/app/api/subagents/settings/route.js +1 -0
  95. package/.next/server/app/api/subagents/settings/route.js.nft.json +1 -0
  96. package/.next/server/app/api/subagents/settings/route_client-reference-manifest.js +1 -0
  97. package/.next/server/app/api/task-card-questions/[id]/answer/route.js +2 -2
  98. package/.next/server/app/api/task-card-questions/route.js +2 -2
  99. package/.next/server/app/api/task-cards/[id]/route.js +1 -1
  100. package/.next/server/app/api/task-cards/route.js +1 -1
  101. package/.next/server/app/api/task-cards/unbind/route.js +3 -3
  102. package/.next/server/app/api/task-scheduler/status/route.js +1 -1
  103. package/.next/server/app/api/task-scheduler/status/route.js.nft.json +1 -1
  104. package/.next/server/app/api/tasks/[id]/assign-session/route.js +2 -12
  105. package/.next/server/app/api/tasks/[id]/board/route.js +1 -1
  106. package/.next/server/app/api/tasks/[id]/route.js +1 -1
  107. package/.next/server/app/api/tasks/[id]/route.js.nft.json +1 -1
  108. package/.next/server/app/api/tasks/reorder/route.js +2 -12
  109. package/.next/server/app/api/tasks/route.js +2 -12
  110. package/.next/server/app/api/terminal/[id]/events/route.js +2 -2
  111. package/.next/server/app/api/terminal/[id]/route.js +2 -2
  112. package/.next/server/app/api/terminal/route.js +2 -2
  113. package/.next/server/app/api/worktrees/route.js +2 -2
  114. package/.next/server/app/api/yjs-version/route.js +1 -1
  115. package/.next/server/app/favicon.ico/route.js +1 -1
  116. package/.next/server/app/index.html +1 -1
  117. package/.next/server/app/index.rsc +6 -5
  118. package/.next/server/app/index.segments/__PAGE__.segment.rsc +6 -5
  119. package/.next/server/app/index.segments/_full.segment.rsc +6 -5
  120. package/.next/server/app/index.segments/_tree.segment.rsc +3 -2
  121. package/.next/server/app/manifest.webmanifest/route.js +2 -2
  122. package/.next/server/app/page.js +175 -8
  123. package/.next/server/app/page_client-reference-manifest.js +1 -1
  124. package/.next/server/app-paths-manifest.json +8 -4
  125. package/.next/server/chunks/1937.js +14 -13
  126. package/.next/server/chunks/2524.js +2 -2
  127. package/.next/server/chunks/3379.js +1 -1
  128. package/.next/server/chunks/3500.js +1 -1
  129. package/.next/server/chunks/402.js +28 -7
  130. package/.next/server/chunks/426.js +14 -0
  131. package/.next/server/chunks/4367.js +1 -1
  132. package/.next/server/chunks/4457.js +3 -13
  133. package/.next/server/chunks/4980.js +1 -1
  134. package/.next/server/chunks/542.js +23 -4
  135. package/.next/server/chunks/5794.js +2 -2
  136. package/.next/server/chunks/5940.js +1 -1
  137. package/.next/server/chunks/6337.js +1 -1
  138. package/.next/server/chunks/6429.js +15 -2
  139. package/.next/server/chunks/6525.js +25 -4
  140. package/.next/server/chunks/6583.js +12 -11
  141. package/.next/server/chunks/7048.js +6 -0
  142. package/.next/server/instrumentation.js +1 -1
  143. package/.next/server/middleware-build-manifest.js +1 -1
  144. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  145. package/.next/server/middleware.js +3 -3
  146. package/.next/server/pages/404.html +1 -1
  147. package/.next/server/pages/500.html +1 -1
  148. package/.next/static/MPBMmQqmDG8zo8tM_bkDy/_buildManifest.js +1 -0
  149. package/.next/static/chunks/752.bafff304e6b00987.js +1 -0
  150. package/.next/static/chunks/8088.39866dc5f1d15868.js +1 -0
  151. package/.next/static/chunks/app/_global-error/page-ad6e695deb9de65c.js +1 -0
  152. package/.next/static/chunks/app/api/agent/[id]/bash-output/route-ad6e695deb9de65c.js +1 -0
  153. package/.next/static/chunks/app/api/agent/[id]/events/route-ad6e695deb9de65c.js +1 -0
  154. package/.next/static/chunks/app/api/agent/[id]/route-ad6e695deb9de65c.js +1 -0
  155. package/.next/static/chunks/app/api/agent/new/route-ad6e695deb9de65c.js +1 -0
  156. package/.next/static/chunks/app/api/agent/running/route-ad6e695deb9de65c.js +1 -0
  157. package/.next/static/chunks/app/api/app-update/route-ad6e695deb9de65c.js +1 -0
  158. package/.next/static/chunks/app/api/auth/all-providers/route-ad6e695deb9de65c.js +1 -0
  159. package/.next/static/chunks/app/api/auth/api-key/[provider]/route-ad6e695deb9de65c.js +1 -0
  160. package/.next/static/chunks/app/api/auth/login/[provider]/route-ad6e695deb9de65c.js +1 -0
  161. package/.next/static/chunks/app/api/auth/logout/[provider]/route-ad6e695deb9de65c.js +1 -0
  162. package/.next/static/chunks/app/api/auth/providers/route-ad6e695deb9de65c.js +1 -0
  163. package/.next/static/chunks/app/api/board-assets/[...path]/route-ad6e695deb9de65c.js +1 -0
  164. package/.next/static/chunks/app/api/board-assets/route-ad6e695deb9de65c.js +1 -0
  165. package/.next/static/chunks/app/api/boards/[id]/add-session/route-ad6e695deb9de65c.js +1 -0
  166. package/.next/static/chunks/app/api/boards/[id]/reconcile/route-ad6e695deb9de65c.js +1 -0
  167. package/.next/static/chunks/app/api/boards/[id]/route-ad6e695deb9de65c.js +1 -0
  168. package/.next/static/chunks/app/api/boards/reorder/route-ad6e695deb9de65c.js +1 -0
  169. package/.next/static/chunks/app/api/boards/route-ad6e695deb9de65c.js +1 -0
  170. package/.next/static/chunks/app/api/cwd/browse/route-ad6e695deb9de65c.js +1 -0
  171. package/.next/static/chunks/app/api/cwd/validate/route-ad6e695deb9de65c.js +1 -0
  172. package/.next/static/chunks/app/api/default-cwd/route-ad6e695deb9de65c.js +1 -0
  173. package/.next/static/chunks/app/api/file-index/route-ad6e695deb9de65c.js +1 -0
  174. package/.next/static/chunks/app/api/files/[...path]/route-ad6e695deb9de65c.js +1 -0
  175. package/.next/static/chunks/app/api/git/diff/route-ad6e695deb9de65c.js +1 -0
  176. package/.next/static/chunks/app/api/git/status/route-ad6e695deb9de65c.js +1 -0
  177. package/.next/static/chunks/app/api/home/route-ad6e695deb9de65c.js +1 -0
  178. package/.next/static/chunks/app/api/mcp/route-ad6e695deb9de65c.js +1 -0
  179. package/.next/static/chunks/app/api/models/route-ad6e695deb9de65c.js +1 -0
  180. package/.next/static/chunks/app/api/models-config/catalog/route-ad6e695deb9de65c.js +1 -0
  181. package/.next/static/chunks/app/api/models-config/discover/route-ad6e695deb9de65c.js +1 -0
  182. package/.next/static/chunks/app/api/models-config/route-ad6e695deb9de65c.js +1 -0
  183. package/.next/static/chunks/app/api/models-config/test/route-ad6e695deb9de65c.js +1 -0
  184. package/.next/static/chunks/app/api/plugins/route-ad6e695deb9de65c.js +1 -0
  185. package/.next/static/chunks/app/api/project-trust/route-ad6e695deb9de65c.js +1 -0
  186. package/.next/static/chunks/app/api/quota/route-ad6e695deb9de65c.js +1 -0
  187. package/.next/static/chunks/app/api/search/route-ad6e695deb9de65c.js +1 -0
  188. package/.next/static/chunks/app/api/sessions/[id]/auto-name/route-ad6e695deb9de65c.js +1 -0
  189. package/.next/static/chunks/app/api/sessions/[id]/context/route-ad6e695deb9de65c.js +1 -0
  190. package/.next/static/chunks/app/api/sessions/[id]/entries/[entryId]/thinking/route-ad6e695deb9de65c.js +1 -0
  191. package/.next/static/chunks/app/api/sessions/[id]/export/route-ad6e695deb9de65c.js +1 -0
  192. package/.next/static/chunks/app/api/sessions/[id]/minimap/route-ad6e695deb9de65c.js +1 -0
  193. package/.next/static/chunks/app/api/sessions/[id]/route-ad6e695deb9de65c.js +1 -0
  194. package/.next/static/chunks/app/api/sessions/[id]/state/route-ad6e695deb9de65c.js +1 -0
  195. package/.next/static/chunks/app/api/sessions/[id]/todos/route-ad6e695deb9de65c.js +1 -0
  196. package/.next/static/chunks/app/api/sessions/route-ad6e695deb9de65c.js +1 -0
  197. package/.next/static/chunks/app/api/sessions/summary/route-ad6e695deb9de65c.js +1 -0
  198. package/.next/static/chunks/app/api/skills/check/route-ad6e695deb9de65c.js +1 -0
  199. package/.next/static/chunks/app/api/skills/install/route-ad6e695deb9de65c.js +1 -0
  200. package/.next/static/chunks/app/api/skills/route-ad6e695deb9de65c.js +1 -0
  201. package/.next/static/chunks/app/api/skills/search/route-ad6e695deb9de65c.js +1 -0
  202. package/.next/static/chunks/app/api/skills/update/route-ad6e695deb9de65c.js +1 -0
  203. package/.next/static/chunks/app/api/subagents/[id]/route-ad6e695deb9de65c.js +1 -0
  204. package/.next/static/chunks/app/api/subagents/profiles/route-ad6e695deb9de65c.js +1 -0
  205. package/.next/static/chunks/app/api/subagents/settings/route-ad6e695deb9de65c.js +1 -0
  206. package/.next/static/chunks/app/api/task-card-questions/[id]/answer/route-ad6e695deb9de65c.js +1 -0
  207. package/.next/static/chunks/app/api/task-card-questions/route-ad6e695deb9de65c.js +1 -0
  208. package/.next/static/chunks/app/api/task-cards/[id]/route-ad6e695deb9de65c.js +1 -0
  209. package/.next/static/chunks/app/api/task-cards/route-ad6e695deb9de65c.js +1 -0
  210. package/.next/static/chunks/app/api/task-cards/unbind/route-ad6e695deb9de65c.js +1 -0
  211. package/.next/static/chunks/app/api/task-scheduler/status/route-ad6e695deb9de65c.js +1 -0
  212. package/.next/static/chunks/app/api/tasks/[id]/assign-session/route-ad6e695deb9de65c.js +1 -0
  213. package/.next/static/chunks/app/api/tasks/[id]/board/route-ad6e695deb9de65c.js +1 -0
  214. package/.next/static/chunks/app/api/tasks/[id]/route-ad6e695deb9de65c.js +1 -0
  215. package/.next/static/chunks/app/api/tasks/reorder/route-ad6e695deb9de65c.js +1 -0
  216. package/.next/static/chunks/app/api/tasks/route-ad6e695deb9de65c.js +1 -0
  217. package/.next/static/chunks/app/api/terminal/[id]/events/route-ad6e695deb9de65c.js +1 -0
  218. package/.next/static/chunks/app/api/terminal/[id]/route-ad6e695deb9de65c.js +1 -0
  219. package/.next/static/chunks/app/api/terminal/route-ad6e695deb9de65c.js +1 -0
  220. package/.next/static/chunks/app/api/worktrees/route-ad6e695deb9de65c.js +1 -0
  221. package/.next/static/chunks/app/api/yjs-version/route-ad6e695deb9de65c.js +1 -0
  222. package/.next/static/chunks/app/layout-fae2b226b3c78448.js +1 -0
  223. package/.next/static/chunks/app/manifest.webmanifest/route-ad6e695deb9de65c.js +1 -0
  224. package/.next/static/chunks/app/page-d5af3bd535589daf.js +256 -0
  225. package/.next/static/chunks/next/dist/client/components/builtin/app-error-ad6e695deb9de65c.js +1 -0
  226. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-ad6e695deb9de65c.js +1 -0
  227. package/.next/static/chunks/next/dist/client/components/builtin/not-found-ad6e695deb9de65c.js +1 -0
  228. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-ad6e695deb9de65c.js +1 -0
  229. package/.next/static/chunks/{webpack-b0effde51c1651b8.js → webpack-1ce35e1249cfd931.js} +1 -1
  230. package/.next/static/css/bb9361c7f832a89d.css +1 -0
  231. package/.next/static/css/c7d127e07b1a786c.css +1 -0
  232. package/.next/trace +8 -7
  233. package/.next/trace-build +1 -1
  234. package/.next/types/app/api/boards/[id]/add-session/route.ts +353 -0
  235. package/.next/types/app/api/subagents/[id]/route.ts +353 -0
  236. package/.next/types/app/api/subagents/profiles/route.ts +353 -0
  237. package/.next/types/app/api/subagents/settings/route.ts +353 -0
  238. package/.next/types/routes.d.ts +5 -1
  239. package/.next/types/validator.ts +36 -0
  240. package/lib/api-types.ts +10 -0
  241. package/lib/board-reconcile.test.mjs +46 -8
  242. package/lib/board-reconcile.ts +57 -1
  243. package/lib/board-store.test.mjs +2 -2
  244. package/lib/chat-lazy-load.test.mjs +0 -62
  245. package/lib/chat-lazy-load.ts +0 -13
  246. package/lib/chat-only.ts +23 -0
  247. package/lib/draft-stash.ts +4 -5
  248. package/lib/file-types.ts +16 -1
  249. package/lib/i18n/messages/en.ts +91 -0
  250. package/lib/i18n/messages/zh-CN.ts +91 -0
  251. package/lib/id.test.mjs +38 -0
  252. package/lib/id.ts +17 -0
  253. package/lib/ime-guard.test.mjs +52 -0
  254. package/lib/ime-guard.ts +54 -0
  255. package/lib/message-text.ts +24 -0
  256. package/lib/models-cache.ts +1 -1
  257. package/lib/pi-types.ts +19 -1
  258. package/lib/pin-store.ts +85 -0
  259. package/lib/powershell-settings.ts +106 -0
  260. package/lib/quoted-selection.ts +4 -0
  261. package/lib/rpc-manager.activity.test.mjs +223 -0
  262. package/lib/rpc-manager.test.mjs +22 -0
  263. package/lib/rpc-manager.title.test.mjs +152 -0
  264. package/lib/rpc-manager.ts +301 -28
  265. package/lib/session-activity.test.mjs +123 -0
  266. package/lib/session-activity.ts +77 -0
  267. package/lib/session-delete.test.mjs +45 -41
  268. package/lib/session-delete.ts +8 -34
  269. package/lib/session-family.test.mjs +49 -0
  270. package/lib/session-family.ts +80 -0
  271. package/lib/session-index-scanner.test.mjs +364 -2
  272. package/lib/session-index-scanner.ts +199 -55
  273. package/lib/session-reader.noscan.test.mjs +371 -0
  274. package/lib/session-reader.project.test.mjs +30 -52
  275. package/lib/session-reader.test.mjs +2 -65
  276. package/lib/session-reader.ts +177 -213
  277. package/lib/session-scanner.test.mjs +151 -38
  278. package/lib/session-scanner.ts +140 -61
  279. package/lib/session-search.ts +6 -5
  280. package/lib/session-time-group.test.mjs +38 -0
  281. package/lib/session-time-group.ts +55 -0
  282. package/lib/settings-navigation.ts +124 -0
  283. package/lib/sqlite-db.test.mjs +26 -0
  284. package/lib/sqlite-db.ts +10 -1
  285. package/lib/subagent-extension.test.mjs +424 -0
  286. package/lib/subagent-extension.ts +309 -0
  287. package/lib/subagent-input.test.mjs +63 -0
  288. package/lib/subagent-input.ts +71 -0
  289. package/lib/subagent-profile-precedence.ts +20 -0
  290. package/lib/subagent-prompt.test.mjs +64 -0
  291. package/lib/subagent-prompt.ts +31 -0
  292. package/lib/subagent-queue.test.mjs +35 -0
  293. package/lib/subagent-queue.ts +74 -0
  294. package/lib/subagent-runtime.test.mjs +150 -0
  295. package/lib/subagent-runtime.ts +629 -0
  296. package/lib/subagent-session-title.test.mjs +120 -0
  297. package/lib/subagent-settings.test.mjs +50 -0
  298. package/lib/subagent-settings.ts +93 -0
  299. package/lib/subagents.test.mjs +601 -0
  300. package/lib/subagents.ts +602 -0
  301. package/lib/task-scheduler.test.mjs +27 -0
  302. package/lib/task-scheduler.ts +8 -1
  303. package/lib/task-store.activity.test.mjs +141 -0
  304. package/lib/task-store.meta-row.test.mjs +25 -5
  305. package/lib/task-store.partial-row.test.mjs +198 -0
  306. package/lib/task-store.subtree.test.mjs +172 -0
  307. package/lib/task-store.test.mjs +46 -20
  308. package/lib/task-store.ts +344 -68
  309. package/lib/tool-presets.ts +4 -0
  310. package/lib/types.ts +19 -0
  311. package/lib/worktree-isolation.test.mjs +51 -0
  312. package/next.config.ts +3 -1
  313. package/package.json +1 -1
  314. package/.next/static/bsW3fla0-BlBHVMKtAM5I/_buildManifest.js +0 -1
  315. package/.next/static/chunks/752.0ad92735d5f38143.js +0 -1
  316. package/.next/static/chunks/8088.53d9cdaf15634d4b.js +0 -1
  317. package/.next/static/chunks/app/_global-error/page-2d5f6296c7e693c6.js +0 -1
  318. package/.next/static/chunks/app/api/agent/[id]/bash-output/route-2d5f6296c7e693c6.js +0 -1
  319. package/.next/static/chunks/app/api/agent/[id]/events/route-2d5f6296c7e693c6.js +0 -1
  320. package/.next/static/chunks/app/api/agent/[id]/route-2d5f6296c7e693c6.js +0 -1
  321. package/.next/static/chunks/app/api/agent/new/route-2d5f6296c7e693c6.js +0 -1
  322. package/.next/static/chunks/app/api/agent/running/route-2d5f6296c7e693c6.js +0 -1
  323. package/.next/static/chunks/app/api/app-update/route-2d5f6296c7e693c6.js +0 -1
  324. package/.next/static/chunks/app/api/auth/all-providers/route-2d5f6296c7e693c6.js +0 -1
  325. package/.next/static/chunks/app/api/auth/api-key/[provider]/route-2d5f6296c7e693c6.js +0 -1
  326. package/.next/static/chunks/app/api/auth/login/[provider]/route-2d5f6296c7e693c6.js +0 -1
  327. package/.next/static/chunks/app/api/auth/logout/[provider]/route-2d5f6296c7e693c6.js +0 -1
  328. package/.next/static/chunks/app/api/auth/providers/route-2d5f6296c7e693c6.js +0 -1
  329. package/.next/static/chunks/app/api/board-assets/[...path]/route-2d5f6296c7e693c6.js +0 -1
  330. package/.next/static/chunks/app/api/board-assets/route-2d5f6296c7e693c6.js +0 -1
  331. package/.next/static/chunks/app/api/boards/[id]/reconcile/route-2d5f6296c7e693c6.js +0 -1
  332. package/.next/static/chunks/app/api/boards/[id]/route-2d5f6296c7e693c6.js +0 -1
  333. package/.next/static/chunks/app/api/boards/reorder/route-2d5f6296c7e693c6.js +0 -1
  334. package/.next/static/chunks/app/api/boards/route-2d5f6296c7e693c6.js +0 -1
  335. package/.next/static/chunks/app/api/cwd/browse/route-2d5f6296c7e693c6.js +0 -1
  336. package/.next/static/chunks/app/api/cwd/validate/route-2d5f6296c7e693c6.js +0 -1
  337. package/.next/static/chunks/app/api/default-cwd/route-2d5f6296c7e693c6.js +0 -1
  338. package/.next/static/chunks/app/api/file-index/route-2d5f6296c7e693c6.js +0 -1
  339. package/.next/static/chunks/app/api/files/[...path]/route-2d5f6296c7e693c6.js +0 -1
  340. package/.next/static/chunks/app/api/git/diff/route-2d5f6296c7e693c6.js +0 -1
  341. package/.next/static/chunks/app/api/git/status/route-2d5f6296c7e693c6.js +0 -1
  342. package/.next/static/chunks/app/api/home/route-2d5f6296c7e693c6.js +0 -1
  343. package/.next/static/chunks/app/api/mcp/route-2d5f6296c7e693c6.js +0 -1
  344. package/.next/static/chunks/app/api/models/route-2d5f6296c7e693c6.js +0 -1
  345. package/.next/static/chunks/app/api/models-config/catalog/route-2d5f6296c7e693c6.js +0 -1
  346. package/.next/static/chunks/app/api/models-config/discover/route-2d5f6296c7e693c6.js +0 -1
  347. package/.next/static/chunks/app/api/models-config/route-2d5f6296c7e693c6.js +0 -1
  348. package/.next/static/chunks/app/api/models-config/test/route-2d5f6296c7e693c6.js +0 -1
  349. package/.next/static/chunks/app/api/plugins/route-2d5f6296c7e693c6.js +0 -1
  350. package/.next/static/chunks/app/api/project-trust/route-2d5f6296c7e693c6.js +0 -1
  351. package/.next/static/chunks/app/api/quota/route-2d5f6296c7e693c6.js +0 -1
  352. package/.next/static/chunks/app/api/search/route-2d5f6296c7e693c6.js +0 -1
  353. package/.next/static/chunks/app/api/sessions/[id]/auto-name/route-2d5f6296c7e693c6.js +0 -1
  354. package/.next/static/chunks/app/api/sessions/[id]/context/route-2d5f6296c7e693c6.js +0 -1
  355. package/.next/static/chunks/app/api/sessions/[id]/entries/[entryId]/thinking/route-2d5f6296c7e693c6.js +0 -1
  356. package/.next/static/chunks/app/api/sessions/[id]/export/route-2d5f6296c7e693c6.js +0 -1
  357. package/.next/static/chunks/app/api/sessions/[id]/minimap/route-2d5f6296c7e693c6.js +0 -1
  358. package/.next/static/chunks/app/api/sessions/[id]/route-2d5f6296c7e693c6.js +0 -1
  359. package/.next/static/chunks/app/api/sessions/[id]/state/route-2d5f6296c7e693c6.js +0 -1
  360. package/.next/static/chunks/app/api/sessions/[id]/todos/route-2d5f6296c7e693c6.js +0 -1
  361. package/.next/static/chunks/app/api/sessions/route-2d5f6296c7e693c6.js +0 -1
  362. package/.next/static/chunks/app/api/sessions/summary/route-2d5f6296c7e693c6.js +0 -1
  363. package/.next/static/chunks/app/api/skills/check/route-2d5f6296c7e693c6.js +0 -1
  364. package/.next/static/chunks/app/api/skills/install/route-2d5f6296c7e693c6.js +0 -1
  365. package/.next/static/chunks/app/api/skills/route-2d5f6296c7e693c6.js +0 -1
  366. package/.next/static/chunks/app/api/skills/search/route-2d5f6296c7e693c6.js +0 -1
  367. package/.next/static/chunks/app/api/skills/update/route-2d5f6296c7e693c6.js +0 -1
  368. package/.next/static/chunks/app/api/task-card-questions/[id]/answer/route-2d5f6296c7e693c6.js +0 -1
  369. package/.next/static/chunks/app/api/task-card-questions/route-2d5f6296c7e693c6.js +0 -1
  370. package/.next/static/chunks/app/api/task-cards/[id]/route-2d5f6296c7e693c6.js +0 -1
  371. package/.next/static/chunks/app/api/task-cards/route-2d5f6296c7e693c6.js +0 -1
  372. package/.next/static/chunks/app/api/task-cards/unbind/route-2d5f6296c7e693c6.js +0 -1
  373. package/.next/static/chunks/app/api/task-scheduler/status/route-2d5f6296c7e693c6.js +0 -1
  374. package/.next/static/chunks/app/api/tasks/[id]/assign-session/route-2d5f6296c7e693c6.js +0 -1
  375. package/.next/static/chunks/app/api/tasks/[id]/board/route-2d5f6296c7e693c6.js +0 -1
  376. package/.next/static/chunks/app/api/tasks/[id]/route-2d5f6296c7e693c6.js +0 -1
  377. package/.next/static/chunks/app/api/tasks/reorder/route-2d5f6296c7e693c6.js +0 -1
  378. package/.next/static/chunks/app/api/tasks/route-2d5f6296c7e693c6.js +0 -1
  379. package/.next/static/chunks/app/api/terminal/[id]/events/route-2d5f6296c7e693c6.js +0 -1
  380. package/.next/static/chunks/app/api/terminal/[id]/route-2d5f6296c7e693c6.js +0 -1
  381. package/.next/static/chunks/app/api/terminal/route-2d5f6296c7e693c6.js +0 -1
  382. package/.next/static/chunks/app/api/worktrees/route-2d5f6296c7e693c6.js +0 -1
  383. package/.next/static/chunks/app/api/yjs-version/route-2d5f6296c7e693c6.js +0 -1
  384. package/.next/static/chunks/app/layout-dace09825b2d3857.js +0 -1
  385. package/.next/static/chunks/app/manifest.webmanifest/route-2d5f6296c7e693c6.js +0 -1
  386. package/.next/static/chunks/app/page-64b9f4b3567a2f73.js +0 -89
  387. package/.next/static/chunks/next/dist/client/components/builtin/app-error-2d5f6296c7e693c6.js +0 -1
  388. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-2d5f6296c7e693c6.js +0 -1
  389. package/.next/static/chunks/next/dist/client/components/builtin/not-found-2d5f6296c7e693c6.js +0 -1
  390. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-2d5f6296c7e693c6.js +0 -1
  391. package/.next/static/css/fd25673802257262.css +0 -1
  392. package/lib/extension-command.test.mjs +0 -63
  393. package/lib/extension-command.ts +0 -81
  394. package/lib/subagent-widget.test.mjs +0 -87
  395. package/lib/subagent-widget.ts +0 -109
  396. /package/.next/static/{bsW3fla0-BlBHVMKtAM5I → MPBMmQqmDG8zo8tM_bkDy}/_ssgManifest.js +0 -0
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { chmodSync, mkdtempSync, mkdirSync, renameSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import test from "node:test";
@@ -8,7 +8,7 @@ import { DatabaseSync } from "node:sqlite";
8
8
 
9
9
  const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false });
10
10
  const { getDb, setDbForTesting } = await jiti.import("./sqlite-db.ts");
11
- const { runSessionIndexScan, ensureSessionIndexReady, resetSessionIndexScannerForTests } = await jiti.import("./session-index-scanner.ts");
11
+ const { runSessionIndexScan, ensureSessionIndexReady, resetSessionIndexScannerForTests, indexSessionFileNow } = await jiti.import("./session-index-scanner.ts");
12
12
 
13
13
  function freshDb() {
14
14
  setDbForTesting(new DatabaseSync(":memory:"));
@@ -117,6 +117,157 @@ test("runSessionIndexScan:first_message 历史欠账(NULL 且 mtime 未变
117
117
  }
118
118
  });
119
119
 
120
+ test("runSessionIndexScan:扫描回填 last_reply(存量行只读一次文件尾,无回复写 '')", async () => {
121
+ freshDb();
122
+ const { root, projDir } = makeRoot();
123
+ try {
124
+ // 存量行(升级前建的):last_reply 为 NULL → 本轮读一次文件尾回填
125
+ const withReply = join(projDir, "2026-01-01T00-00-00-000Z_reply.jsonl");
126
+ writeFileSync(withReply, sessionFileLines("reply", "/home/u/project/alpha") +
127
+ '{"type":"message","id":"a1","parentId":"u1","message":{"role":"assistant","content":[{"type":"text","text":"最后一条回复"}]}}\n');
128
+ const noReply = join(projDir, "2026-01-02T00-00-00-000Z_quiet.jsonl");
129
+ writeFileSync(noReply, sessionFileLines("quiet", "/home/u/project/alpha"));
130
+ const insert = getDb().prepare(
131
+ "INSERT INTO session_meta (session_id, task_id, updated, pinned, path, created, modified) VALUES (?, NULL, 1, 0, ?, 1, ?)",
132
+ );
133
+ insert.run("reply", withReply, statSync(withReply).mtime.getTime());
134
+ insert.run("quiet", noReply, statSync(noReply).mtime.getTime());
135
+
136
+ const summary = await runSessionIndexScan(root);
137
+ const reply = getDb().prepare("SELECT last_reply, modified FROM session_meta WHERE session_id='reply'").get();
138
+ const quiet = getDb().prepare("SELECT last_reply FROM session_meta WHERE session_id='quiet'").get();
139
+ assert.equal(reply.last_reply, "最后一条回复");
140
+ assert.equal(quiet.last_reply, "", "无回复 → ''(已回填,不再读文件)");
141
+ assert.equal(reply.modified, statSync(withReply).mtime.getTime(), "回填不碰 modified");
142
+ assert.ok(summary.updated >= 2, "回填写入计入 updated");
143
+
144
+ // 幂等:已回填(非 NULL)的行下一轮不再读文件尾。把文件换成「没有回复」的内容
145
+ // (mtime 随之变化 → 走 mtime 分支 + first_message 收敛),last_reply 必须保持原值。
146
+ writeFileSync(withReply, sessionFileLines("reply", "/home/u/project/alpha", "改过的首条"));
147
+ await runSessionIndexScan(root);
148
+ const after = getDb().prepare("SELECT last_reply, first_message FROM session_meta WHERE session_id='reply'").get();
149
+ assert.equal(after.last_reply, "最后一条回复", "已回填的值不被后续扫描覆盖");
150
+ assert.equal(after.first_message, "改过的首条", "同轮其他派生列照常收敛");
151
+ } finally {
152
+ rmSync(root, { recursive: true, force: true });
153
+ }
154
+ });
155
+
156
+ test("runSessionIndexScan:不可读文件不中止回填(EACCES 行保持 NULL,其余行照常回填)", async (t) => {
157
+ freshDb();
158
+ const { root, projDir } = makeRoot();
159
+ const openFailed = join(mkdtempSync(join(tmpdir(), "pi-sidx-unreadable-")), "2026-01-02T00-00-00-000Z_broken.jsonl");
160
+ try {
161
+ if (process.getuid?.() === 0) {
162
+ t.diagnostic("以 root 运行:文件权限不生效,跳过(无法构造 open 失败)");
163
+ return;
164
+ }
165
+ // aaa(可读):回填要成功;broken(stat 能过、open EACCES):保持 NULL,且不能让
166
+ // 整轮扫描抛出去(否则同一行每轮都被重新命中 → 它后面的行永远得不到回填)。
167
+ const okFile = join(projDir, "2026-01-01T00-00-00-000Z_aaa.jsonl");
168
+ writeFileSync(okFile, sessionFileLines("aaa", "/home/u/project/alpha") +
169
+ '{"type":"message","id":"a1","parentId":"u1","message":{"role":"assistant","content":[{"type":"text","text":"回填成功"}]}}\n');
170
+ writeFileSync(openFailed, '{"type":"session","version":3,"id":"broken","cwd":"/home/u/project/alpha"}\n');
171
+ chmodSync(openFailed, 0o000);
172
+ const insert = getDb().prepare(
173
+ "INSERT INTO session_meta (session_id, task_id, updated, pinned, path, created, modified) VALUES (?, NULL, 1, 0, ?, 1, ?)",
174
+ );
175
+ insert.run("aaa", okFile, statSync(okFile).mtime.getTime());
176
+ // broken 行:path 指向不可读文件;行不在磁盘快照里(所在目录不在扫描根下)→ 不受删除分支影响
177
+ insert.run("broken", openFailed, Date.now());
178
+
179
+ const summary = await runSessionIndexScan(root); // 不得抛
180
+ assert.equal(summary.scanned, 1, "只有 aaa 在扫描根内");
181
+ const rows = getDb()
182
+ .prepare("SELECT session_id, last_reply FROM session_meta ORDER BY session_id")
183
+ .all()
184
+ .map((r) => [r.session_id, r.last_reply]);
185
+ assert.deepEqual(rows, [["aaa", "回填成功"], ["broken", null]], "可读行回填、不可读行保持 NULL(未回填)");
186
+ } finally {
187
+ rmSync(openFailed.slice(0, openFailed.lastIndexOf("/")), { recursive: true, force: true });
188
+ rmSync(root, { recursive: true, force: true });
189
+ }
190
+ });
191
+
192
+ test("runSessionIndexScan:path 指向不存在的文件 → last_reply 保持 NULL(下一轮再试)", async () => {
193
+ freshDb();
194
+ const { root } = makeRoot();
195
+ try {
196
+ getDb()
197
+ .prepare("INSERT INTO session_meta (session_id, task_id, updated, pinned, path) VALUES ('ghost', NULL, ?, 0, ?)")
198
+ .run(Date.now(), join(root, "--p--", "2026-01-01T00-00-00-000Z_ghost.jsonl"));
199
+ const summary = await runSessionIndexScan(root);
200
+ assert.equal(summary.deleted, 0, "年轻行保护:不删");
201
+ assert.equal(
202
+ getDb().prepare("SELECT last_reply FROM session_meta WHERE session_id='ghost'").get().last_reply,
203
+ null,
204
+ "读不出文件尾 → 保持 NULL(三态里的「未回填」)",
205
+ );
206
+ } finally {
207
+ rmSync(root, { recursive: true, force: true });
208
+ }
209
+ });
210
+
211
+ test("runSessionIndexScan:modified 单调——事件链路刚写的「现在就活跃」不被更旧的 mtime 拉回", async () => {
212
+ freshDb();
213
+ const { root, projDir } = makeRoot();
214
+ try {
215
+ const file = join(projDir, "2026-01-01T00-00-00-000Z_act.jsonl");
216
+ writeFileSync(file, sessionFileLines("act", "/home/u/project/alpha"));
217
+ await runSessionIndexScan(root);
218
+ // 事件链路(agent_settled/agent_start)刚把 modified 推到「现在」(> 文件 mtime)。
219
+ // 列表读取会触发懒初始化扫描 → 这个窗口是可达路径,扫描器不能把它拉回去。
220
+ const eventTime = Date.now() + 60_000;
221
+ getDb().prepare("UPDATE session_meta SET modified = ?, first_message = 'hello' WHERE session_id='act'").run(eventTime);
222
+ // 让 mtime 与库里 modified 差(改文件但把 mtime 往早调,模拟「事件写入比 mtime 新」)
223
+ const past = new Date(Date.now() - 3_600_000);
224
+ utimesSync(file, past, past);
225
+
226
+ // 「mtime 没前移」等于「没有变化」:不能再进 mtime 分支(否则每 30s 多读一次 header
227
+ // + 记一次幻影 updated,幂等扫描不成立),所以 summary.updated 必须是 0。
228
+ const quiet = await runSessionIndexScan(root);
229
+ assert.equal(quiet.updated, 0, "库内 modified 更新时,更旧的 mtime 不算变化(无幻影 updated)");
230
+ const row = getDb().prepare("SELECT modified FROM session_meta WHERE session_id='act'").get();
231
+ assert.equal(row.modified, eventTime, "扫描不把 modified 往旧的方向拉(单调)");
232
+
233
+ // 反向:文件 mtime 更新(会话真的又活跃了)→ 正常前移
234
+ const future = new Date(eventTime + 3_600_000);
235
+ utimesSync(file, future, future);
236
+ const ahead = await runSessionIndexScan(root);
237
+ assert.equal(ahead.updated, 1, "mtime 前移照走更新分支");
238
+ assert.equal(
239
+ getDb().prepare("SELECT modified FROM session_meta WHERE session_id='act'").get().modified,
240
+ future.getTime(),
241
+ "更新的 mtime 照常写入",
242
+ );
243
+ } finally {
244
+ rmSync(root, { recursive: true, force: true });
245
+ }
246
+ });
247
+
248
+ test("runSessionIndexScan:path 迁移走 upsert(ON CONFLICT)时也不把事件写的 modified 拉回", async () => {
249
+ freshDb();
250
+ const { root, projDir } = makeRoot();
251
+ try {
252
+ // 旧行:path 指向早已不存在的旧位置(模拟文件被移动/重写),modified 由事件链路写成「现在」。
253
+ // 这一轮走 INSERT ... ON CONFLICT 分支修正 path——modified 必须单调,不能被更旧的 mtime 覆盖。
254
+ const movedFile = join(projDir, "2026-01-20T00-00-00-000Z_mv.jsonl");
255
+ writeFileSync(movedFile, sessionFileLines("mv", "/home/u/project/alpha", "移动后"));
256
+ const eventTime = Date.now() + 60_000;
257
+ getDb()
258
+ .prepare("INSERT INTO session_meta (session_id, task_id, updated, pinned, path, created, modified) VALUES ('mv', NULL, 1, 0, ?, 1, ?)")
259
+ .run(join(projDir, "2026-01-01T00-00-00-000Z_mv.jsonl"), eventTime);
260
+
261
+ const summary = await runSessionIndexScan(root);
262
+ assert.equal(summary.inserted, 1, "path 变化 → 走 upsert 修正");
263
+ const row = getDb().prepare("SELECT path, modified FROM session_meta WHERE session_id='mv'").get();
264
+ assert.equal(row.path, movedFile, "path 跟随最新文件");
265
+ assert.equal(row.modified, eventTime, "upsert 不把事件写的 modified 拉回旧 mtime(单调)");
266
+ } finally {
267
+ rmSync(root, { recursive: true, force: true });
268
+ }
269
+ });
270
+
120
271
  test("runSessionIndexScan:磁盘文件删除 → 删行", async () => {
121
272
  freshDb();
122
273
  const { root, projDir } = makeRoot();
@@ -127,6 +278,9 @@ test("runSessionIndexScan:磁盘文件删除 → 删行", async () => {
127
278
  assert.equal(getDb().prepare("SELECT COUNT(*) AS n FROM session_meta").get().n, 2);
128
279
 
129
280
  rmSync(join(projDir, "2026-01-02T00-00-00-000Z_bbb.jsonl"));
281
+ // 行「成年」后再删文件:新建 60s 内的行有删除保护(见下文「年轻行保护」用例),
282
+ // 这条用例只关心「文件确实被删 → 行被删」的收敛行为。
283
+ ageRow("bbb");
130
284
  const summary = await runSessionIndexScan(root);
131
285
  assert.equal(summary.deleted, 1);
132
286
  assert.equal(getDb().prepare("SELECT COUNT(*) AS n FROM session_meta").get().n, 1);
@@ -228,6 +382,7 @@ test("runSessionIndexScan:mtime 变化时重挂(header.parentSession 变)
228
382
  assert.equal(getDb().prepare("SELECT parent_id FROM session_meta WHERE session_id='child'").get().parent_id, "oldparent");
229
383
 
230
384
  // 删除 oldparent + 重挂 child 到 newparent(DELETE API 行为:header 重写 → mtime 变)
385
+ ageRow("oldparent"); // 同上:绕开新建 60s 内的删除保护,只验删除收敛
231
386
  rmSync(oldParent);
232
387
  await new Promise((r) => setTimeout(r, 10));
233
388
  writeFileSync(childFile, [
@@ -263,6 +418,213 @@ test("runSessionIndexScan:库有行 + path 文件存在(扫描快照外新
263
418
  }
264
419
  });
265
420
 
421
+ /** 把一行的时间戳推老(绕过新建 60s 内的删除保护,见「年轻行保护」用例)。 */
422
+ function ageRow(sessionId) {
423
+ getDb().prepare("UPDATE session_meta SET updated = ? WHERE session_id = ?").run(Date.now() - 120_000, sessionId);
424
+ }
425
+
426
+ /** 带 header.parentSession 的会话文件内容(本文件既有 header 写法 + 父路径)。 */
427
+ function sessionLinesWithParent(id, cwd, parentPath) {
428
+ return [
429
+ `{"type":"session","version":3,"id":"${id}","timestamp":"2026-01-01T00:00:00.000Z","cwd":"${cwd}","parentSession":"${parentPath}"}`,
430
+ '{"type":"message","id":"u1","parentId":null,"message":{"role":"user","content":"fork"}}',
431
+ ].join("\n") + "\n";
432
+ }
433
+
434
+ test("扫描收敛:父会话归属任务、子会话 task_id 为空 → 子会话继承父归属", async () => {
435
+ const dir = mkdtempSync(join(tmpdir(), "pi-sidx-affil-"));
436
+ const proj = join(dir, "--p--");
437
+ mkdirSync(proj, { recursive: true });
438
+ try {
439
+ const parentPath = join(proj, "2026-01-01T00-00-00-000Z_parent.jsonl");
440
+ const childPath = join(proj, "2026-01-01T00-01-00-000Z_child.jsonl");
441
+ writeFileSync(parentPath, sessionFileLines("parent", "/tmp/proj"));
442
+ writeFileSync(childPath, sessionLinesWithParent("child", "/tmp/proj", parentPath));
443
+ const db = new DatabaseSync(":memory:");
444
+ setDbForTesting(db);
445
+ db.prepare("INSERT INTO tasks (id, project_key, name, created, updated) VALUES ('T','/tmp/proj','t',1,1)").run();
446
+ db.prepare("INSERT INTO session_meta (session_id, task_id, updated, pinned, path) VALUES ('parent','T',1,0,?)").run(parentPath);
447
+ await runSessionIndexScan(dir);
448
+ assert.equal(db.prepare("SELECT task_id FROM session_meta WHERE session_id='child'").get().task_id, "T");
449
+ assert.equal(db.prepare("SELECT parent_id FROM session_meta WHERE session_id='child'").get().parent_id, "parent");
450
+ } finally {
451
+ rmSync(dir, { recursive: true, force: true });
452
+ }
453
+ });
454
+
455
+ test("扫描收敛:多层脏链(A 有归属、B/C 为空)一轮扫描全部收敛", async () => {
456
+ const dir = mkdtempSync(join(tmpdir(), "pi-sidx-affil2-"));
457
+ const proj = join(dir, "--p--");
458
+ mkdirSync(proj, { recursive: true });
459
+ try {
460
+ const aPath = join(proj, "2026-01-01T00-00-00-000Z_A.jsonl");
461
+ const bPath = join(proj, "2026-01-01T00-01-00-000Z_B.jsonl");
462
+ const cPath = join(proj, "2026-01-01T00-02-00-000Z_C.jsonl");
463
+ writeFileSync(aPath, sessionFileLines("A", "/tmp/proj"));
464
+ writeFileSync(bPath, sessionLinesWithParent("B", "/tmp/proj", aPath));
465
+ writeFileSync(cPath, sessionLinesWithParent("C", "/tmp/proj", bPath));
466
+ const db = new DatabaseSync(":memory:");
467
+ setDbForTesting(db);
468
+ const insert = db.prepare("INSERT INTO session_meta (session_id, task_id, updated, pinned, path) VALUES (?,?,1,0,?)");
469
+ insert.run("A", "T", aPath);
470
+ insert.run("B", null, bPath);
471
+ insert.run("C", null, cPath);
472
+ await runSessionIndexScan(dir);
473
+ // node:sqlite 返回 null-prototype 行对象 → 转成数组元组再断言(与 deepEqual 的严格原型比较无关)
474
+ const rows = db.prepare("SELECT session_id, task_id FROM session_meta ORDER BY session_id").all()
475
+ .map((r) => [r.session_id, r.task_id]);
476
+ assert.deepEqual(rows, [["A", "T"], ["B", "T"], ["C", "T"]]);
477
+ } finally {
478
+ rmSync(dir, { recursive: true, force: true });
479
+ }
480
+ });
481
+
482
+ test("扫描递归:forks/ 与 <session>/<run>/run-0/ 子目录里的会话同样入库,父子链正确", async () => {
483
+ const dir = mkdtempSync(join(tmpdir(), "pi-sidx-nested-"));
484
+ const proj = join(dir, "--p--");
485
+ const rootFile = join(proj, "2026-01-01T00-00-00-000Z_root.jsonl");
486
+ const forksDir = join(proj, "2026-01-01T00-00-00-000Z_root", "forks");
487
+ const runDir = join(proj, "2026-01-01T00-00-00-000Z_root", "run-1", "run-0");
488
+ try {
489
+ mkdirSync(forksDir, { recursive: true });
490
+ mkdirSync(runDir, { recursive: true });
491
+ writeFileSync(rootFile, sessionFileLines("root", "/tmp/proj"));
492
+ writeFileSync(join(forksDir, "2026-01-01T00-10-00-000Z_fork1.jsonl"), sessionLinesWithParent("fork1", "/tmp/proj", rootFile));
493
+ writeFileSync(join(runDir, "session.jsonl"), sessionLinesWithParent("run1", "/tmp/proj", rootFile));
494
+ writeFileSync(join(proj, "transcript.jsonl"), '{"type":"message","role":"user","content":"x"}\n');
495
+ const db = new DatabaseSync(":memory:");
496
+ setDbForTesting(db);
497
+
498
+ const summary = await runSessionIndexScan(dir);
499
+ const rows = db.prepare("SELECT session_id, parent_id, task_id FROM session_meta ORDER BY session_id").all()
500
+ .map((r) => [r.session_id, r.parent_id, r.task_id]);
501
+ assert.equal(summary.scanned, 3, "只认 3 个会话文件(transcript.jsonl 非会话被跳过)");
502
+ assert.deepEqual(rows, [
503
+ ["fork1", "root", null],
504
+ ["root", null, null],
505
+ ["run1", "root", null],
506
+ ], "子目录里的会话也入库,父子链按全量 path 反查");
507
+ assert.ok(rows.every((r) => r[2] === null), "磁盘有、库无的会话=临时会话(task_id NULL)");
508
+ } finally {
509
+ rmSync(dir, { recursive: true, force: true });
510
+ }
511
+ });
512
+
513
+ test("扫描:库行按 path 精确匹配(会话文件移到子目录 → path 跟随,不另建行也不留旧行)", async () => {
514
+ freshDb();
515
+ const { root, projDir } = makeRoot();
516
+ try {
517
+ const file = join(projDir, "2026-01-01T00-00-00-000Z_mv.jsonl");
518
+ writeFileSync(file, sessionFileLines("mv", "/home/u/project/alpha", "移动前"));
519
+ await runSessionIndexScan(root);
520
+ assert.equal(getDb().prepare("SELECT path FROM session_meta WHERE session_id='mv'").get().path, file);
521
+
522
+ // 同一会话文件被搬到子目录(旧 path 消失):path 是会话与文件的唯一绑定,
523
+ // 新文件用 header 权威 id 认回同一行 → 修正 path,不另建行、不把旧行留下。
524
+ const movedDir = join(projDir, "2026-01-01T00-00-00-000Z_mv", "forks");
525
+ mkdirSync(movedDir, { recursive: true });
526
+ const moved = join(movedDir, "2026-01-20T00-00-00-000Z_mv.jsonl");
527
+ renameSync(file, moved);
528
+
529
+ const summary = await runSessionIndexScan(root);
530
+ const rows = getDb().prepare("SELECT session_id, path FROM session_meta").all().map((r) => [r.session_id, r.path]);
531
+ assert.deepEqual(rows, [["mv", moved]], "只有一行,path 指向移动后的文件");
532
+ assert.equal(summary.deleted, 0, "库行仍在磁盘上(换了路径)→ 不删");
533
+ assert.equal(summary.inserted, 1, "库行 path 变 → 按 upsert 修正路径(计 inserted,与既有约定一致)");
534
+ } finally {
535
+ rmSync(root, { recursive: true, force: true });
536
+ }
537
+ });
538
+
539
+ test("扫描:新建 60s 内的行即使磁盘上没有文件也不删(subagent 惰性落盘窗口)", async () => {
540
+ freshDb();
541
+ const { root, projDir } = makeRoot();
542
+ try {
543
+ // persist/subagent 建行先于文件 flush:path 指向尚未落盘的文件。
544
+ // 此时删行会让会话在列表里闪断(再建行又丢归属),故只跳过、下一轮再判。
545
+ const file = join(projDir, "2026-01-01T00-00-00-000Z_sg.jsonl");
546
+ getDb().prepare("INSERT INTO session_meta (session_id, task_id, updated, pinned, path) VALUES ('sg', NULL, ?, 0, ?)").run(Date.now(), file);
547
+
548
+ const summary = await runSessionIndexScan(root);
549
+ assert.equal(summary.deleted, 0, "年轻行跳过删除");
550
+ assert.ok(getDb().prepare("SELECT session_id FROM session_meta WHERE session_id='sg'").get(), "行保留");
551
+ } finally {
552
+ rmSync(root, { recursive: true, force: true });
553
+ }
554
+ });
555
+
556
+ test("扫描:upsert 不刷新 updated(否则每轮都把自己的行变『年轻』,删除分支永不生效)", async () => {
557
+ freshDb();
558
+ const { root, projDir } = makeRoot();
559
+ try {
560
+ const file = join(projDir, "2026-01-01T00-00-00-000Z_keep.jsonl");
561
+ writeFileSync(file, sessionFileLines("keep", "/home/u/project/alpha"));
562
+ await runSessionIndexScan(root);
563
+ const baseline = getDb().prepare("SELECT updated FROM session_meta WHERE session_id='keep'").get().updated;
564
+
565
+ // ① 无变化的一轮:没有任何写入路径该碰 updated
566
+ await runSessionIndexScan(root);
567
+ assert.equal(getDb().prepare("SELECT updated FROM session_meta WHERE session_id='keep'").get().updated, baseline, "无变化轮不碰 updated");
568
+
569
+ // ② 走 ON CONFLICT 分支的一轮(会话文件被移动到子目录 → path 修正):updated 同样不变。
570
+ // 若后人把 `updated = excluded.updated` 加进冲突分支(让行每次都变「年轻」、外部删除
571
+ // 永不收敛),这条断言就会因 updated 被刷成新的 now 而失败(静默失败的护栏)。
572
+ await new Promise((r) => setTimeout(r, 10)); // 保证刷新后的 now 与 baseline 至少差 1ms
573
+ const movedDir = join(projDir, "2026-01-01T00-00-00-000Z_keep", "forks");
574
+ mkdirSync(movedDir, { recursive: true });
575
+ const moved = join(movedDir, "2026-01-30T00-00-00-000Z_keep.jsonl");
576
+ renameSync(file, moved);
577
+ const summary = await runSessionIndexScan(root);
578
+ const after = getDb().prepare("SELECT updated, path FROM session_meta WHERE session_id='keep'").get();
579
+ assert.equal(summary.inserted, 1, "path 修正走 upsert(ON CONFLICT 分支)");
580
+ assert.equal(after.path, moved, "path 跟随移动后的文件");
581
+ assert.equal(after.updated, baseline, "upsert 不刷新 updated");
582
+ } finally {
583
+ rmSync(root, { recursive: true, force: true });
584
+ }
585
+ });
586
+
587
+ test("扫描 upsert(ON CONFLICT):不带首条消息的一轮不擦库内已有 first_message", async () => {
588
+ freshDb();
589
+ const { root, projDir } = makeRoot();
590
+ try {
591
+ // 文件只有 header、没有 user 消息 → head.firstMessage = "" → upsert 传 NULL。
592
+ // 库行已有一条首条消息(历史收敛值):path 修正的一轮不得把它擦掉。
593
+ const file = join(projDir, "2026-01-01T00-00-00-000Z_keep.jsonl");
594
+ writeFileSync(file, `{"type":"session","version":3,"id":"keep","timestamp":"2026-01-01T00:00:00.000Z","cwd":"/home/u/project/alpha"}\n`);
595
+ getDb()
596
+ .prepare("INSERT INTO session_meta (session_id, task_id, updated, pinned, path, first_message, created, modified) VALUES ('keep', NULL, 1, 0, ?, '历史首条', 1, 1)")
597
+ .run(join(projDir, "2026-01-01T00-00-00-000Z_moved.jsonl"));
598
+
599
+ const summary = await runSessionIndexScan(root);
600
+ assert.equal(summary.inserted, 1, "path 变化 → 走 upsert(ON CONFLICT 分支)");
601
+ const row = getDb().prepare("SELECT path, first_message FROM session_meta WHERE session_id='keep'").get();
602
+ assert.equal(row.path, file, "path 照常修正");
603
+ assert.equal(row.first_message, "历史首条", "已有首条消息不被无消息文件的 upsert 擦成 NULL(COALESCE)");
604
+ } finally {
605
+ rmSync(root, { recursive: true, force: true });
606
+ }
607
+ });
608
+
609
+ test("indexSessionFileNow:不带首条消息的 upsert 不擦库内已有 first_message", async () => {
610
+ freshDb();
611
+ const { root, projDir } = makeRoot();
612
+ try {
613
+ const file = join(projDir, "2026-01-01T00-00-00-000Z_ifn.jsonl");
614
+ writeFileSync(file, `{"type":"session","version":3,"id":"ifn","timestamp":"2026-01-01T00:00:00.000Z","cwd":"/home/u/project/alpha"}\n`);
615
+ getDb()
616
+ .prepare("INSERT INTO session_meta (session_id, task_id, updated, pinned, path, first_message, created, modified) VALUES ('ifn', NULL, 1, 0, ?, '历史首条', 1, 1)")
617
+ .run(join(projDir, "2026-01-01T00-00-00-000Z_old.jsonl"));
618
+
619
+ await indexSessionFileNow(file, null, null);
620
+ const row = getDb().prepare("SELECT path, first_message FROM session_meta WHERE session_id='ifn'").get();
621
+ assert.equal(row.path, file, "path 修正到新文件");
622
+ assert.equal(row.first_message, "历史首条", "fork_branch 单文件索引不得擦掉已有首条消息");
623
+ } finally {
624
+ rmSync(root, { recursive: true, force: true });
625
+ }
626
+ });
627
+
266
628
  test("runSessionIndexScan:同 id 多历史文件 → 只认最新 mtime,path 指向最新", async () => {
267
629
  freshDb();
268
630
  const { root, projDir } = makeRoot();