@baique/pi-web-sky 0.1.39 → 0.1.41

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 (381) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +7 -12
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +3 -3
  5. package/.next/react-loadable-manifest.json +51 -51
  6. package/.next/required-server-files.js +1 -1
  7. package/.next/required-server-files.json +1 -1
  8. package/.next/routes-manifest.json +3 -45
  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 +3 -3
  20. package/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +3 -3
  22. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -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]/route.js +2 -2
  26. package/.next/server/app/api/agent/new/route.js +1 -1
  27. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  28. package/.next/server/app/api/agent/running/route.js +1 -1
  29. package/.next/server/app/api/app-update/route.js +1 -1
  30. package/.next/server/app/api/auth/all-providers/route.js +1 -1
  31. package/.next/server/app/api/auth/api-key/[provider]/route.js +1 -1
  32. package/.next/server/app/api/auth/login/[provider]/route.js +1 -1
  33. package/.next/server/app/api/auth/logout/[provider]/route.js +1 -1
  34. package/.next/server/app/api/auth/providers/route.js +1 -1
  35. package/.next/server/app/api/boards/[id]/reconcile/route.js +1 -0
  36. package/.next/server/app/api/boards/[id]/reconcile/route_client-reference-manifest.js +1 -0
  37. package/.next/server/app/api/boards/[id]/route.js +1 -1
  38. package/.next/server/app/api/boards/purge-orphans/route.js +1 -1
  39. package/.next/server/app/api/boards/reorder/route.js +1 -1
  40. package/.next/server/app/api/boards/route.js +1 -1
  41. package/.next/server/app/api/cwd/browse/route.js +1 -1
  42. package/.next/server/app/api/cwd/validate/route.js +2 -2
  43. package/.next/server/app/api/default-cwd/route.js +2 -2
  44. package/.next/server/app/api/file-index/route.js +2 -2
  45. package/.next/server/app/api/files/[...path]/route.js +3 -3
  46. package/.next/server/app/api/files/[...path]/route.js.nft.json +1 -1
  47. package/.next/server/app/api/git/diff/route.js +1 -1
  48. package/.next/server/app/api/git/status/route.js +1 -1
  49. package/.next/server/app/api/home/route.js +1 -1
  50. package/.next/server/app/api/mcp/route.js +1 -1
  51. package/.next/server/app/api/models/route.js +2 -2
  52. package/.next/server/app/api/models-config/catalog/route.js +1 -1
  53. package/.next/server/app/api/models-config/discover/route.js +1 -1
  54. package/.next/server/app/api/models-config/route.js +1 -1
  55. package/.next/server/app/api/models-config/test/route.js +1 -1
  56. package/.next/server/app/api/plugins/route.js +2 -2
  57. package/.next/server/app/api/project-trust/route.js +2 -2
  58. package/.next/server/app/api/quota/route.js +1 -1
  59. package/.next/server/app/api/search/route.js +2 -2
  60. package/.next/server/app/api/sessions/[id]/auto-name/route.js +3 -3
  61. package/.next/server/app/api/sessions/[id]/context/route.js +2 -2
  62. package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +2 -2
  63. package/.next/server/app/api/sessions/[id]/export/route.js +2 -2
  64. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  65. package/.next/server/app/api/sessions/[id]/state/route.js +2 -2
  66. package/.next/server/app/api/sessions/[id]/todos/route.js +2 -2
  67. package/.next/server/app/api/sessions/route.js +2 -2
  68. package/.next/server/app/api/skills/check/route.js +1 -1
  69. package/.next/server/app/api/skills/install/route.js +2 -2
  70. package/.next/server/app/api/skills/route.js +1 -1
  71. package/.next/server/app/api/skills/search/route.js +1 -1
  72. package/.next/server/app/api/skills/update/route.js +1 -1
  73. package/.next/server/app/api/task-card-questions/[id]/answer/route.js +1 -1
  74. package/.next/server/app/api/task-card-questions/route.js +1 -1
  75. package/.next/server/app/api/task-cards/[id]/route.js +1 -1
  76. package/.next/server/app/api/task-cards/route.js +1 -1
  77. package/.next/server/app/api/task-scheduler/status/route.js +1 -1
  78. package/.next/server/app/api/tasks/[id]/board/route.js +1 -1
  79. package/.next/server/app/api/tasks/[id]/route.js +1 -1
  80. package/.next/server/app/api/tasks/reorder/route.js +1 -1
  81. package/.next/server/app/api/tasks/route.js +1 -1
  82. package/.next/server/app/api/terminal/[id]/events/route.js +2 -2
  83. package/.next/server/app/api/terminal/[id]/route.js +2 -2
  84. package/.next/server/app/api/terminal/route.js +2 -2
  85. package/.next/server/app/api/worktrees/route.js +2 -2
  86. package/.next/server/app/favicon.ico/route.js +1 -1
  87. package/.next/server/app/index.html +1 -1
  88. package/.next/server/app/index.rsc +5 -5
  89. package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
  90. package/.next/server/app/index.segments/_full.segment.rsc +5 -5
  91. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  92. package/.next/server/app/manifest.webmanifest/route.js +2 -2
  93. package/.next/server/app/page.js +7 -7
  94. package/.next/server/app/page_client-reference-manifest.js +1 -1
  95. package/.next/server/app-paths-manifest.json +7 -12
  96. package/.next/server/chunks/1094.js +2 -2
  97. package/.next/server/chunks/1601.js +1 -1
  98. package/.next/server/chunks/1860.js +1 -1
  99. package/.next/server/chunks/2075.js +1 -1
  100. package/.next/server/chunks/2169.js +1 -1
  101. package/.next/server/chunks/2404.js +1 -1
  102. package/.next/server/chunks/2524.js +1 -0
  103. package/.next/server/chunks/3012.js +1 -1
  104. package/.next/server/chunks/{5760.js → 3379.js} +1 -1
  105. package/.next/server/chunks/3500.js +1 -1
  106. package/.next/server/chunks/4226.js +5 -2
  107. package/.next/server/chunks/4267.js +1 -1
  108. package/.next/server/chunks/4563.js +1 -1
  109. package/.next/server/chunks/5318.js +1 -1
  110. package/.next/server/chunks/542.js +3 -44
  111. package/.next/server/chunks/5794.js +1 -1
  112. package/.next/server/chunks/6052.js +1 -1
  113. package/.next/server/chunks/6253.js +1 -1
  114. package/.next/server/chunks/6288.js +1 -1
  115. package/.next/server/chunks/6366.js +1 -1
  116. package/.next/server/chunks/673.js +1 -1
  117. package/.next/server/chunks/6916.js +2 -2
  118. package/.next/server/chunks/7128.js +43 -0
  119. package/.next/server/chunks/7265.js +2 -2
  120. package/.next/server/chunks/7799.js +1 -1
  121. package/.next/server/chunks/8106.js +1 -1
  122. package/.next/server/chunks/8333.js +1 -1
  123. package/.next/server/chunks/8604.js +1 -1
  124. package/.next/server/chunks/861.js +1 -0
  125. package/.next/server/chunks/8713.js +1 -1
  126. package/.next/server/chunks/8782.js +2 -2
  127. package/.next/server/chunks/898.js +1 -1
  128. package/.next/server/chunks/922.js +2 -2
  129. package/.next/server/chunks/9280.js +6 -6
  130. package/.next/server/chunks/9510.js +1 -1
  131. package/.next/server/instrumentation.js +1 -1
  132. package/.next/server/instrumentation.js.nft.json +1 -1
  133. package/.next/server/middleware-build-manifest.js +1 -1
  134. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  135. package/.next/server/pages/404.html +1 -1
  136. package/.next/server/pages/500.html +1 -1
  137. package/.next/server/server-reference-manifest.json +1 -1
  138. package/.next/static/chunks/{103.23142a67b02246a8.js → 103.6c91a059aa01d92c.js} +2 -2
  139. package/.next/static/chunks/{1032.12b382a9096ab2a4.js → 1032.9e8f793d46f463f8.js} +1 -1
  140. package/.next/static/chunks/{1063.775c442bdbeef08b.js → 1063.470df8a037dc8409.js} +1 -1
  141. package/.next/static/chunks/1a258343.f4fc2c133917ded8.js +2 -0
  142. package/.next/static/chunks/{1ba63609.b2ae90f43319a29e.js → 1ba63609.569928c7234f832c.js} +1 -1
  143. package/.next/static/chunks/{2181.ba21239d3ac39f81.js → 2181.b8dda21843ef5e74.js} +1 -1
  144. package/.next/static/chunks/{2325.23caf22075e51aa5.js → 2325.23e538886331cf64.js} +1 -1
  145. package/.next/static/chunks/{2356.523277bb8c6bf72a.js → 2356.b7312589c345edec.js} +1 -1
  146. package/.next/static/chunks/{268.72794ee2773c2060.js → 268.a1d90aca268be18a.js} +1 -1
  147. package/.next/static/chunks/{2695.6f49435bae1e6f33.js → 2695.5624aa3aae540e87.js} +1 -1
  148. package/.next/static/chunks/{2703.495a65ed14b2f094.js → 2703.097bc88286733740.js} +1 -1
  149. package/.next/static/chunks/{2821.934bcf60fbdc28c6.js → 2821.d2392a93d192fe92.js} +1 -1
  150. package/.next/static/chunks/3308.2cd6f20e6c0f4e92.js +73 -0
  151. package/.next/static/chunks/{3336.a76f9485fe7cb762.js → 3336.9b1dc9ec3507d64f.js} +1 -1
  152. package/.next/static/chunks/3546.779d2c7646aa77cc.js +1 -0
  153. package/.next/static/chunks/{370.8e8f3cf8eb0e29dd.js → 370.cde2a84caa84054f.js} +1 -1
  154. package/.next/static/chunks/{3716.72bf4c9d7325debd.js → 3716.5bac496cea32ca8f.js} +1 -1
  155. package/.next/static/chunks/{3995.eca0f5bef1fc3ae5.js → 3995.cd112debcd64c610.js} +1 -1
  156. package/.next/static/chunks/{4004.74b9bcf5478b1663.js → 4004.a8f79ef62f32aa30.js} +2 -2
  157. package/.next/static/chunks/{4202.8440832386f023c7.js → 4202.96f5b1beb8b3e3f5.js} +1 -1
  158. package/.next/static/chunks/{4238.6f102dcbcf2ae339.js → 4238.28efbf9aa0c82d90.js} +1 -1
  159. package/.next/static/chunks/{4405.7a19458960a1058e.js → 4405.cda46a596a02aef7.js} +1 -1
  160. package/.next/static/chunks/54a60aa6.5e4421e7afde4de8.js +78 -0
  161. package/.next/static/chunks/{6121.eb9023b1cbc78ee2.js → 6121.5e650e714b402b6f.js} +1 -1
  162. package/.next/static/chunks/{6195.1153369f5f8b13a2.js → 6195.1b7f9dc127381bbc.js} +1 -1
  163. package/.next/static/chunks/6380.d148a8e2c701ad78.js +68 -0
  164. package/.next/static/chunks/6582.fb2c709abc2c5981.js +1 -0
  165. package/.next/static/chunks/6be7e44c.00d0163f409986f0.js +1 -0
  166. package/.next/static/chunks/{7331.2add3c4e125a2dba.js → 7331.4afa66d6353c8e07.js} +1 -1
  167. package/.next/static/chunks/752.a05a2904b96af4d0.js +1 -0
  168. package/.next/static/chunks/{8200.a36b9f8c983c6b9e.js → 8200.350d650649b977eb.js} +1 -1
  169. package/.next/static/chunks/{8885.cf60cac98bf8b70c.js → 8885.2c060cb45c85547b.js} +1 -1
  170. package/.next/static/chunks/{918.6a9bbe564e4a2d0f.js → 918.593eb466d35cb8ad.js} +1 -1
  171. package/.next/static/chunks/{9698.4d09a99b9d99f3e5.js → 9698.758755d0387e0e22.js} +2 -2
  172. package/.next/static/chunks/97.fb73c551e14b3612.js +2 -0
  173. package/.next/static/chunks/{993e5495.8797001fcfcf2889.js → 993e5495.12cdfbc3af5c79e9.js} +1 -1
  174. package/.next/static/chunks/{9974.9a0f79beca65e8c4.js → 9974.5ee982e074ad047e.js} +1 -1
  175. package/.next/static/chunks/app/_global-error/page-bb8f79f321e26750.js +1 -0
  176. package/.next/static/chunks/app/api/agent/[id]/bash-output/route-bb8f79f321e26750.js +1 -0
  177. package/.next/static/chunks/app/api/agent/[id]/events/route-bb8f79f321e26750.js +1 -0
  178. package/.next/static/chunks/app/api/agent/[id]/route-bb8f79f321e26750.js +1 -0
  179. package/.next/static/chunks/app/api/agent/new/route-bb8f79f321e26750.js +1 -0
  180. package/.next/static/chunks/app/api/agent/running/route-bb8f79f321e26750.js +1 -0
  181. package/.next/static/chunks/app/api/app-update/route-bb8f79f321e26750.js +1 -0
  182. package/.next/static/chunks/app/api/auth/all-providers/route-bb8f79f321e26750.js +1 -0
  183. package/.next/static/chunks/app/api/auth/api-key/[provider]/route-bb8f79f321e26750.js +1 -0
  184. package/.next/static/chunks/app/api/auth/login/[provider]/route-bb8f79f321e26750.js +1 -0
  185. package/.next/static/chunks/app/api/auth/logout/[provider]/route-bb8f79f321e26750.js +1 -0
  186. package/.next/static/chunks/app/api/auth/providers/route-bb8f79f321e26750.js +1 -0
  187. package/.next/static/chunks/app/api/boards/[id]/reconcile/route-bb8f79f321e26750.js +1 -0
  188. package/.next/static/chunks/app/api/boards/[id]/route-bb8f79f321e26750.js +1 -0
  189. package/.next/static/chunks/app/api/boards/purge-orphans/route-bb8f79f321e26750.js +1 -0
  190. package/.next/static/chunks/app/api/boards/reorder/route-bb8f79f321e26750.js +1 -0
  191. package/.next/static/chunks/app/api/boards/route-bb8f79f321e26750.js +1 -0
  192. package/.next/static/chunks/app/api/cwd/browse/route-bb8f79f321e26750.js +1 -0
  193. package/.next/static/chunks/app/api/cwd/validate/route-bb8f79f321e26750.js +1 -0
  194. package/.next/static/chunks/app/api/default-cwd/route-bb8f79f321e26750.js +1 -0
  195. package/.next/static/chunks/app/api/file-index/route-bb8f79f321e26750.js +1 -0
  196. package/.next/static/chunks/app/api/files/[...path]/route-bb8f79f321e26750.js +1 -0
  197. package/.next/static/chunks/app/api/git/diff/route-bb8f79f321e26750.js +1 -0
  198. package/.next/static/chunks/app/api/git/status/route-bb8f79f321e26750.js +1 -0
  199. package/.next/static/chunks/app/api/home/route-bb8f79f321e26750.js +1 -0
  200. package/.next/static/chunks/app/api/mcp/route-bb8f79f321e26750.js +1 -0
  201. package/.next/static/chunks/app/api/models/route-bb8f79f321e26750.js +1 -0
  202. package/.next/static/chunks/app/api/models-config/catalog/route-bb8f79f321e26750.js +1 -0
  203. package/.next/static/chunks/app/api/models-config/discover/route-bb8f79f321e26750.js +1 -0
  204. package/.next/static/chunks/app/api/models-config/route-bb8f79f321e26750.js +1 -0
  205. package/.next/static/chunks/app/api/models-config/test/route-bb8f79f321e26750.js +1 -0
  206. package/.next/static/chunks/app/api/plugins/route-bb8f79f321e26750.js +1 -0
  207. package/.next/static/chunks/app/api/project-trust/route-bb8f79f321e26750.js +1 -0
  208. package/.next/static/chunks/app/api/quota/route-bb8f79f321e26750.js +1 -0
  209. package/.next/static/chunks/app/api/search/route-bb8f79f321e26750.js +1 -0
  210. package/.next/static/chunks/app/api/sessions/[id]/auto-name/route-bb8f79f321e26750.js +1 -0
  211. package/.next/static/chunks/app/api/sessions/[id]/context/route-bb8f79f321e26750.js +1 -0
  212. package/.next/static/chunks/app/api/sessions/[id]/entries/[entryId]/thinking/route-bb8f79f321e26750.js +1 -0
  213. package/.next/static/chunks/app/api/sessions/[id]/export/route-bb8f79f321e26750.js +1 -0
  214. package/.next/static/chunks/app/api/sessions/[id]/route-bb8f79f321e26750.js +1 -0
  215. package/.next/static/chunks/app/api/sessions/[id]/state/route-bb8f79f321e26750.js +1 -0
  216. package/.next/static/chunks/app/api/sessions/[id]/todos/route-bb8f79f321e26750.js +1 -0
  217. package/.next/static/chunks/app/api/sessions/route-bb8f79f321e26750.js +1 -0
  218. package/.next/static/chunks/app/api/skills/check/route-bb8f79f321e26750.js +1 -0
  219. package/.next/static/chunks/app/api/skills/install/route-bb8f79f321e26750.js +1 -0
  220. package/.next/static/chunks/app/api/skills/route-bb8f79f321e26750.js +1 -0
  221. package/.next/static/chunks/app/api/skills/search/route-bb8f79f321e26750.js +1 -0
  222. package/.next/static/chunks/app/api/skills/update/route-bb8f79f321e26750.js +1 -0
  223. package/.next/static/chunks/app/api/task-card-questions/[id]/answer/route-bb8f79f321e26750.js +1 -0
  224. package/.next/static/chunks/app/api/task-card-questions/route-bb8f79f321e26750.js +1 -0
  225. package/.next/static/chunks/app/api/task-cards/[id]/route-bb8f79f321e26750.js +1 -0
  226. package/.next/static/chunks/app/api/task-cards/route-bb8f79f321e26750.js +1 -0
  227. package/.next/static/chunks/app/api/task-scheduler/status/route-bb8f79f321e26750.js +1 -0
  228. package/.next/static/chunks/app/api/tasks/[id]/board/route-bb8f79f321e26750.js +1 -0
  229. package/.next/static/chunks/app/api/tasks/[id]/route-bb8f79f321e26750.js +1 -0
  230. package/.next/static/chunks/app/api/tasks/reorder/route-bb8f79f321e26750.js +1 -0
  231. package/.next/static/chunks/app/api/tasks/route-bb8f79f321e26750.js +1 -0
  232. package/.next/static/chunks/app/api/terminal/[id]/events/route-bb8f79f321e26750.js +1 -0
  233. package/.next/static/chunks/app/api/terminal/[id]/route-bb8f79f321e26750.js +1 -0
  234. package/.next/static/chunks/app/api/terminal/route-bb8f79f321e26750.js +1 -0
  235. package/.next/static/chunks/app/api/worktrees/route-bb8f79f321e26750.js +1 -0
  236. package/.next/static/chunks/app/{layout-7761e430ed68b347.js → layout-2bec2662143a3d1b.js} +1 -1
  237. package/.next/static/chunks/app/manifest.webmanifest/route-bb8f79f321e26750.js +1 -0
  238. package/.next/static/chunks/app/page-e38c6cf3174fa143.js +89 -0
  239. package/.next/static/chunks/next/dist/client/components/builtin/app-error-bb8f79f321e26750.js +1 -0
  240. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-bb8f79f321e26750.js +1 -0
  241. package/.next/static/chunks/next/dist/client/components/builtin/not-found-bb8f79f321e26750.js +1 -0
  242. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-bb8f79f321e26750.js +1 -0
  243. package/.next/static/chunks/webpack-b4924d2a22ad3650.js +1 -0
  244. package/.next/static/css/0c7244f7214af81f.css +1 -0
  245. package/.next/static/css/12456255969ac8de.css +31 -0
  246. package/.next/trace +8 -49
  247. package/.next/trace-build +1 -1
  248. package/.next/types/app/api/boards/[id]/{reconcile-task-edges → reconcile}/route.ts +3 -3
  249. package/.next/types/routes.d.ts +2 -7
  250. package/.next/types/validator.ts +3 -48
  251. package/README.md +6 -9
  252. package/THIRD_PARTY_NOTICES.md +10 -82
  253. package/bin/pi-web-sky.js +3 -3
  254. package/lib/board-reconcile-scheduler.ts +56 -0
  255. package/lib/board-reconcile.test.mjs +252 -0
  256. package/lib/board-reconcile.ts +421 -0
  257. package/lib/board-store.test.mjs +26 -4
  258. package/lib/board-store.ts +44 -6
  259. package/lib/board-types.ts +7 -2
  260. package/lib/i18n/messages/en.ts +1 -1
  261. package/lib/i18n/messages/zh-CN.ts +1 -1
  262. package/lib/session-delete.ts +5 -0
  263. package/lib/sqlite-db.ts +13 -1
  264. package/lib/task-card-store.test.mjs +16 -1
  265. package/lib/task-card-store.ts +37 -1
  266. package/lib/task-scheduler.test.mjs +79 -0
  267. package/lib/task-scheduler.ts +195 -21
  268. package/lib/yjs-room-server.mjs +139 -0
  269. package/package.json +12 -9
  270. package/server.mjs +7 -7
  271. package/.next/server/app/api/boards/[id]/canvas/route.js +0 -1
  272. package/.next/server/app/api/boards/[id]/canvas/route_client-reference-manifest.js +0 -1
  273. package/.next/server/app/api/boards/[id]/edges/[eid]/route.js +0 -1
  274. package/.next/server/app/api/boards/[id]/edges/[eid]/route.js.nft.json +0 -1
  275. package/.next/server/app/api/boards/[id]/edges/[eid]/route_client-reference-manifest.js +0 -1
  276. package/.next/server/app/api/boards/[id]/edges/route.js +0 -1
  277. package/.next/server/app/api/boards/[id]/edges/route.js.nft.json +0 -1
  278. package/.next/server/app/api/boards/[id]/edges/route_client-reference-manifest.js +0 -1
  279. package/.next/server/app/api/boards/[id]/nodes/[nid]/route.js +0 -1
  280. package/.next/server/app/api/boards/[id]/nodes/[nid]/route.js.nft.json +0 -1
  281. package/.next/server/app/api/boards/[id]/nodes/[nid]/route_client-reference-manifest.js +0 -1
  282. package/.next/server/app/api/boards/[id]/nodes/route.js +0 -1
  283. package/.next/server/app/api/boards/[id]/nodes/route.js.nft.json +0 -1
  284. package/.next/server/app/api/boards/[id]/nodes/route_client-reference-manifest.js +0 -1
  285. package/.next/server/app/api/boards/[id]/reconcile-task-edges/route.js +0 -1
  286. package/.next/server/app/api/boards/[id]/reconcile-task-edges/route.js.nft.json +0 -1
  287. package/.next/server/app/api/boards/[id]/reconcile-task-edges/route_client-reference-manifest.js +0 -1
  288. package/.next/static/chunks/3308.857c554428b3294e.js +0 -73
  289. package/.next/static/chunks/333.bf0651c789b2eb33.js +0 -1
  290. package/.next/static/chunks/5315.dba7515ae6e139ae.js +0 -4
  291. package/.next/static/chunks/54a60aa6.0e43cde7298ebcb7.js +0 -78
  292. package/.next/static/chunks/7999.879d5a20e8641752.js +0 -259
  293. package/.next/static/chunks/app/_global-error/page-d6f52627eddf9119.js +0 -1
  294. package/.next/static/chunks/app/api/agent/[id]/bash-output/route-d6f52627eddf9119.js +0 -1
  295. package/.next/static/chunks/app/api/agent/[id]/events/route-d6f52627eddf9119.js +0 -1
  296. package/.next/static/chunks/app/api/agent/[id]/route-d6f52627eddf9119.js +0 -1
  297. package/.next/static/chunks/app/api/agent/new/route-d6f52627eddf9119.js +0 -1
  298. package/.next/static/chunks/app/api/agent/running/route-d6f52627eddf9119.js +0 -1
  299. package/.next/static/chunks/app/api/app-update/route-d6f52627eddf9119.js +0 -1
  300. package/.next/static/chunks/app/api/auth/all-providers/route-d6f52627eddf9119.js +0 -1
  301. package/.next/static/chunks/app/api/auth/api-key/[provider]/route-d6f52627eddf9119.js +0 -1
  302. package/.next/static/chunks/app/api/auth/login/[provider]/route-d6f52627eddf9119.js +0 -1
  303. package/.next/static/chunks/app/api/auth/logout/[provider]/route-d6f52627eddf9119.js +0 -1
  304. package/.next/static/chunks/app/api/auth/providers/route-d6f52627eddf9119.js +0 -1
  305. package/.next/static/chunks/app/api/boards/[id]/canvas/route-d6f52627eddf9119.js +0 -1
  306. package/.next/static/chunks/app/api/boards/[id]/edges/[eid]/route-d6f52627eddf9119.js +0 -1
  307. package/.next/static/chunks/app/api/boards/[id]/edges/route-d6f52627eddf9119.js +0 -1
  308. package/.next/static/chunks/app/api/boards/[id]/nodes/[nid]/route-d6f52627eddf9119.js +0 -1
  309. package/.next/static/chunks/app/api/boards/[id]/nodes/route-d6f52627eddf9119.js +0 -1
  310. package/.next/static/chunks/app/api/boards/[id]/reconcile-task-edges/route-d6f52627eddf9119.js +0 -1
  311. package/.next/static/chunks/app/api/boards/[id]/route-d6f52627eddf9119.js +0 -1
  312. package/.next/static/chunks/app/api/boards/purge-orphans/route-d6f52627eddf9119.js +0 -1
  313. package/.next/static/chunks/app/api/boards/reorder/route-d6f52627eddf9119.js +0 -1
  314. package/.next/static/chunks/app/api/boards/route-d6f52627eddf9119.js +0 -1
  315. package/.next/static/chunks/app/api/cwd/browse/route-d6f52627eddf9119.js +0 -1
  316. package/.next/static/chunks/app/api/cwd/validate/route-d6f52627eddf9119.js +0 -1
  317. package/.next/static/chunks/app/api/default-cwd/route-d6f52627eddf9119.js +0 -1
  318. package/.next/static/chunks/app/api/file-index/route-d6f52627eddf9119.js +0 -1
  319. package/.next/static/chunks/app/api/files/[...path]/route-d6f52627eddf9119.js +0 -1
  320. package/.next/static/chunks/app/api/git/diff/route-d6f52627eddf9119.js +0 -1
  321. package/.next/static/chunks/app/api/git/status/route-d6f52627eddf9119.js +0 -1
  322. package/.next/static/chunks/app/api/home/route-d6f52627eddf9119.js +0 -1
  323. package/.next/static/chunks/app/api/mcp/route-d6f52627eddf9119.js +0 -1
  324. package/.next/static/chunks/app/api/models/route-d6f52627eddf9119.js +0 -1
  325. package/.next/static/chunks/app/api/models-config/catalog/route-d6f52627eddf9119.js +0 -1
  326. package/.next/static/chunks/app/api/models-config/discover/route-d6f52627eddf9119.js +0 -1
  327. package/.next/static/chunks/app/api/models-config/route-d6f52627eddf9119.js +0 -1
  328. package/.next/static/chunks/app/api/models-config/test/route-d6f52627eddf9119.js +0 -1
  329. package/.next/static/chunks/app/api/plugins/route-d6f52627eddf9119.js +0 -1
  330. package/.next/static/chunks/app/api/project-trust/route-d6f52627eddf9119.js +0 -1
  331. package/.next/static/chunks/app/api/quota/route-d6f52627eddf9119.js +0 -1
  332. package/.next/static/chunks/app/api/search/route-d6f52627eddf9119.js +0 -1
  333. package/.next/static/chunks/app/api/sessions/[id]/auto-name/route-d6f52627eddf9119.js +0 -1
  334. package/.next/static/chunks/app/api/sessions/[id]/context/route-d6f52627eddf9119.js +0 -1
  335. package/.next/static/chunks/app/api/sessions/[id]/entries/[entryId]/thinking/route-d6f52627eddf9119.js +0 -1
  336. package/.next/static/chunks/app/api/sessions/[id]/export/route-d6f52627eddf9119.js +0 -1
  337. package/.next/static/chunks/app/api/sessions/[id]/route-d6f52627eddf9119.js +0 -1
  338. package/.next/static/chunks/app/api/sessions/[id]/state/route-d6f52627eddf9119.js +0 -1
  339. package/.next/static/chunks/app/api/sessions/[id]/todos/route-d6f52627eddf9119.js +0 -1
  340. package/.next/static/chunks/app/api/sessions/route-d6f52627eddf9119.js +0 -1
  341. package/.next/static/chunks/app/api/skills/check/route-d6f52627eddf9119.js +0 -1
  342. package/.next/static/chunks/app/api/skills/install/route-d6f52627eddf9119.js +0 -1
  343. package/.next/static/chunks/app/api/skills/route-d6f52627eddf9119.js +0 -1
  344. package/.next/static/chunks/app/api/skills/search/route-d6f52627eddf9119.js +0 -1
  345. package/.next/static/chunks/app/api/skills/update/route-d6f52627eddf9119.js +0 -1
  346. package/.next/static/chunks/app/api/task-card-questions/[id]/answer/route-d6f52627eddf9119.js +0 -1
  347. package/.next/static/chunks/app/api/task-card-questions/route-d6f52627eddf9119.js +0 -1
  348. package/.next/static/chunks/app/api/task-cards/[id]/route-d6f52627eddf9119.js +0 -1
  349. package/.next/static/chunks/app/api/task-cards/route-d6f52627eddf9119.js +0 -1
  350. package/.next/static/chunks/app/api/task-scheduler/status/route-d6f52627eddf9119.js +0 -1
  351. package/.next/static/chunks/app/api/tasks/[id]/board/route-d6f52627eddf9119.js +0 -1
  352. package/.next/static/chunks/app/api/tasks/[id]/route-d6f52627eddf9119.js +0 -1
  353. package/.next/static/chunks/app/api/tasks/reorder/route-d6f52627eddf9119.js +0 -1
  354. package/.next/static/chunks/app/api/tasks/route-d6f52627eddf9119.js +0 -1
  355. package/.next/static/chunks/app/api/terminal/[id]/events/route-d6f52627eddf9119.js +0 -1
  356. package/.next/static/chunks/app/api/terminal/[id]/route-d6f52627eddf9119.js +0 -1
  357. package/.next/static/chunks/app/api/terminal/route-d6f52627eddf9119.js +0 -1
  358. package/.next/static/chunks/app/api/worktrees/route-d6f52627eddf9119.js +0 -1
  359. package/.next/static/chunks/app/manifest.webmanifest/route-d6f52627eddf9119.js +0 -1
  360. package/.next/static/chunks/app/page-2c87c41e7f848f45.js +0 -89
  361. package/.next/static/chunks/e3e7ddb2.a35db430b3564999.js +0 -1
  362. package/.next/static/chunks/next/dist/client/components/builtin/app-error-d6f52627eddf9119.js +0 -1
  363. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-d6f52627eddf9119.js +0 -1
  364. package/.next/static/chunks/next/dist/client/components/builtin/not-found-d6f52627eddf9119.js +0 -1
  365. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-d6f52627eddf9119.js +0 -1
  366. package/.next/static/chunks/webpack-4bbf04891e910713.js +0 -1
  367. package/.next/static/css/80c818c5781fbadc.css +0 -4
  368. package/.next/static/css/d3a2b5077b52c727.css +0 -1
  369. package/.next/types/app/api/boards/[id]/canvas/route.ts +0 -353
  370. package/.next/types/app/api/boards/[id]/edges/[eid]/route.ts +0 -353
  371. package/.next/types/app/api/boards/[id]/edges/route.ts +0 -353
  372. package/.next/types/app/api/boards/[id]/nodes/[nid]/route.ts +0 -353
  373. package/.next/types/app/api/boards/[id]/nodes/route.ts +0 -353
  374. package/lib/board-search.test.mjs +0 -110
  375. package/lib/board-search.ts +0 -63
  376. package/lib/file-explorer-state.test.mjs +0 -63
  377. package/lib/file-explorer-state.ts +0 -36
  378. package/lib/sync-room-server.mjs +0 -137
  379. /package/.next/server/app/api/boards/[id]/{canvas → reconcile}/route.js.nft.json +0 -0
  380. /package/.next/static/{IzmOG04jJst1VpUvr_fr5 → 8V884w_cp5HEp_rRpjnCR}/_buildManifest.js +0 -0
  381. /package/.next/static/{IzmOG04jJst1VpUvr_fr5 → 8V884w_cp5HEp_rRpjnCR}/_ssgManifest.js +0 -0
@@ -43,6 +43,10 @@ export interface TaskCard {
43
43
  sessionId: string | null;
44
44
  /** 跨进程派发互斥 token:谁抢到谁执行(多实例共库防双会话) */
45
45
  dispatchToken: string | null;
46
+ /** 状态判定实例归属:派发/续会话时写入的实例 id;null=旧数据(任何实例可接管) */
47
+ owner: string | null;
48
+ /** 归属心跳(ms epoch):owner 每 ~10s 刷新一次;过期视 owner 已死可接管 */
49
+ heartbeat: number;
46
50
  created: number;
47
51
  updated: number;
48
52
  }
@@ -73,6 +77,8 @@ interface TaskCardRow {
73
77
  retryCount: number;
74
78
  sessionId: string | null;
75
79
  dispatchToken: string | null;
80
+ owner: string | null;
81
+ heartbeat: number;
76
82
  created: number;
77
83
  updated: number;
78
84
  }
@@ -106,7 +112,7 @@ function rowToCard(row: TaskCardRow): TaskCard {
106
112
  }
107
113
 
108
114
  const CARD_COLUMNS =
109
- "id, board_id AS boardId, project_key AS projectKey, number, name, description, ready_status AS readyStatus, exec_status AS execStatus, priority, due, attachments, cwd, use_worktree AS useWorktree, max_retries AS maxRetries, retry_count AS retryCount, session_id AS sessionId, dispatch_token AS dispatchToken, created, updated";
115
+ "id, board_id AS boardId, project_key AS projectKey, number, name, description, ready_status AS readyStatus, exec_status AS execStatus, priority, due, attachments, cwd, use_worktree AS useWorktree, max_retries AS maxRetries, retry_count AS retryCount, session_id AS sessionId, dispatch_token AS dispatchToken, owner, heartbeat, created, updated";
110
116
 
111
117
  function getCardRow(id: string): TaskCardRow | undefined {
112
118
  return getDb()
@@ -120,6 +126,17 @@ export function getCard(id: string): TaskCard | undefined {
120
126
  return row ? rowToCard(row) : undefined;
121
127
  }
122
128
 
129
+ /** 按 id 列表批量取卡(保持传入顺序;不存在 id 跳过)。可见卡状态查询用,单次 IN 查询。 */
130
+ export function listCardsByIds(ids: string[]): TaskCard[] {
131
+ if (ids.length === 0) return [];
132
+ const placeholders = ids.map(() => "?").join(",");
133
+ const rows = getDb()
134
+ .prepare(`SELECT ${CARD_COLUMNS} FROM task_cards WHERE id IN (${placeholders})`)
135
+ .all(...ids) as unknown as TaskCardRow[];
136
+ const byId = new Map(rows.map((r) => [r.id, rowToCard(r)]));
137
+ return ids.map((id) => byId.get(id)).filter((c): c is TaskCard => Boolean(c));
138
+ }
139
+
123
140
  /** 某看板的全部任务卡,按编号升序。 */
124
141
  export function listCards(boardId: string): TaskCard[] {
125
142
  const rows = getDb()
@@ -202,6 +219,8 @@ export function updateCard(
202
219
  maxRetries?: number;
203
220
  sessionId?: string | null;
204
221
  retryCount?: number;
222
+ owner?: string | null;
223
+ heartbeat?: number;
205
224
  },
206
225
  ): TaskCard | null {
207
226
  const db = getDb();
@@ -231,6 +250,8 @@ export function updateCard(
231
250
  if (patch.maxRetries !== undefined) bump("max_retries", patch.maxRetries);
232
251
  if (patch.sessionId !== undefined) bump("session_id", patch.sessionId);
233
252
  if (patch.retryCount !== undefined) bump("retry_count", patch.retryCount);
253
+ if (patch.owner !== undefined) bump("owner", patch.owner);
254
+ if (patch.heartbeat !== undefined) bump("heartbeat", patch.heartbeat);
234
255
  if (sets.length === 0) return rowToCard(existing);
235
256
 
236
257
  bump("updated", now());
@@ -428,6 +449,21 @@ export function clearDispatchToken(cardId: string, token: string): void {
428
449
  .run(cardId, now(), token);
429
450
  }
430
451
 
452
+ /**
453
+ * 归属心跳刷新:把本实例拥有且仍在活跃生命周期(running/review/waiting_reply)
454
+ * 的卡的 heartbeat 推到 now。多实例共库时,非 owner 靠它判断 owner 是否活着
455
+ * (心跳过期 = owner 已死 → 可接管判定),避免误翻他人正在跑的卡。
456
+ */
457
+ export function heartbeatOwnedCards(owner: string): number {
458
+ const res = getDb()
459
+ .prepare(
460
+ `UPDATE task_cards SET heartbeat = ?1
461
+ WHERE owner = ?2 AND exec_status IN ('running', 'review', 'waiting_reply')`,
462
+ )
463
+ .run(now(), owner);
464
+ return Number(res.changes);
465
+ }
466
+
431
467
  /** 调度器已派发且正在运行的任务卡数(全局并发闸门用)。 */
432
468
  export function countRunningDispatched(): number {
433
469
  const row = getDb()
@@ -0,0 +1,79 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import { createJiti } from "jiti";
5
+
6
+ const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false });
7
+ const { getDb, setDbForTesting } = await jiti.import("./sqlite-db.ts");
8
+ const { createCard, updateCard, getCard, heartbeatOwnedCards } = await jiti.import("./task-card-store.ts");
9
+ const { createBoard } = await jiti.import("./board-store.ts");
10
+ // 只 import 纯函数/归属守卫,不触发 rpc-manager(避免拉起整个 coding-agent SDK)
11
+ const { isCardHandledByInstance, OWNER_STALE_MS } = await jiti.import("./task-scheduler.ts");
12
+
13
+ function freshDb() {
14
+ setDbForTesting(new DatabaseSync(":memory:"));
15
+ }
16
+
17
+ test("迁移后 task_cards 含 owner/heartbeat 列", () => {
18
+ freshDb();
19
+ const b = createBoard("proj-a", "看板A");
20
+ const c = createCard({ boardId: b.id, projectKey: "proj-a", name: "卡" });
21
+ assert.equal(c.owner, null);
22
+ assert.equal(c.heartbeat, 0);
23
+ const cols = getDb().prepare("PRAGMA table_info(task_cards)").all().map((r) => r.name);
24
+ assert.ok(cols.includes("owner"));
25
+ assert.ok(cols.includes("heartbeat"));
26
+ });
27
+
28
+ test("updateCard 可写 owner/heartbeat", () => {
29
+ freshDb();
30
+ const b = createBoard("proj-a", "看板A");
31
+ const c = createCard({ boardId: b.id, projectKey: "proj-a", name: "卡" });
32
+ const ts = 12345;
33
+ const updated = updateCard(c.id, { execStatus: "running", owner: "inst-a", heartbeat: ts });
34
+ assert.equal(updated?.owner, "inst-a");
35
+ assert.equal(updated?.heartbeat, ts);
36
+ assert.equal(getCard(c.id)?.owner, "inst-a");
37
+ assert.equal(getCard(c.id)?.heartbeat, ts);
38
+ });
39
+
40
+ test("heartbeatOwnedCards 只刷本实例活跃生命周期(running/review/waiting_reply)卡", () => {
41
+ freshDb();
42
+ const b = createBoard("proj-a", "看板A");
43
+ const running = createCard({ boardId: b.id, projectKey: "proj-a", name: "跑" });
44
+ const review = createCard({ boardId: b.id, projectKey: "proj-a", name: "审" });
45
+ const waiting = createCard({ boardId: b.id, projectKey: "proj-a", name: "等" });
46
+ const done = createCard({ boardId: b.id, projectKey: "proj-a", name: "完" });
47
+ const other = createCard({ boardId: b.id, projectKey: "proj-a", name: "别人" });
48
+ updateCard(running.id, { execStatus: "running", owner: "me" });
49
+ updateCard(review.id, { execStatus: "review", owner: "me" });
50
+ updateCard(waiting.id, { execStatus: "waiting_reply", owner: "me" });
51
+ updateCard(done.id, { execStatus: "done", owner: "me" });
52
+ updateCard(other.id, { execStatus: "running", owner: "them" });
53
+
54
+ heartbeatOwnedCards("me");
55
+ assert.ok(getCard(running.id)?.heartbeat > 0);
56
+ assert.ok(getCard(review.id)?.heartbeat > 0);
57
+ assert.ok(getCard(waiting.id)?.heartbeat > 0);
58
+ assert.equal(getCard(done.id)?.heartbeat, 0); // 结束态不刷新
59
+ assert.equal(getCard(other.id)?.heartbeat, 0); // 别人家不刷新
60
+ });
61
+
62
+ test("isCardHandledByInstance 归属守卫矩阵(#27 核心)", () => {
63
+ const me = "inst-me";
64
+ const them = "inst-them";
65
+ const now = 1_000_000;
66
+ const fresh = now - 5_000; // 5s 前心跳(OWNER_STALE_MS=120s 内 → 活着)
67
+ const stale = now - (OWNER_STALE_MS + 1_000); // 超期 → 已死
68
+
69
+ // owner 为空(旧数据)→ 任何实例可处理
70
+ assert.equal(isCardHandledByInstance({ owner: null, heartbeat: 0 }, me, now), true);
71
+ // owner == 我 → 我负责
72
+ assert.equal(isCardHandledByInstance({ owner: me, heartbeat: 0 }, me, now), true);
73
+ // owner == 他人 且 心跳新鲜 → 跳过(← 防误翻他人正在跑的会话)
74
+ assert.equal(isCardHandledByInstance({ owner: them, heartbeat: fresh }, me, now), false);
75
+ // owner == 他人 但 心跳过期 → 接管
76
+ assert.equal(isCardHandledByInstance({ owner: them, heartbeat: stale }, me, now), true);
77
+ // 边界:正好 = OWNER_STALE_MS 视为过期(>=)
78
+ assert.equal(isCardHandledByInstance({ owner: them, heartbeat: now - OWNER_STALE_MS }, me, now), true);
79
+ });
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "fs";
2
2
  import { randomUUID } from "crypto";
3
3
  import { getBoard } from "./board-store";
4
+ import { reconcileBoard } from "./board-reconcile";
4
5
  import { startRpcSession, getRunningRpcSessionIds } from "./rpc-manager";
5
6
  import { resolveSessionPath } from "./session-reader";
6
7
  import { assignSessionToTask } from "./task-store";
@@ -16,6 +17,7 @@ import {
16
17
  updateCard,
17
18
  acquireDispatchToken,
18
19
  clearDispatchToken,
20
+ heartbeatOwnedCards,
19
21
  type TaskCard,
20
22
  type TaskCardQuestion,
21
23
  } from "./task-card-store";
@@ -36,6 +38,11 @@ import {
36
38
  export const TASK_SCHEDULER_INTERVAL_MS = 10_000;
37
39
  export const TASK_SCHEDULER_MAX_CONCURRENCY = 1;
38
40
 
41
+ /** 归属心跳刷新周期(独立于长 tick,防审核阻塞期间心跳断更) */
42
+ export const HEARTBEAT_INTERVAL_MS = 10_000;
43
+ /** owner 心跳过期阈值:超时视为 owner 已死,其他实例可接管判定 */
44
+ export const OWNER_STALE_MS = 120_000;
45
+
39
46
  // ============================================================================
40
47
  // S3 巡检/审核参数
41
48
  // ============================================================================
@@ -58,10 +65,15 @@ export const SESSION_SETTLE_MS = 5_000;
58
65
  declare global {
59
66
  var __piTaskScheduler: {
60
67
  timer: ReturnType<typeof setInterval>;
68
+ heartbeatTimer: ReturnType<typeof setInterval>;
61
69
  startedAt: number;
62
70
  /** 最近一次调度动作(挂 globalThis 防 instrumentation 与 API 路由加载两份模块实例不互通) */
63
71
  lastAction: TaskSchedulerLastAction;
72
+ /** 调度器此刻正在做的动作(看板状态展示:工作中/休眠两态);无=休眠 */
73
+ activity: SchedulerActivity | null;
64
74
  } | undefined;
75
+ /** 本进程实例 id(防热重载换 id;多实例各持不同 id,作为任务卡 owner) */
76
+ var __piWebInstanceId: string | undefined;
65
77
  }
66
78
 
67
79
  /** 派发 cwd:card.cwd 优先;否则看板所属项目根(project_key 若是存在的目录)。 */
@@ -111,6 +123,47 @@ async function ensureExecutionSession(
111
123
  return { ...fresh, created: true };
112
124
  }
113
125
 
126
+ /** 本进程实例 id:随机 UUID,globalThis 缓存(热重载不换),多实例各不同。 */
127
+ export function getInstanceId(): string {
128
+ if (!globalThis.__piWebInstanceId) globalThis.__piWebInstanceId = randomUUID();
129
+ return globalThis.__piWebInstanceId;
130
+ }
131
+
132
+ /**
133
+ * 归属守卫:这张卡的「状态判定」该不该由本实例做(#27 根因修复)。
134
+ * - owner 为空(旧数据/未升级实例派发)→ 任何实例可处理(保持旧语义)
135
+ * - owner == 我 → 我负责
136
+ * - owner == 他人且心跳新鲜 → 他人活着,跳过(防误翻他人正在跑的会话)
137
+ * - owner == 他人但心跳过期 → 他人已死,接管
138
+ */
139
+ export function isCardHandledByInstance(
140
+ card: Pick<TaskCard, "owner" | "heartbeat">,
141
+ myId: string,
142
+ now: number,
143
+ ): boolean {
144
+ return !card.owner || card.owner === myId || now - card.heartbeat >= OWNER_STALE_MS;
145
+ }
146
+
147
+ /**
148
+ * 自愈:本地 registry 里真实在跑、但卡状态却是 review 的 → 拉回 running 并接管归属。
149
+ * 兜底任何来源(未升级旧实例 / 竞态 / 手动)把还在跑的卡误标成 review 的情况——
150
+ * 只有会话确实在跑(isRunning 真)才回拉,真结束待审核的卡不会被动。
151
+ */
152
+ export async function restoreRunningReviewCards(): Promise<number> {
153
+ const myId = getInstanceId();
154
+ const now = Date.now();
155
+ const runningIds = new Set(getRunningRpcSessionIds());
156
+ let restored = 0;
157
+ for (const card of listCardsByExecStatus(["review"])) {
158
+ if (!card.sessionId) continue;
159
+ if (!runningIds.has(card.sessionId)) continue;
160
+ updateCard(card.id, { execStatus: "running", owner: myId, heartbeat: now });
161
+ console.log(`[task-scheduler] #${card.number} ${card.name} 自愈:会话仍在跑 → 拉回 running`);
162
+ restored += 1;
163
+ }
164
+ return restored;
165
+ }
166
+
114
167
  /** 派发单卡:建/复执行会话 → 命名 #N 标题 → 任务看板归属 → node 绑 session → 发 prompt → running。
115
168
  * 跨进程互斥:派发前先原子抢 dispatch_token(谁抢到谁执行),防多实例共库双会话。 */
116
169
  export async function dispatchCard(card: TaskCard): Promise<boolean> {
@@ -135,11 +188,16 @@ export async function dispatchCard(card: TaskCard): Promise<boolean> {
135
188
  const board = getBoard(card.boardId);
136
189
  if (board?.taskId) {
137
190
  assignSessionToTask(session.realSessionId, board.taskId);
191
+ // 注意:不在 assignSessionToTask 后 reconcile——此刻卡 sessionId 未落表,
192
+ // reconcile 只能补会话卡、建不了 exec 线 → 造成「先出卡后出线」。
193
+ // 统一在 updateCard(sessionId) 之后触发(补卡+建线同一次事务、一次广播)。
138
194
  }
139
195
 
140
196
  // 会话正忙(已在流式/处理中):说明上一轮已发过 prompt,直接标 running,不重复发
141
197
  if (session.session.isRunning()) {
142
- updateCard(card.id, { sessionId: session.realSessionId, execStatus: "running" });
198
+ updateCard(card.id, { sessionId: session.realSessionId, execStatus: "running", owner: getInstanceId(), heartbeat: Date.now() });
199
+ // sessionId 落表后立即 reconcile:补执行会话卡 + exec 线(卡线同现,不依赖定时兜底)
200
+ if (board?.taskId) reconcileTaskBoard(card.boardId);
143
201
  watchForAgentEnd(card, session.session);
144
202
  clearDispatchToken(card.id, token);
145
203
  console.log(`[task-scheduler] #${card.number} ${card.name} 会话忙(复用中),标 running 不重发`);
@@ -148,7 +206,9 @@ export async function dispatchCard(card: TaskCard): Promise<boolean> {
148
206
 
149
207
  await session.session.send({ type: "prompt", message: buildTaskPrompt(card) });
150
208
 
151
- updateCard(card.id, { sessionId: session.realSessionId, execStatus: "running" });
209
+ updateCard(card.id, { sessionId: session.realSessionId, execStatus: "running", owner: getInstanceId(), heartbeat: Date.now() });
210
+ // sessionId 落表后立即 reconcile:补执行会话卡 + exec 线(卡线同现,不依赖定时兜底)
211
+ if (board?.taskId) reconcileTaskBoard(card.boardId);
152
212
  watchForAgentEnd(card, session.session);
153
213
  clearDispatchToken(card.id, token);
154
214
  console.log(
@@ -173,6 +233,13 @@ export async function dispatchCard(card: TaskCard): Promise<boolean> {
173
233
  }
174
234
  }
175
235
 
236
+ /** 任务看板派生 reconcile(后端权威):补执行会话卡 + exec 线,广播到所有客户端 */
237
+ function reconcileTaskBoard(boardId: string): void {
238
+ void reconcileBoard(boardId).catch((e) =>
239
+ console.warn(`[task-scheduler] reconcile ${boardId} 异常:`, e?.message ?? e),
240
+ );
241
+ }
242
+
176
243
  /**
177
244
  * 订阅执行会话 agent_end:会话跑完立即把卡转 review(待审核),不等下一轮 reconcile。
178
245
  * 事件驱动实现「会话执行完立即进入待审核」。回调只处理卡仍为 running 的情况,避免覆盖后续流转。
@@ -198,9 +265,12 @@ function watchForAgentEnd(card: TaskCard, session: Awaited<ReturnType<typeof sta
198
265
  export async function reconcileEndedRunningCards(): Promise<number> {
199
266
  const runningIds = new Set(getRunningRpcSessionIds());
200
267
  const now = Date.now();
268
+ const myId = getInstanceId();
201
269
  let flipped = 0;
202
270
  for (const card of listRunningDispatched()) {
203
271
  if (!card.sessionId) continue;
272
+ // 归属守卫:他人活着拥有的卡不归本实例判定(防误翻别人正在跑的会话,#27 根因)
273
+ if (!isCardHandledByInstance(card, myId, now)) continue;
204
274
  // 会话真实在跑 → 保持 running
205
275
  if (runningIds.has(card.sessionId)) continue;
206
276
  // 会话不在跑:确认已静默一段时间(防回合间隙误判),才转 review
@@ -216,6 +286,8 @@ export async function reconcileEndedRunningCards(): Promise<number> {
216
286
 
217
287
  /** 单轮调度:并发闸门未满才派发;最多补满到全局上限。返回本轮派发数。 */
218
288
  export async function runSchedulerTick(): Promise<number> {
289
+ // 自愈:本地在跑但被误标 review 的卡先拉回 running(先于审核,避免对运行中卡审核烧 token)
290
+ await restoreRunningReviewCards();
219
291
  // 先巡检:会话已结束的 running 卡流转走(review),释放闸门(以真实会话状态为准)
220
292
  await reconcileEndedRunningCards();
221
293
  // S3:回复队列优先 → 审核 review 卡 → 巡检阻塞
@@ -235,7 +307,10 @@ export async function runSchedulerTick(): Promise<number> {
235
307
  for (const card of cards) {
236
308
  // 并发闸门按 running 数实时判断(上一张派发后 running 可能已满)
237
309
  if (countRunningDispatched() >= TASK_SCHEDULER_MAX_CONCURRENCY) break;
238
- const ok = await dispatchCard(card);
310
+ const ok = await withSchedulerActivity(
311
+ { kind: "dispatch", cardNumber: card.number, cardName: card.name, at: Date.now() },
312
+ () => dispatchCard(card),
313
+ );
239
314
  if (ok) {
240
315
  dispatched += 1;
241
316
  setLastAction({ type: "dispatch", cardNumber: card.number, cardName: card.name, at: Date.now() });
@@ -250,12 +325,19 @@ export async function runSchedulerTick(): Promise<number> {
250
325
 
251
326
  /** 回复队列:answered 且卡仍在 waiting_reply → 发回复续会话 → running。 */
252
327
  export async function processReplyQueue(): Promise<number> {
328
+ const myId = getInstanceId();
329
+ const now = Date.now();
253
330
  let resumed = 0;
254
331
  for (const q of listAnswerableQuestions()) {
255
332
  const card = getCard(q.cardId);
256
333
  if (!card || card.execStatus !== "waiting_reply" || !card.sessionId) continue;
334
+ // 归属守卫:他人活着拥有的卡不归本实例续会话(防双实例重复发回复)
335
+ if (!isCardHandledByInstance(card, myId, now)) continue;
257
336
  if (countRunningDispatched() >= TASK_SCHEDULER_MAX_CONCURRENCY) break;
258
- const ok = await resumeWithAnswer(card, q);
337
+ const ok = await withSchedulerActivity(
338
+ { kind: "resume", cardNumber: card.number, cardName: card.name, at: Date.now() },
339
+ () => resumeWithAnswer(card, q),
340
+ );
259
341
  if (ok) {
260
342
  resumed += 1;
261
343
  setLastAction({ type: "resume", cardNumber: card.number, cardName: card.name, at: Date.now() });
@@ -273,7 +355,7 @@ async function resumeWithAnswer(card: TaskCard, q: TaskCardQuestion): Promise<bo
273
355
  type: "prompt",
274
356
  message: `【用户回答了你的提问】\n${q.answer}\n\n请据此继续完成任务。`,
275
357
  });
276
- updateCard(card.id, { execStatus: "running" });
358
+ updateCard(card.id, { execStatus: "running", owner: getInstanceId(), heartbeat: Date.now() });
277
359
  console.log(`[task-scheduler] 回复续会话 #${card.number} ${card.name}`);
278
360
  return true;
279
361
  } catch (error) {
@@ -309,8 +391,11 @@ function enterWaitingReply(card: TaskCard, questionHint: string): void {
309
391
  /** 审核 review 卡:程序检测失败→failed;否则 AI 审核 → done/failed/waiting_reply/other。 */
310
392
  export async function processReviewCards(): Promise<number> {
311
393
  const now = Date.now();
394
+ const myId = getInstanceId();
312
395
  let processed = 0;
313
396
  for (const card of listCardsByExecStatus(["review"])) {
397
+ // 归属守卫:他人活着拥有的卡不归本实例审核(防双实例重复审核/判定互覆盖)
398
+ if (!isCardHandledByInstance(card, myId, now)) continue;
314
399
  const snapshot = card.sessionId ? await readSessionAuditSnapshot(card.sessionId) : null;
315
400
 
316
401
  // 1. 程序检测:最后消息有失败迹象 → 直接 failed(计次重试)
@@ -332,13 +417,16 @@ export async function processReviewCards(): Promise<number> {
332
417
  reviewAuditAt.set(card.id, now);
333
418
  const cwd = resolveDispatchCwd(card);
334
419
  if (!cwd) continue;
335
- const result = await runAuditVerdict({
336
- cwd,
337
- cardNumber: card.number,
338
- cardName: card.name,
339
- taskDescription: card.description,
340
- recentMessages: snapshot.recentText,
341
- });
420
+ const result = await withSchedulerActivity(
421
+ { kind: "review", cardNumber: card.number, cardName: card.name, at: Date.now() },
422
+ () => runAuditVerdict({
423
+ cwd,
424
+ cardNumber: card.number,
425
+ cardName: card.name,
426
+ taskDescription: card.description,
427
+ recentMessages: snapshot.recentText,
428
+ }),
429
+ );
342
430
  if (result?.verdict === "done") {
343
431
  updateCard(card.id, { execStatus: "done" });
344
432
  console.log(`[task-scheduler] #${card.number} 审核通过 → done(${result.reason})`);
@@ -361,9 +449,12 @@ export async function processReviewCards(): Promise<number> {
361
449
  /** running 卡阻塞巡检:最后消息 > BLOCK_IDLE_MS 无进展 → AI 判定阻塞类型 → 处置。 */
362
450
  export async function checkRunningCardsBlocked(): Promise<number> {
363
451
  const now = Date.now();
452
+ const myId = getInstanceId();
364
453
  let handled = 0;
365
454
  for (const card of listCardsByExecStatus(["running"])) {
366
455
  if (!card.sessionId) continue;
456
+ // 归属守卫:他人活着拥有的卡不归本实例巡检
457
+ if (!isCardHandledByInstance(card, myId, now)) continue;
367
458
  // 冷却:同一卡冷却期内不重复判定
368
459
  const last = blockCheckAt.get(card.id) ?? 0;
369
460
  if (now - last < BLOCK_COOLDOWN_MS) continue;
@@ -374,13 +465,16 @@ export async function checkRunningCardsBlocked(): Promise<number> {
374
465
  blockCheckAt.set(card.id, now);
375
466
  const cwd = resolveDispatchCwd(card);
376
467
  if (!cwd) continue;
377
- const result = await runBlockCheck({
378
- cwd,
379
- cardNumber: card.number,
380
- cardName: card.name,
381
- taskDescription: card.description,
382
- recentMessages: snapshot.recentText,
383
- });
468
+ const result = await withSchedulerActivity(
469
+ { kind: "blockcheck", cardNumber: card.number, cardName: card.name, at: Date.now() },
470
+ () => runBlockCheck({
471
+ cwd,
472
+ cardNumber: card.number,
473
+ cardName: card.name,
474
+ taskDescription: card.description,
475
+ recentMessages: snapshot.recentText,
476
+ }),
477
+ );
384
478
  if (!result) continue;
385
479
  switch (result.kind) {
386
480
  case "sync_server":
@@ -453,7 +547,23 @@ export function startTaskScheduler(): void {
453
547
  });
454
548
  }, TASK_SCHEDULER_INTERVAL_MS);
455
549
  timer.unref?.();
456
- const sched = { timer, startedAt: Date.now(), lastAction: { type: "none" as const, at: 0 } };
550
+ // 归属心跳:独立于长 tick(审核最长 90s),owner 存活期间心跳不断更,
551
+ // 其他实例据此知道该卡归谁、不误翻。
552
+ const heartbeatTimer = setInterval(() => {
553
+ try {
554
+ heartbeatOwnedCards(getInstanceId());
555
+ } catch (error) {
556
+ console.error("[task-scheduler] heartbeat error:", error instanceof Error ? error.message : error);
557
+ }
558
+ }, HEARTBEAT_INTERVAL_MS);
559
+ heartbeatTimer.unref?.();
560
+ const sched = {
561
+ timer,
562
+ heartbeatTimer,
563
+ startedAt: Date.now(),
564
+ lastAction: { type: "none" as const, at: 0 },
565
+ activity: null,
566
+ };
457
567
  globalThis.__piTaskScheduler = sched;
458
568
  console.log(`[pi-web] task scheduler started (interval ${TASK_SCHEDULER_INTERVAL_MS}ms, max ${TASK_SCHEDULER_MAX_CONCURRENCY})`);
459
569
  }
@@ -477,6 +587,41 @@ export interface TaskSchedulerStatus {
477
587
  running: TaskCard[];
478
588
  /** 最近一次调度动作 */
479
589
  lastAction: TaskSchedulerLastAction;
590
+ /** 调度器此刻正在做的动作(工作中);null=休眠 */
591
+ activity: SchedulerActivity | null;
592
+ /** 任务卡队列统计(按执行状态实时算,展示用) */
593
+ queue: TaskSchedulerQueueStats;
594
+ }
595
+
596
+ /** 调度器当前动作(看板状态展示)。kind:派发/回复续会话/审核/阻塞巡检。 */
597
+ export interface SchedulerActivity {
598
+ kind: "dispatch" | "resume" | "review" | "blockcheck";
599
+ cardNumber?: number;
600
+ cardName?: string;
601
+ at: number;
602
+ }
603
+
604
+ export interface TaskSchedulerQueueStats {
605
+ /** 就绪可派发(todo & 未开始/可重试 & 前置均完成) */
606
+ dispatchable: TaskCardBrief[];
607
+ /** 待审核(review 态) */
608
+ review: TaskCardBrief[];
609
+ /** 等回复(waiting_reply 态) */
610
+ waitingReply: TaskCardBrief[];
611
+ /** 失败(含重试超限) */
612
+ failed: TaskCardBrief[];
613
+ }
614
+
615
+ /** 队列卡片轻量字段(展示/定位用,不携带 description 等大字段) */
616
+ export interface TaskCardBrief {
617
+ id: string;
618
+ boardId: string;
619
+ number: number;
620
+ name: string;
621
+ }
622
+
623
+ function toBrief(card: TaskCard): TaskCardBrief {
624
+ return { id: card.id, boardId: card.boardId, number: card.number, name: card.name };
480
625
  }
481
626
 
482
627
  function getLastAction(): TaskSchedulerLastAction {
@@ -486,13 +631,42 @@ function setLastAction(action: TaskSchedulerLastAction): void {
486
631
  if (globalThis.__piTaskScheduler) globalThis.__piTaskScheduler.lastAction = action;
487
632
  }
488
633
 
634
+ /** 读取当前调度器动作(工作中状态),无则 null */
635
+ function getActivity(): SchedulerActivity | null {
636
+ return globalThis.__piTaskScheduler?.activity ?? null;
637
+ }
638
+
639
+ /** 置/清当前调度器动作。UI 轮询读到非空 = 该动作工作中。 */
640
+ function setActivity(activity: SchedulerActivity | null): void {
641
+ if (globalThis.__piTaskScheduler) globalThis.__piTaskScheduler.activity = activity;
642
+ }
643
+
644
+ /** 包一段调度动作:执行期间置 activity(工作中),结束清空(休眠)。
645
+ * 串行 tick 内各阶段不嵌套(同 tick 无并发),末尾 clear 不影响其他阶段。 */
646
+ async function withSchedulerActivity<T>(activity: SchedulerActivity, fn: () => Promise<T>): Promise<T> {
647
+ setActivity(activity);
648
+ try {
649
+ return await fn();
650
+ } finally {
651
+ setActivity(null);
652
+ }
653
+ }
654
+
489
655
  /** 当前调度器状态:真实运行中的卡 + 最近动作。UI 轮询展示「正在运行 xxx任务」。
490
656
  * running 只列执行会话真实在跑的卡(以 getRunningRpcSessionIds 为准,不读过期卡状态)。 */
491
657
  export function getSchedulerStatus(): TaskSchedulerStatus {
492
658
  const runningIds = new Set(getRunningRpcSessionIds());
659
+ const running = listRunningDispatched().filter((c) => c.sessionId && runningIds.has(c.sessionId));
493
660
  return {
494
661
  started: Boolean(globalThis.__piTaskScheduler),
495
- running: listRunningDispatched().filter((c) => c.sessionId && runningIds.has(c.sessionId)),
662
+ running,
496
663
  lastAction: getLastAction(),
664
+ activity: getActivity(),
665
+ queue: {
666
+ dispatchable: listDispatchableCards().map(toBrief),
667
+ review: listCardsByExecStatus(["review"]).map(toBrief),
668
+ waitingReply: listCardsByExecStatus(["waiting_reply"]).map(toBrief),
669
+ failed: listCardsByExecStatus(["failed"]).map(toBrief),
670
+ },
497
671
  };
498
672
  }
@@ -0,0 +1,139 @@
1
+ // ============================================================================
2
+ // yjs 协同房间服务器(Hocuspocus 内嵌版)—— 替代 tldraw sync(TLSocketRoom)
3
+ // - 每看板一个 Y.Doc(documentName = boardId),结构:nodes/edges/view 三个 Y.Map
4
+ // - 持久化:SQLite(sync.db 独立表 yjs_documents),onStoreDocument 存全量 update
5
+ // - 内嵌:server.mjs 的 upgrade 事件分流 —— 仅 /connect 走 crossws,其余交 Next
6
+ // - 服务端权威写:openDirectConnection(boardId).transact() —— 派生 reconcile 用
7
+ //
8
+ // 用法(内嵌):server.on("upgrade", handleYjsUpgrade) —— 返回 false 表示未处理
9
+ // ============================================================================
10
+ import { createRequire } from "node:module";
11
+ const require = createRequire(import.meta.url);
12
+ import { homedir } from "node:os";
13
+ import { join, normalize } from "node:path";
14
+
15
+ const { DatabaseSync } = require("node:sqlite");
16
+ const { Server } = require("@hocuspocus/server");
17
+ const Y = require("yjs");
18
+
19
+ // ---- agent dir(与 lib/sqlite-db.ts 同语义)----
20
+ export function getAgentDir() {
21
+ const envDir = process.env.PI_CODING_AGENT_DIR;
22
+ if (envDir) {
23
+ const expanded = envDir.startsWith("~") ? join(homedir(), envDir.slice(1)) : envDir;
24
+ return normalize(expanded);
25
+ }
26
+ return join(homedir(), ".pi", "agent");
27
+ }
28
+ export const YJS_DB_FILE = join(getAgentDir(), "sync.db");
29
+
30
+ // 单一 DatabaseSync 连接(node:sqlite 同步,单连接线程安全)
31
+ const db = new DatabaseSync(YJS_DB_FILE);
32
+ db.exec(`
33
+ CREATE TABLE IF NOT EXISTS yjs_documents (
34
+ board_id TEXT PRIMARY KEY,
35
+ data BLOB NOT NULL,
36
+ updated INTEGER NOT NULL
37
+ );
38
+ `);
39
+
40
+ /** Hocuspocus server 实例(惰性创建) */
41
+ let hocuspocus = null;
42
+
43
+ /** 建(或复用)Hocuspocus server */
44
+ function getServer() {
45
+ if (hocuspocus) return hocuspocus;
46
+ hocuspocus = new Server({
47
+ // 端口留给外部 httpServer,这里不 listen
48
+ port: 0,
49
+ // 客户端断开后延迟持久化再卸载
50
+ unloadImmediately: false,
51
+ // 文档变更持久化 debounce(默认 2s,配小一点让写库及时)
52
+ debounce: 500,
53
+ async onLoadDocument({ documentName, document }) {
54
+ const row = db.prepare("SELECT data FROM yjs_documents WHERE board_id = ?").get(documentName);
55
+ if (row) {
56
+ try {
57
+ Y.applyUpdate(document, new Uint8Array(row.data));
58
+ } catch (e) {
59
+ console.warn(`[yjs] ${documentName} onLoad 解码失败:`, e?.message ?? e);
60
+ }
61
+ }
62
+ },
63
+ async onStoreDocument({ documentName, document }) {
64
+ const update = Y.encodeStateAsUpdate(document);
65
+ db.prepare(
66
+ "INSERT INTO yjs_documents (board_id, data, updated) VALUES (?, ?, ?) " +
67
+ "ON CONFLICT(board_id) DO UPDATE SET data = excluded.data, updated = excluded.updated",
68
+ ).run(documentName, Buffer.from(update), Date.now());
69
+ },
70
+ });
71
+ return hocuspocus;
72
+ }
73
+
74
+ /**
75
+ * 处理一条 WebSocket upgrade 请求(挂在 Node http server 的 "upgrade" 事件)。
76
+ * 仅 /connect 路径交 Hocuspocus(provider 的 name 参数 = documentName);
77
+ * 其余(Next HMR 等)返回 false,调用方转交 Next。
78
+ * @returns {boolean} 是否已处理
79
+ */
80
+ export function handleYjsUpgrade(req, socket, head) {
81
+ let url;
82
+ try {
83
+ url = new URL(req.url, "http://localhost");
84
+ } catch {
85
+ return false;
86
+ }
87
+ if (url.pathname !== "/connect") return false;
88
+ const server = getServer();
89
+ server.crossws.handleUpgrade(req, socket, head).catch((e) => {
90
+ console.error(`[yjs] /connect handleUpgrade 异常:`, e?.message ?? e);
91
+ try { socket.destroy(); } catch { /* ignore */ }
92
+ });
93
+ return true;
94
+ }
95
+
96
+ /**
97
+ * 服务端权威写:对指定看板执行一个事务(直接改 Y.Doc,不经 WebSocket)。
98
+ * transaction(maps, doc) 内用 maps.nodes / maps.edges / maps.view 操作。
99
+ * 变更自动经 Hocuspocus 广播到已连接客户端并持久化(onStoreDocument)。
100
+ */
101
+ export async function mutateBoard(boardId, transaction) {
102
+ const server = getServer();
103
+ const dc = await server.hocuspocus.openDirectConnection(boardId);
104
+ try {
105
+ await dc.transact((document) => {
106
+ const maps = {
107
+ nodes: document.getMap("nodes"),
108
+ edges: document.getMap("edges"),
109
+ view: document.getMap("view"),
110
+ ydoc: document,
111
+ };
112
+ transaction(maps, document);
113
+ });
114
+ } finally {
115
+ await dc.disconnect().catch(() => {});
116
+ }
117
+ }
118
+
119
+ /** 删除看板文档(删除看板时调用) */
120
+ export async function destroyBoardDocument(boardId) {
121
+ const server = getServer();
122
+ try {
123
+ await server.hocuspocus.closeConnections(boardId);
124
+ db.prepare("DELETE FROM yjs_documents WHERE board_id = ?").run(boardId);
125
+ } catch (e) {
126
+ console.warn(`[yjs] destroy ${boardId} 异常:`, e?.message ?? e);
127
+ }
128
+ }
129
+
130
+ /** 当前加载中的文档数(诊断用) */
131
+ export function getYjsRoomCount() {
132
+ return hocuspocus?.hocuspocus?.documents?.size ?? 0;
133
+ }
134
+
135
+ // ---- 注入 globalThis(供 lib/board-reconcile.ts 调用,避免 Next 打包 node:sqlite)----
136
+ globalThis.__yjsBoard = {
137
+ mutateBoard,
138
+ destroyBoardDocument,
139
+ };