@superblocksteam/vite-plugin-file-sync 2.0.162 → 2.0.163-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (420) hide show
  1. package/dist/ai-service/agent/middleware.d.ts.map +1 -1
  2. package/dist/ai-service/agent/middleware.js +18 -7
  3. package/dist/ai-service/agent/middleware.js.map +1 -1
  4. package/dist/ai-service/agent/prompts/build-base-system-prompt.d.ts.map +1 -1
  5. package/dist/ai-service/agent/prompts/build-base-system-prompt.js +43 -29
  6. package/dist/ai-service/agent/prompts/build-base-system-prompt.js.map +1 -1
  7. package/dist/ai-service/agent/prompts/build-security-scan-prompt.d.ts.map +1 -1
  8. package/dist/ai-service/agent/prompts/build-security-scan-prompt.js +25 -0
  9. package/dist/ai-service/agent/prompts/build-security-scan-prompt.js.map +1 -1
  10. package/dist/ai-service/agent/subagents/coding/run-coding-subagent.d.ts.map +1 -1
  11. package/dist/ai-service/agent/subagents/coding/run-coding-subagent.js +15 -0
  12. package/dist/ai-service/agent/subagents/coding/run-coding-subagent.js.map +1 -1
  13. package/dist/ai-service/agent/tools/apis/test-api.d.ts.map +1 -1
  14. package/dist/ai-service/agent/tools/apis/test-api.js +4 -2
  15. package/dist/ai-service/agent/tools/apis/test-api.js.map +1 -1
  16. package/dist/ai-service/agent/tools/app-environment/switch-selected-environment.d.ts +18 -0
  17. package/dist/ai-service/agent/tools/app-environment/switch-selected-environment.d.ts.map +1 -0
  18. package/dist/ai-service/agent/tools/app-environment/switch-selected-environment.js +61 -0
  19. package/dist/ai-service/agent/tools/app-environment/switch-selected-environment.js.map +1 -0
  20. package/dist/ai-service/agent/tools/build-capture-screenshot.js +1 -1
  21. package/dist/ai-service/agent/tools/build-install-packages.d.ts.map +1 -1
  22. package/dist/ai-service/agent/tools/build-install-packages.js +21 -8
  23. package/dist/ai-service/agent/tools/build-install-packages.js.map +1 -1
  24. package/dist/ai-service/agent/tools/build-read-file.d.ts +87 -1
  25. package/dist/ai-service/agent/tools/build-read-file.d.ts.map +1 -1
  26. package/dist/ai-service/agent/tools/build-read-file.js +453 -82
  27. package/dist/ai-service/agent/tools/build-read-file.js.map +1 -1
  28. package/dist/ai-service/agent/tools/build-write-file.d.ts +1 -0
  29. package/dist/ai-service/agent/tools/build-write-file.d.ts.map +1 -1
  30. package/dist/ai-service/agent/tools/build-write-file.js +18 -2
  31. package/dist/ai-service/agent/tools/build-write-file.js.map +1 -1
  32. package/dist/ai-service/agent/tools/debug-cache.d.ts +11 -0
  33. package/dist/ai-service/agent/tools/debug-cache.d.ts.map +1 -1
  34. package/dist/ai-service/agent/tools/debug-cache.js +62 -14
  35. package/dist/ai-service/agent/tools/debug-cache.js.map +1 -1
  36. package/dist/ai-service/agent/tools/index.d.ts +2 -1
  37. package/dist/ai-service/agent/tools/index.d.ts.map +1 -1
  38. package/dist/ai-service/agent/tools/index.js +2 -1
  39. package/dist/ai-service/agent/tools/index.js.map +1 -1
  40. package/dist/ai-service/agent/tools/integrations/metadata.d.ts +1 -0
  41. package/dist/ai-service/agent/tools/integrations/metadata.d.ts.map +1 -1
  42. package/dist/ai-service/agent/tools/integrations/metadata.js +7 -2
  43. package/dist/ai-service/agent/tools/integrations/metadata.js.map +1 -1
  44. package/dist/ai-service/agent/tools/read-file-metrics.d.ts +30 -0
  45. package/dist/ai-service/agent/tools/read-file-metrics.d.ts.map +1 -0
  46. package/dist/ai-service/agent/tools/read-file-metrics.js +158 -0
  47. package/dist/ai-service/agent/tools/read-file-metrics.js.map +1 -0
  48. package/dist/ai-service/agent/tools/subagent-isolation.d.ts +27 -0
  49. package/dist/ai-service/agent/tools/subagent-isolation.d.ts.map +1 -0
  50. package/dist/ai-service/agent/tools/subagent-isolation.js +57 -0
  51. package/dist/ai-service/agent/tools/subagent-isolation.js.map +1 -0
  52. package/dist/ai-service/agent/tools.d.ts +4 -1
  53. package/dist/ai-service/agent/tools.d.ts.map +1 -1
  54. package/dist/ai-service/agent/tools.js +51 -5
  55. package/dist/ai-service/agent/tools.js.map +1 -1
  56. package/dist/ai-service/agent/tools2/tools/download-attachments.d.ts.map +1 -1
  57. package/dist/ai-service/agent/tools2/tools/download-attachments.js +1 -0
  58. package/dist/ai-service/agent/tools2/tools/download-attachments.js.map +1 -1
  59. package/dist/ai-service/agent/tools2/tools/start-test-run.d.ts.map +1 -1
  60. package/dist/ai-service/agent/tools2/tools/start-test-run.js +14 -0
  61. package/dist/ai-service/agent/tools2/tools/start-test-run.js.map +1 -1
  62. package/dist/ai-service/agent/utils.d.ts.map +1 -1
  63. package/dist/ai-service/agent/utils.js +2 -2
  64. package/dist/ai-service/agent/utils.js.map +1 -1
  65. package/dist/ai-service/app-interface/filesystem/draft-manager.d.ts +2 -1
  66. package/dist/ai-service/app-interface/filesystem/draft-manager.d.ts.map +1 -1
  67. package/dist/ai-service/app-interface/filesystem/draft-manager.js +3 -0
  68. package/dist/ai-service/app-interface/filesystem/draft-manager.js.map +1 -1
  69. package/dist/ai-service/app-interface/filesystem/handlers/skill-handler.d.ts +2 -1
  70. package/dist/ai-service/app-interface/filesystem/handlers/skill-handler.d.ts.map +1 -1
  71. package/dist/ai-service/app-interface/filesystem/handlers/skill-handler.js +13 -11
  72. package/dist/ai-service/app-interface/filesystem/handlers/skill-handler.js.map +1 -1
  73. package/dist/ai-service/app-interface/filesystem/virtual-file-system.d.ts.map +1 -1
  74. package/dist/ai-service/app-interface/filesystem/virtual-file-system.js +1 -1
  75. package/dist/ai-service/app-interface/filesystem/virtual-file-system.js.map +1 -1
  76. package/dist/ai-service/app-interface/shell.d.ts +18 -9
  77. package/dist/ai-service/app-interface/shell.d.ts.map +1 -1
  78. package/dist/ai-service/app-interface/shell.js +92 -112
  79. package/dist/ai-service/app-interface/shell.js.map +1 -1
  80. package/dist/ai-service/app-interface/typecheck-output-parser.d.ts +9 -0
  81. package/dist/ai-service/app-interface/typecheck-output-parser.d.ts.map +1 -0
  82. package/dist/ai-service/app-interface/typecheck-output-parser.js +21 -0
  83. package/dist/ai-service/app-interface/typecheck-output-parser.js.map +1 -0
  84. package/dist/ai-service/app-interface/vulnerability-gate.d.ts +5 -0
  85. package/dist/ai-service/app-interface/vulnerability-gate.d.ts.map +1 -1
  86. package/dist/ai-service/app-interface/vulnerability-gate.js +9 -2
  87. package/dist/ai-service/app-interface/vulnerability-gate.js.map +1 -1
  88. package/dist/ai-service/app-layout.d.ts +13 -0
  89. package/dist/ai-service/app-layout.d.ts.map +1 -0
  90. package/dist/ai-service/app-layout.js +25 -0
  91. package/dist/ai-service/app-layout.js.map +1 -0
  92. package/dist/ai-service/app-skills/manager.d.ts +2 -0
  93. package/dist/ai-service/app-skills/manager.d.ts.map +1 -1
  94. package/dist/ai-service/app-skills/manager.js +13 -6
  95. package/dist/ai-service/app-skills/manager.js.map +1 -1
  96. package/dist/ai-service/attachment-label-metrics.d.ts +5 -0
  97. package/dist/ai-service/attachment-label-metrics.d.ts.map +1 -0
  98. package/dist/ai-service/attachment-label-metrics.js +26 -0
  99. package/dist/ai-service/attachment-label-metrics.js.map +1 -0
  100. package/dist/ai-service/attachment-upload.d.ts.map +1 -1
  101. package/dist/ai-service/attachment-upload.js +6 -1
  102. package/dist/ai-service/attachment-upload.js.map +1 -1
  103. package/dist/ai-service/attachments/validate-prompt-attachments.d.ts +7 -0
  104. package/dist/ai-service/attachments/validate-prompt-attachments.d.ts.map +1 -0
  105. package/dist/ai-service/attachments/validate-prompt-attachments.js +96 -0
  106. package/dist/ai-service/attachments/validate-prompt-attachments.js.map +1 -0
  107. package/dist/ai-service/chat/chat-push-metrics.d.ts +17 -0
  108. package/dist/ai-service/chat/chat-push-metrics.d.ts.map +1 -1
  109. package/dist/ai-service/chat/chat-push-metrics.js +45 -0
  110. package/dist/ai-service/chat/chat-push-metrics.js.map +1 -1
  111. package/dist/ai-service/chat/chat-session-store.d.ts +51 -0
  112. package/dist/ai-service/chat/chat-session-store.d.ts.map +1 -1
  113. package/dist/ai-service/chat/chat-session-store.js +181 -4
  114. package/dist/ai-service/chat/chat-session-store.js.map +1 -1
  115. package/dist/ai-service/features.d.ts +32 -0
  116. package/dist/ai-service/features.d.ts.map +1 -1
  117. package/dist/ai-service/features.js +32 -0
  118. package/dist/ai-service/features.js.map +1 -1
  119. package/dist/ai-service/helper-system-ai-attributes.d.ts +1 -1
  120. package/dist/ai-service/helper-system-ai-attributes.d.ts.map +1 -1
  121. package/dist/ai-service/helper-system-ai-attributes.js.map +1 -1
  122. package/dist/ai-service/index.d.ts +19 -4
  123. package/dist/ai-service/index.d.ts.map +1 -1
  124. package/dist/ai-service/index.js +176 -23
  125. package/dist/ai-service/index.js.map +1 -1
  126. package/dist/ai-service/llm/client.d.ts +9 -0
  127. package/dist/ai-service/llm/client.d.ts.map +1 -1
  128. package/dist/ai-service/llm/client.js +13 -43
  129. package/dist/ai-service/llm/client.js.map +1 -1
  130. package/dist/ai-service/llm/context-v2/adapter.d.ts +2 -0
  131. package/dist/ai-service/llm/context-v2/adapter.d.ts.map +1 -1
  132. package/dist/ai-service/llm/context-v2/adapter.js +6 -0
  133. package/dist/ai-service/llm/context-v2/adapter.js.map +1 -1
  134. package/dist/ai-service/llm/context-v2/compaction/server-side.d.ts +8 -0
  135. package/dist/ai-service/llm/context-v2/compaction/server-side.d.ts.map +1 -1
  136. package/dist/ai-service/llm/context-v2/compaction/server-side.js +8 -0
  137. package/dist/ai-service/llm/context-v2/compaction/server-side.js.map +1 -1
  138. package/dist/ai-service/llm/context-v2/context-component-partition.d.ts +24 -0
  139. package/dist/ai-service/llm/context-v2/context-component-partition.d.ts.map +1 -0
  140. package/dist/ai-service/llm/context-v2/context-component-partition.js +222 -0
  141. package/dist/ai-service/llm/context-v2/context-component-partition.js.map +1 -0
  142. package/dist/ai-service/llm/context-v2/context-metrics.d.ts +9 -0
  143. package/dist/ai-service/llm/context-v2/context-metrics.d.ts.map +1 -1
  144. package/dist/ai-service/llm/context-v2/context-metrics.js +40 -2
  145. package/dist/ai-service/llm/context-v2/context-metrics.js.map +1 -1
  146. package/dist/ai-service/llm/context-v2/context.d.ts +116 -4
  147. package/dist/ai-service/llm/context-v2/context.d.ts.map +1 -1
  148. package/dist/ai-service/llm/context-v2/context.js +276 -64
  149. package/dist/ai-service/llm/context-v2/context.js.map +1 -1
  150. package/dist/ai-service/llm/context-v2/conversation-context.d.ts +18 -0
  151. package/dist/ai-service/llm/context-v2/conversation-context.d.ts.map +1 -1
  152. package/dist/ai-service/llm/context-v2/file-read-ledger.d.ts +218 -0
  153. package/dist/ai-service/llm/context-v2/file-read-ledger.d.ts.map +1 -0
  154. package/dist/ai-service/llm/context-v2/file-read-ledger.js +275 -0
  155. package/dist/ai-service/llm/context-v2/file-read-ledger.js.map +1 -0
  156. package/dist/ai-service/llm/context-v2/phase1-tool-summarizer.d.ts.map +1 -1
  157. package/dist/ai-service/llm/context-v2/phase1-tool-summarizer.js +5 -2
  158. package/dist/ai-service/llm/context-v2/phase1-tool-summarizer.js.map +1 -1
  159. package/dist/ai-service/llm/context-v2/token-tracker.d.ts +2 -0
  160. package/dist/ai-service/llm/context-v2/token-tracker.d.ts.map +1 -1
  161. package/dist/ai-service/llm/context-v2/token-tracker.js +4 -0
  162. package/dist/ai-service/llm/context-v2/token-tracker.js.map +1 -1
  163. package/dist/ai-service/llm/context-v2/tool-schema-tokens.d.ts +22 -0
  164. package/dist/ai-service/llm/context-v2/tool-schema-tokens.d.ts.map +1 -0
  165. package/dist/ai-service/llm/context-v2/tool-schema-tokens.js +36 -0
  166. package/dist/ai-service/llm/context-v2/tool-schema-tokens.js.map +1 -0
  167. package/dist/ai-service/llm/context-v2/types.d.ts +8 -0
  168. package/dist/ai-service/llm/context-v2/types.d.ts.map +1 -1
  169. package/dist/ai-service/llm/context-v2/types.js +48 -0
  170. package/dist/ai-service/llm/context-v2/types.js.map +1 -1
  171. package/dist/ai-service/llm/error.d.ts +2 -0
  172. package/dist/ai-service/llm/error.d.ts.map +1 -1
  173. package/dist/ai-service/llm/error.js +1 -0
  174. package/dist/ai-service/llm/error.js.map +1 -1
  175. package/dist/ai-service/llm/retry-exhaustion-reason.d.ts +2 -0
  176. package/dist/ai-service/llm/retry-exhaustion-reason.d.ts.map +1 -0
  177. package/dist/ai-service/llm/retry-exhaustion-reason.js +2 -0
  178. package/dist/ai-service/llm/retry-exhaustion-reason.js.map +1 -0
  179. package/dist/ai-service/llm/stream/errors.d.ts +6 -7
  180. package/dist/ai-service/llm/stream/errors.d.ts.map +1 -1
  181. package/dist/ai-service/llm/stream/errors.js +7 -1
  182. package/dist/ai-service/llm/stream/errors.js.map +1 -1
  183. package/dist/ai-service/llm/stream/event-bus.d.ts +9 -1
  184. package/dist/ai-service/llm/stream/event-bus.d.ts.map +1 -1
  185. package/dist/ai-service/llm/stream/event-bus.js.map +1 -1
  186. package/dist/ai-service/llm/stream/idle-monitor.d.ts.map +1 -1
  187. package/dist/ai-service/llm/stream/idle-monitor.js.map +1 -1
  188. package/dist/ai-service/llm/stream/observers/context.d.ts.map +1 -1
  189. package/dist/ai-service/llm/stream/observers/context.js +13 -0
  190. package/dist/ai-service/llm/stream/observers/context.js.map +1 -1
  191. package/dist/ai-service/llm/stream/observers/llmobs.d.ts +14 -1
  192. package/dist/ai-service/llm/stream/observers/llmobs.d.ts.map +1 -1
  193. package/dist/ai-service/llm/stream/observers/llmobs.js +91 -7
  194. package/dist/ai-service/llm/stream/observers/llmobs.js.map +1 -1
  195. package/dist/ai-service/llm/stream/observers/logging.d.ts +3 -1
  196. package/dist/ai-service/llm/stream/observers/logging.d.ts.map +1 -1
  197. package/dist/ai-service/llm/stream/observers/logging.js +73 -52
  198. package/dist/ai-service/llm/stream/observers/logging.js.map +1 -1
  199. package/dist/ai-service/llm/stream/observers/retry-notification.d.ts.map +1 -1
  200. package/dist/ai-service/llm/stream/observers/retry-notification.js +16 -9
  201. package/dist/ai-service/llm/stream/observers/retry-notification.js.map +1 -1
  202. package/dist/ai-service/llm/stream/orchestrator.d.ts +5 -1
  203. package/dist/ai-service/llm/stream/orchestrator.d.ts.map +1 -1
  204. package/dist/ai-service/llm/stream/orchestrator.js +103 -12
  205. package/dist/ai-service/llm/stream/orchestrator.js.map +1 -1
  206. package/dist/ai-service/llm/stream/retry-engine.d.ts +3 -1
  207. package/dist/ai-service/llm/stream/retry-engine.d.ts.map +1 -1
  208. package/dist/ai-service/llm/stream/retry-engine.js +9 -0
  209. package/dist/ai-service/llm/stream/retry-engine.js.map +1 -1
  210. package/dist/ai-service/llm/stream/session.d.ts +8 -1
  211. package/dist/ai-service/llm/stream/session.d.ts.map +1 -1
  212. package/dist/ai-service/llm/stream/session.js +19 -0
  213. package/dist/ai-service/llm/stream/session.js.map +1 -1
  214. package/dist/ai-service/llm/stream/types.d.ts +13 -0
  215. package/dist/ai-service/llm/stream/types.d.ts.map +1 -1
  216. package/dist/ai-service/llm/stream/wait-state-tracker.d.ts +5 -14
  217. package/dist/ai-service/llm/stream/wait-state-tracker.d.ts.map +1 -1
  218. package/dist/ai-service/llm/stream/wait-state-tracker.js +25 -0
  219. package/dist/ai-service/llm/stream/wait-state-tracker.js.map +1 -1
  220. package/dist/ai-service/mcp/embedded-playwright-mcp-server.js +8 -8
  221. package/dist/ai-service/mcp/embedded-playwright-mcp-server.js.map +1 -1
  222. package/dist/ai-service/policy-gate-remediation-usage.d.ts +2 -0
  223. package/dist/ai-service/policy-gate-remediation-usage.d.ts.map +1 -0
  224. package/dist/ai-service/policy-gate-remediation-usage.js +56 -0
  225. package/dist/ai-service/policy-gate-remediation-usage.js.map +1 -0
  226. package/dist/ai-service/prompts/summarize-destructive-migration.d.ts +12 -0
  227. package/dist/ai-service/prompts/summarize-destructive-migration.d.ts.map +1 -0
  228. package/dist/ai-service/prompts/summarize-destructive-migration.js +37 -0
  229. package/dist/ai-service/prompts/summarize-destructive-migration.js.map +1 -0
  230. package/dist/ai-service/recording/storage/session-recording-storage.js +2 -2
  231. package/dist/ai-service/recording/storage/session-recording-storage.js.map +1 -1
  232. package/dist/ai-service/security/secret-scanner-service.d.ts.map +1 -1
  233. package/dist/ai-service/security/secret-scanner-service.js +88 -23
  234. package/dist/ai-service/security/secret-scanner-service.js.map +1 -1
  235. package/dist/ai-service/skills/index.d.ts +0 -7
  236. package/dist/ai-service/skills/index.d.ts.map +1 -1
  237. package/dist/ai-service/skills/index.js +0 -9
  238. package/dist/ai-service/skills/index.js.map +1 -1
  239. package/dist/ai-service/skills/system/_registry.generated.d.ts.map +1 -1
  240. package/dist/ai-service/skills/system/_registry.generated.js +4 -0
  241. package/dist/ai-service/skills/system/_registry.generated.js.map +1 -1
  242. package/dist/ai-service/skills/system/resolve.d.ts +2 -0
  243. package/dist/ai-service/skills/system/resolve.d.ts.map +1 -0
  244. package/dist/ai-service/skills/system/resolve.js +32 -0
  245. package/dist/ai-service/skills/system/resolve.js.map +1 -0
  246. package/dist/ai-service/skills/system/superblocks-frontend/_variants/api-imports-legacy.generated.d.ts +2 -0
  247. package/dist/ai-service/skills/system/superblocks-frontend/_variants/api-imports-legacy.generated.d.ts.map +1 -0
  248. package/dist/ai-service/skills/system/superblocks-frontend/_variants/api-imports-legacy.generated.js +9 -0
  249. package/dist/ai-service/skills/system/superblocks-frontend/_variants/api-imports-legacy.generated.js.map +1 -0
  250. package/dist/ai-service/skills/system/superblocks-frontend/_variants/api-imports-sdk.generated.d.ts +2 -0
  251. package/dist/ai-service/skills/system/superblocks-frontend/_variants/api-imports-sdk.generated.d.ts.map +1 -0
  252. package/dist/ai-service/skills/system/superblocks-frontend/_variants/api-imports-sdk.generated.js +11 -0
  253. package/dist/ai-service/skills/system/superblocks-frontend/_variants/api-imports-sdk.generated.js.map +1 -0
  254. package/dist/ai-service/skills/system/superblocks-frontend/skill.generated.d.ts +1 -1
  255. package/dist/ai-service/skills/system/superblocks-frontend/skill.generated.d.ts.map +1 -1
  256. package/dist/ai-service/skills/system/superblocks-frontend/skill.generated.js +16 -24
  257. package/dist/ai-service/skills/system/superblocks-frontend/skill.generated.js.map +1 -1
  258. package/dist/ai-service/skills/system/third-party-migration/skill.generated.d.ts +1 -1
  259. package/dist/ai-service/skills/system/third-party-migration/skill.generated.d.ts.map +1 -1
  260. package/dist/ai-service/skills/system/third-party-migration/skill.generated.js +1 -1
  261. package/dist/ai-service/state-machine/clark-fsm.d.ts +11 -0
  262. package/dist/ai-service/state-machine/clark-fsm.d.ts.map +1 -1
  263. package/dist/ai-service/state-machine/clark-fsm.js +7 -1
  264. package/dist/ai-service/state-machine/clark-fsm.js.map +1 -1
  265. package/dist/ai-service/state-machine/handlers/agent-planning.d.ts.map +1 -1
  266. package/dist/ai-service/state-machine/handlers/agent-planning.js +21 -0
  267. package/dist/ai-service/state-machine/handlers/agent-planning.js.map +1 -1
  268. package/dist/ai-service/state-machine/handlers/llm-generating.d.ts.map +1 -1
  269. package/dist/ai-service/state-machine/handlers/llm-generating.js +32 -9
  270. package/dist/ai-service/state-machine/handlers/llm-generating.js.map +1 -1
  271. package/dist/ai-service/state-machine/handlers/runtime-reviewing.d.ts.map +1 -1
  272. package/dist/ai-service/state-machine/handlers/runtime-reviewing.js +21 -2
  273. package/dist/ai-service/state-machine/handlers/runtime-reviewing.js.map +1 -1
  274. package/dist/ai-service/state-machine/helpers/stable-peer.d.ts +11 -2
  275. package/dist/ai-service/state-machine/helpers/stable-peer.d.ts.map +1 -1
  276. package/dist/ai-service/state-machine/helpers/stable-peer.js +105 -18
  277. package/dist/ai-service/state-machine/helpers/stable-peer.js.map +1 -1
  278. package/dist/ai-service/state-machine/mocks.d.ts +3 -1
  279. package/dist/ai-service/state-machine/mocks.d.ts.map +1 -1
  280. package/dist/ai-service/state-machine/mocks.js +52 -13
  281. package/dist/ai-service/state-machine/mocks.js.map +1 -1
  282. package/dist/ai-service/tasks/peer-push.d.ts +3 -0
  283. package/dist/ai-service/tasks/peer-push.d.ts.map +1 -1
  284. package/dist/ai-service/tasks/peer-push.js +33 -0
  285. package/dist/ai-service/tasks/peer-push.js.map +1 -1
  286. package/dist/ai-service/util/call-peer.d.ts +2 -0
  287. package/dist/ai-service/util/call-peer.d.ts.map +1 -1
  288. package/dist/ai-service/util/call-peer.js +17 -4
  289. package/dist/ai-service/util/call-peer.js.map +1 -1
  290. package/dist/ai-service/util/line-ranges.d.ts +21 -0
  291. package/dist/ai-service/util/line-ranges.d.ts.map +1 -0
  292. package/dist/ai-service/util/line-ranges.js +36 -0
  293. package/dist/ai-service/util/line-ranges.js.map +1 -0
  294. package/dist/ai-service/util/peer-liveness.d.ts +1 -0
  295. package/dist/ai-service/util/peer-liveness.d.ts.map +1 -1
  296. package/dist/ai-service/util/peer-liveness.js +2 -0
  297. package/dist/ai-service/util/peer-liveness.js.map +1 -1
  298. package/dist/dev-server-ws-smoke-client.d.ts.map +1 -1
  299. package/dist/dev-server-ws-smoke-client.js +7 -0
  300. package/dist/dev-server-ws-smoke-client.js.map +1 -1
  301. package/dist/draft-interface.d.ts +8 -0
  302. package/dist/draft-interface.d.ts.map +1 -1
  303. package/dist/early-error-relay.d.ts +2 -0
  304. package/dist/early-error-relay.d.ts.map +1 -1
  305. package/dist/early-error-relay.js +7 -3
  306. package/dist/early-error-relay.js.map +1 -1
  307. package/dist/file-sync-vite-plugin.d.ts.map +1 -1
  308. package/dist/file-sync-vite-plugin.js +43 -15
  309. package/dist/file-sync-vite-plugin.js.map +1 -1
  310. package/dist/file-system-helpers.d.ts +2 -2
  311. package/dist/file-system-helpers.d.ts.map +1 -1
  312. package/dist/file-system-helpers.js +3 -3
  313. package/dist/file-system-helpers.js.map +1 -1
  314. package/dist/git-service/errors.d.ts +9 -0
  315. package/dist/git-service/errors.d.ts.map +1 -1
  316. package/dist/git-service/errors.js +15 -0
  317. package/dist/git-service/errors.js.map +1 -1
  318. package/dist/git-service/index.d.ts +2 -2
  319. package/dist/git-service/index.d.ts.map +1 -1
  320. package/dist/git-service/index.js +2 -2
  321. package/dist/git-service/index.js.map +1 -1
  322. package/dist/index.d.ts +1 -0
  323. package/dist/index.d.ts.map +1 -1
  324. package/dist/index.js +1 -0
  325. package/dist/index.js.map +1 -1
  326. package/dist/lock-service/activity-tracker.d.ts +5 -0
  327. package/dist/lock-service/activity-tracker.d.ts.map +1 -1
  328. package/dist/lock-service/activity-tracker.js +10 -0
  329. package/dist/lock-service/activity-tracker.js.map +1 -1
  330. package/dist/lock-service/index.d.ts +69 -4
  331. package/dist/lock-service/index.d.ts.map +1 -1
  332. package/dist/lock-service/index.js +139 -29
  333. package/dist/lock-service/index.js.map +1 -1
  334. package/dist/migration-templates/app-fullstack/client/components/ui/accordion.tsx +9 -65
  335. package/dist/migration-templates/app-fullstack/client/components/ui/area-chart.tsx +36 -156
  336. package/dist/migration-templates/app-fullstack/client/components/ui/aspect-ratio.tsx +1 -5
  337. package/dist/migration-templates/app-fullstack/client/components/ui/avatar.tsx +4 -18
  338. package/dist/migration-templates/app-fullstack/client/components/ui/badge.tsx +6 -29
  339. package/dist/migration-templates/app-fullstack/client/components/ui/bar-chart.tsx +41 -167
  340. package/dist/migration-templates/app-fullstack/client/components/ui/breadcrumb.tsx +1 -4
  341. package/dist/migration-templates/app-fullstack/client/components/ui/button.tsx +5 -64
  342. package/dist/migration-templates/app-fullstack/client/components/ui/calendar.tsx +6 -42
  343. package/dist/migration-templates/app-fullstack/client/components/ui/card.tsx +1 -4
  344. package/dist/migration-templates/app-fullstack/client/components/ui/checkbox.tsx +4 -29
  345. package/dist/migration-templates/app-fullstack/client/components/ui/date-range-picker.tsx +13 -154
  346. package/dist/migration-templates/app-fullstack/client/components/ui/dialog.tsx +7 -34
  347. package/dist/migration-templates/app-fullstack/client/components/ui/file-dropzone.tsx +12 -95
  348. package/dist/migration-templates/app-fullstack/client/components/ui/file-input.tsx +12 -73
  349. package/dist/migration-templates/app-fullstack/client/components/ui/hover-card.tsx +10 -66
  350. package/dist/migration-templates/app-fullstack/client/components/ui/icon.tsx +3 -16
  351. package/dist/migration-templates/app-fullstack/client/components/ui/image.tsx +4 -16
  352. package/dist/migration-templates/app-fullstack/client/components/ui/input.tsx +18 -159
  353. package/dist/migration-templates/app-fullstack/client/components/ui/label.tsx +1 -4
  354. package/dist/migration-templates/app-fullstack/client/components/ui/line-chart.tsx +32 -148
  355. package/dist/migration-templates/app-fullstack/client/components/ui/link.tsx +4 -24
  356. package/dist/migration-templates/app-fullstack/client/components/ui/navigation-menu.tsx +3 -24
  357. package/dist/migration-templates/app-fullstack/client/components/ui/pagination.tsx +4 -24
  358. package/dist/migration-templates/app-fullstack/client/components/ui/pie-chart.tsx +28 -142
  359. package/dist/migration-templates/app-fullstack/client/components/ui/popover.tsx +9 -61
  360. package/dist/migration-templates/app-fullstack/client/components/ui/progress.tsx +2 -11
  361. package/dist/migration-templates/app-fullstack/client/components/ui/radar-chart.tsx +37 -171
  362. package/dist/migration-templates/app-fullstack/client/components/ui/radial-chart.tsx +50 -202
  363. package/dist/migration-templates/app-fullstack/client/components/ui/select.tsx +5 -35
  364. package/dist/migration-templates/app-fullstack/client/components/ui/separator.tsx +1 -8
  365. package/dist/migration-templates/app-fullstack/client/components/ui/sheet.tsx +7 -40
  366. package/dist/migration-templates/app-fullstack/client/components/ui/sidebar.tsx +3 -35
  367. package/dist/migration-templates/app-fullstack/client/components/ui/slider.tsx +11 -95
  368. package/dist/migration-templates/app-fullstack/client/components/ui/switch.tsx +5 -32
  369. package/dist/migration-templates/app-fullstack/client/components/ui/textarea.tsx +13 -90
  370. package/dist/migration-templates/app-fullstack/client/components/ui/toggle-group.tsx +11 -75
  371. package/dist/migration-templates/app-fullstack/client/components/ui/toggle.tsx +8 -70
  372. package/dist/migration-templates/app-fullstack/client/components/ui/tooltip.tsx +9 -62
  373. package/dist/parsing/imports.d.ts +22 -0
  374. package/dist/parsing/imports.d.ts.map +1 -1
  375. package/dist/parsing/imports.js +71 -0
  376. package/dist/parsing/imports.js.map +1 -1
  377. package/dist/reconnect-guard-plugin.d.ts +53 -0
  378. package/dist/reconnect-guard-plugin.d.ts.map +1 -0
  379. package/dist/reconnect-guard-plugin.js +203 -0
  380. package/dist/reconnect-guard-plugin.js.map +1 -0
  381. package/dist/resolver.d.ts +2 -1
  382. package/dist/resolver.d.ts.map +1 -1
  383. package/dist/resolver.js +4 -6
  384. package/dist/resolver.js.map +1 -1
  385. package/dist/router-parser-metrics.d.ts +2 -1
  386. package/dist/router-parser-metrics.d.ts.map +1 -1
  387. package/dist/router-parser-metrics.js.map +1 -1
  388. package/dist/router-parser.d.ts +10 -1
  389. package/dist/router-parser.d.ts.map +1 -1
  390. package/dist/router-parser.js +306 -69
  391. package/dist/router-parser.js.map +1 -1
  392. package/dist/scan-scope-merge-metrics.d.ts +2 -0
  393. package/dist/scan-scope-merge-metrics.d.ts.map +1 -0
  394. package/dist/scan-scope-merge-metrics.js +18 -0
  395. package/dist/scan-scope-merge-metrics.js.map +1 -0
  396. package/dist/scan-scope-merge.d.ts +2 -2
  397. package/dist/scan-scope-merge.d.ts.map +1 -1
  398. package/dist/scan-scope-merge.js +64 -14
  399. package/dist/scan-scope-merge.js.map +1 -1
  400. package/dist/server-rpc/index.d.ts.map +1 -1
  401. package/dist/server-rpc/index.js +12 -2
  402. package/dist/server-rpc/index.js.map +1 -1
  403. package/dist/socket-manager.d.ts +1 -1
  404. package/dist/socket-manager.d.ts.map +1 -1
  405. package/dist/socket-manager.js +276 -15
  406. package/dist/socket-manager.js.map +1 -1
  407. package/dist/sync-service/download.js +2 -2
  408. package/dist/sync-service/download.js.map +1 -1
  409. package/dist/sync-service/index.d.ts +95 -22
  410. package/dist/sync-service/index.d.ts.map +1 -1
  411. package/dist/sync-service/index.js +402 -56
  412. package/dist/sync-service/index.js.map +1 -1
  413. package/dist/sync-service/page-reference-guard.d.ts +17 -0
  414. package/dist/sync-service/page-reference-guard.d.ts.map +1 -0
  415. package/dist/sync-service/page-reference-guard.js +170 -0
  416. package/dist/sync-service/page-reference-guard.js.map +1 -0
  417. package/dist/util.d.ts.map +1 -1
  418. package/dist/util.js +16 -2
  419. package/dist/util.js.map +1 -1
  420. package/package.json +15 -14
@@ -1,13 +1,13 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import * as fs from "node:fs/promises";
3
+ import path from "node:path";
2
4
  import { normalizePath } from "vite";
3
5
  import { SyncronizationStatus, } from "@superblocksteam/library-shared/types";
4
- import { addTracingToMethods, TracedEventEmitter, } from "@superblocksteam/shared";
5
- import { BadRequestError } from "@superblocksteam/shared";
6
- import { unwrapResponseDto } from "@superblocksteam/shared";
6
+ import { addTracingToMethods, TracedEventEmitter, BadRequestError, unwrapResponseDto, } from "@superblocksteam/shared";
7
7
  import { applyErrorHandling, } from "../errors/error-handler.js";
8
8
  import { makeServerError } from "../errors/make-server-error.js";
9
9
  import { POLICY_GATE_RUN_SYNC_BLOCKED_ERROR, policyGateRunRegistry, } from "../policy-gate-run-registry.js";
10
- import { getErrorMeta } from "../util/logger.js";
10
+ import { getErrorMeta, } from "../util/logger.js";
11
11
  import { OperationQueue } from "../util/operation-queue.js";
12
12
  import { withResolvers } from "../util/with-resolvers.js";
13
13
  import { classifyCheckpointResult } from "./checkpoint-result.js";
@@ -15,6 +15,7 @@ import { downloadApplicationDirectory } from "./download.js";
15
15
  import { hasSwapFiles, discardAllDrafts, commitAllDrafts, getDraftInfo, createDraft, generateDraftDiff, createDraftMarkerOnly, createDraftDelete, cleanUpDraftsDirectory, } from "./draft-helpers.js";
16
16
  import { recordDraftStateReport } from "./draft-state-report-metrics.js";
17
17
  import { HashCache } from "./hash-cache.js";
18
+ import { validatePageReferences } from "./page-reference-guard.js";
18
19
  import { snapshotLocalDirectory, snapshotLocalDirectoryWithDrafts, } from "./snapshot/take-snapshot.js";
19
20
  import { uploadDirectorySnapshot } from "./snapshot/upload-snapshot.js";
20
21
  const SYNC_INTERVAL_MS = 10_000;
@@ -115,6 +116,16 @@ export class SyncService extends TracedEventEmitter {
115
116
  draftStateReports = new Map();
116
117
  /** This will be defined if (and only if) a sync is currently pending */
117
118
  pendingSyncUp;
119
+ /**
120
+ * The sync most recently handed to `syncOperationQueue`.
121
+ *
122
+ * A timer clears `pendingSyncUp` before it enqueues, so an empty pending
123
+ * slot does not mean nothing newer is outstanding: a second save can arm and
124
+ * fire its own timer while the first sync waits its turn in the queue. This
125
+ * is how a sync that finds the slot empty tells "I am the newest" from "a
126
+ * newer one is already queued ahead of me".
127
+ */
128
+ lastEnqueuedSyncUp;
118
129
  handledAutosyncUploads = new WeakSet();
119
130
  latestAutosyncPreparation;
120
131
  latestAutosyncUpload;
@@ -128,6 +139,7 @@ export class SyncService extends TracedEventEmitter {
128
139
  _rpcClient;
129
140
  _logger;
130
141
  _moduleInvalidationCallback;
142
+ pageReferenceResolver;
131
143
  removeManualCommitsEnabled;
132
144
  constructor(config) {
133
145
  super(config.tracer);
@@ -149,12 +161,16 @@ export class SyncService extends TracedEventEmitter {
149
161
  operation: "syncing current directory to server",
150
162
  },
151
163
  hasLocalDraftChanges: { operation: "checking for local draft changes" },
152
- commitLocalDraftChanges: { operation: "committing local draft changes" },
164
+ commitLocalDraftChanges: {
165
+ operation: "committing local draft changes",
166
+ rethrow: true,
167
+ },
153
168
  });
154
169
  const methods = [
155
170
  "downloadDirectory",
156
171
  "uploadDirectory",
157
172
  "syncCurrentDirectoryWithDraftsNow",
173
+ "syncCurrentDirectoryWithDraftsNowIfChanged",
158
174
  "uploadDirectoryNowIfNeeded",
159
175
  "scheduleUploadDirectory",
160
176
  "performUploadDirectory",
@@ -238,6 +254,9 @@ export class SyncService extends TracedEventEmitter {
238
254
  setModuleInvalidationCallback(callback) {
239
255
  this._moduleInvalidationCallback = callback;
240
256
  }
257
+ setPageReferenceResolver(resolver) {
258
+ this.pageReferenceResolver = resolver;
259
+ }
241
260
  /**
242
261
  * Pulls the application down from the server.
243
262
  */
@@ -297,15 +316,7 @@ export class SyncService extends TracedEventEmitter {
297
316
  }
298
317
  });
299
318
  }
300
- /**
301
- * Request a sync of the local application directory to the server.
302
- *
303
- * This method is asynchronous, in the sense that it does not wait for the sync to complete. It simply schedules
304
- * a sync to happen in the future. The returned promise resolves as soon as the method has collected enough information
305
- * to schedule the sync. So the caller must wait for the returned promise to resolve before performing any filesystem
306
- * operations.
307
- * This method is also idempotent.
308
- */
319
+ /** Resolves after scheduling, not uploading. Await before filesystem changes. */
309
320
  uploadDirectory(source) {
310
321
  if (this._autoSyncPaused) {
311
322
  this.getLogger().info("[sync-service] Auto-sync paused (migration in progress), skipping upload");
@@ -367,14 +378,18 @@ export class SyncService extends TracedEventEmitter {
367
378
  * Requests an immediate sync of the local application directory to the server, if there are any pending changes.
368
379
  * If there are no pending changes, the method returns immediately.
369
380
  *
381
+ * @param options.reclaimAfterHandback - Whether this drain may take back an
382
+ * edit lock this session handed back. Pass it only from a call site a user
383
+ * action is behind; see {@link PendingSyncUpState.reclaimAfterHandback}.
370
384
  * @returns the dbfs hash of the most recent sync
371
385
  */
372
- uploadDirectoryNowIfNeeded(source) {
386
+ uploadDirectoryNowIfNeeded(source, options) {
373
387
  if (!this.pendingSyncUp) {
374
388
  this.getLogger().info("[sync-service] No pending sync, skipping");
375
389
  return Promise.resolve({
376
390
  directoryHash: this.lastSyncedDirectoryHash || "",
377
391
  commitId: undefined,
392
+ localNoPending: true,
378
393
  });
379
394
  }
380
395
  const directorySnapshot = this.pendingSyncUp.directorySnapshot;
@@ -383,7 +398,7 @@ export class SyncService extends TracedEventEmitter {
383
398
  this.latestAutosyncPreparation = preparation;
384
399
  this.latestAutosyncUpload = undefined;
385
400
  return this.fsOperation(async () => {
386
- const completedPromise = this.scheduleUploadDirectory(directorySnapshot, true, this.generationNumberSequence?.currentValue, source);
401
+ const completedPromise = this.scheduleUploadDirectory(directorySnapshot, true, this.generationNumberSequence?.currentValue, source, { reclaimAfterHandback: options?.reclaimAfterHandback ?? false });
387
402
  return await this.trackAutosyncUpload(completedPromise, source, preparation);
388
403
  });
389
404
  }
@@ -405,7 +420,50 @@ export class SyncService extends TracedEventEmitter {
405
420
  return result;
406
421
  });
407
422
  }
408
- scheduleUploadDirectory(directorySnapshot, performNow, generationNumber, source) {
423
+ /**
424
+ * Flushes the current directory for teardown without creating a redundant
425
+ * setApplicationHash request when disk already matches the last successful
426
+ * sync. Termination can race application or branch deletion; avoiding the
427
+ * no-op request prevents that expected teardown from looking like lost work,
428
+ * while a genuinely changed directory still attempts its final upload.
429
+ */
430
+ syncCurrentDirectoryWithDraftsNowIfChanged(source) {
431
+ if (this._autoSyncPaused) {
432
+ this.getLogger().info("[sync-service] Auto-sync paused (migration in progress), skipping final upload");
433
+ return Promise.resolve({
434
+ directoryHash: this.lastSyncedDirectoryHash || "",
435
+ });
436
+ }
437
+ return this.fsOperation(async () => {
438
+ const directorySnapshot = await snapshotLocalDirectoryWithDrafts(this.appRootDirPath);
439
+ // Debounce clears pendingSyncUp before performUploadDirectory runs, so
440
+ // an in-flight upload can still rewrite lastSyncedDirectoryHash after a
441
+ // skip. Drain that queue first; fs-outer/sync-inner is the same nesting
442
+ // as flipTemplate. Clear the timer before waiting so it cannot enqueue
443
+ // a stale snapshot behind this barrier; keep pendingSyncUp so skip can
444
+ // still settle its promise.
445
+ if (this.pendingSyncUp) {
446
+ clearTimeout(this.pendingSyncUp.timeoutId);
447
+ }
448
+ await this.syncOperationQueue.enqueue(async () => undefined);
449
+ if (directorySnapshot.hash === this.lastSyncedDirectoryHash) {
450
+ const result = { directoryHash: directorySnapshot.hash };
451
+ if (this.pendingSyncUp) {
452
+ this.pendingSyncUp.resolve(result);
453
+ this.pendingSyncUp = undefined;
454
+ }
455
+ this.status = SyncServiceStatus.IDLE;
456
+ this.setSynchronizationStatus(SyncronizationStatus.IN_SYNC, source);
457
+ this.getLogger().info(`[sync-service] Current directory already synchronized, skipping final upload (source=${source})`);
458
+ return result;
459
+ }
460
+ const completedPromise = this.scheduleUploadDirectory(directorySnapshot, true, this.generationNumberSequence?.currentValue, source);
461
+ const result = await completedPromise;
462
+ this.getLogger().info("[sync-service] Changed current directory synchronized during final flush");
463
+ return result;
464
+ });
465
+ }
466
+ scheduleUploadDirectory(directorySnapshot, performNow, generationNumber, source, options) {
409
467
  if (performNow && this.pendingSyncUp) {
410
468
  // There is already a sync pending, cancel it so that we can schedule a new one, to be performed immediately
411
469
  clearTimeout(this.pendingSyncUp.timeoutId);
@@ -418,7 +476,13 @@ export class SyncService extends TracedEventEmitter {
418
476
  // server classifies the commit by it.
419
477
  this.pendingSyncUp.directorySnapshot = directorySnapshot;
420
478
  this.pendingSyncUp.generationNumber = generationNumber;
421
- if (!performNow && this.removeManualCommitsEnabled) {
479
+ // A sync kept pending after a skip has no timer (see
480
+ // `keepSkippedSyncPending`), so a save joining it arms one whichever
481
+ // debounce is in force; otherwise the snapshot only leaves on an
482
+ // explicit drain.
483
+ if (!performNow &&
484
+ (this.removeManualCommitsEnabled ||
485
+ this.pendingSyncUp.timeoutId === undefined)) {
422
486
  this.armPendingSyncTimeout(this.nextAutosyncDelayMs(this.pendingSyncUp.burstStartedAt));
423
487
  }
424
488
  }
@@ -433,6 +497,8 @@ export class SyncService extends TracedEventEmitter {
433
497
  completed: completedPromise,
434
498
  directorySnapshot,
435
499
  generationNumber,
500
+ performNow,
501
+ reclaimAfterHandback: options?.reclaimAfterHandback ?? false,
436
502
  reject,
437
503
  resolve,
438
504
  source,
@@ -464,10 +530,44 @@ export class SyncService extends TracedEventEmitter {
464
530
  pendingSyncUp.timeoutId = setTimeout(() => {
465
531
  void (async () => {
466
532
  this.pendingSyncUp = undefined;
533
+ this.lastEnqueuedSyncUp = pendingSyncUp;
467
534
  await this.syncOperationQueue.enqueue(async () => {
468
535
  try {
469
- const isLockValid = await this.lockService.ensureLockForWrite();
536
+ // Only a sync a user action is behind may take back a lock this
537
+ // session handed over, and the call site says so. A debounced
538
+ // save is not evidence its user came back to the keyboard — a tab
539
+ // left open reconnects its socket by itself — and after a handback
540
+ // the lock is free, so letting one reclaim would hand the app to
541
+ // somebody who walked away (APPS-4840).
542
+ const isLockValid = await this.lockService.ensureLockForWrite({
543
+ reclaimAfterHandback: pendingSyncUp.reclaimAfterHandback,
544
+ });
470
545
  if (!isLockValid) {
546
+ if (!pendingSyncUp.performNow) {
547
+ // Nothing is waiting on this one, and not holding the lock is
548
+ // its ordinary answer: the session it belonged to handed the
549
+ // lock back, or another client holds it. Failing it took the
550
+ // sandbox down with it — nothing awaits this promise, so the
551
+ // rejection was unhandled, and the dev server turns an
552
+ // unhandled rejection into `process.exit` (ENG-6808).
553
+ //
554
+ // Skipped, not synced: the status stays LOCAL_AHEAD, the
555
+ // last-synced hash is left alone, and the snapshot stays
556
+ // pending so the next save or an explicit drain still sends
557
+ // it.
558
+ this.getLogger().info(`[sync-service] Skipping autosync, this session does not hold the edit lock (source=${pendingSyncUp.source})`);
559
+ this.keepSkippedSyncPending(pendingSyncUp);
560
+ return;
561
+ }
562
+ // Its caller hears about this one, but the local files are ahead
563
+ // either way, so the snapshot is retained on both branches. A
564
+ // drain that finds nothing pending hands back the last synced
565
+ // hash without asking the server, and a failed accept followed
566
+ // by a confident "DBFS is current" is how stale files reach an
567
+ // API run.
568
+ if (this.retentionFor(pendingSyncUp).kind === "retain") {
569
+ this.retainAsPendingSync(this.forkPendingSyncForRetry(pendingSyncUp));
570
+ }
471
571
  throw new Error("Lock is not valid");
472
572
  }
473
573
  const result = await this.performUploadDirectory(
@@ -486,6 +586,112 @@ export class SyncService extends TracedEventEmitter {
486
586
  })();
487
587
  }, timeoutDelayMs);
488
588
  }
589
+ /**
590
+ * Where the snapshot of a sync that could not have the lock belongs.
591
+ *
592
+ * Keeping it matters because the timer cleared `pendingSyncUp` before the
593
+ * sync ran: `uploadDirectoryNowIfNeeded` drains only what is pending and
594
+ * otherwise hands back the last synced hash, so an editor calling
595
+ * `ensureFilesSynced` before running an API would be told DBFS is current
596
+ * while the status says LOCAL_AHEAD, and run against stale files.
597
+ *
598
+ * Only the newest outstanding snapshot may be kept. Snapshots are
599
+ * whole-directory and `performUploadDirectory` sets the application hash to
600
+ * whatever it uploads, so parking an older one behind a newer upload reverts
601
+ * the server files to it once something drains. An empty pending slot does
602
+ * not establish newest: a save that landed while this one waited its turn
603
+ * may already have fired its own timer and queued behind it, emptying the
604
+ * slot on the way. That is what `lastEnqueuedSyncUp` answers.
605
+ */
606
+ retentionFor(enqueued) {
607
+ if (this._autoSyncPaused) {
608
+ // A migration paused autosync while this waited its turn; it wants the
609
+ // queues empty, and the pod restarts with a fresh service on success.
610
+ return { kind: "discarded" };
611
+ }
612
+ if (this.pendingSyncUp) {
613
+ // A save landed while this waited its turn and opened a new pending
614
+ // sync, which carries everything this one had.
615
+ return { kind: "superseded", by: this.pendingSyncUp };
616
+ }
617
+ if (this.lastEnqueuedSyncUp && this.lastEnqueuedSyncUp !== enqueued) {
618
+ // A save landed, armed its timer, and fired it, so the newer snapshot is
619
+ // queued behind this one rather than sitting in the pending slot. It
620
+ // still carries everything this one had, and it uploads after this
621
+ // returns, so retaining this one would leave an older snapshot drainable
622
+ // over the top of it.
623
+ return { kind: "superseded", by: this.lastEnqueuedSyncUp };
624
+ }
625
+ return { kind: "retain" };
626
+ }
627
+ /**
628
+ * Park a snapshot in the pending slot, without a timer.
629
+ *
630
+ * Re-arming one would retry every interval for as long as the lock stays
631
+ * handed back, which for a pod nobody came back to is until it is reaped.
632
+ * Instead the snapshot leaves on the next save, which arms a timer as it
633
+ * joins (see `scheduleUploadDirectory`), or on an explicit drain. The burst
634
+ * clock restarts so that save gets an ordinary debounce rather than firing
635
+ * at once because the original burst's max wait has long passed.
636
+ */
637
+ retainAsPendingSync(toRetain) {
638
+ toRetain.timeoutId = undefined;
639
+ toRetain.burstStartedAt = performance.now();
640
+ this.pendingSyncUp = toRetain;
641
+ }
642
+ /**
643
+ * Retain a skipped sync's snapshot and settle its promise, which for a
644
+ * superseded one is the newer sync's outcome.
645
+ *
646
+ * Only for the skip path: a sync that fails settles its own promise through
647
+ * the caller's `catch`, and must not also adopt another sync's outcome.
648
+ */
649
+ keepSkippedSyncPending(skipped) {
650
+ const retention = this.retentionFor(skipped);
651
+ if (retention.kind === "superseded") {
652
+ retention.by.completed.then(skipped.resolve, skipped.reject);
653
+ return;
654
+ }
655
+ if (retention.kind === "retain") {
656
+ this.retainAsPendingSync(skipped);
657
+ }
658
+ }
659
+ /**
660
+ * Copy a sync whose promise is already spoken for, so its snapshot can be
661
+ * retained without that promise coming along.
662
+ *
663
+ * A failed sync's promise is rejected for its caller, and a retained sync's
664
+ * is handed to whatever picks the snapshot up next: a save coalescing into
665
+ * it is returned this promise, and the final flush resolves it. Both would
666
+ * be no-ops against the settled one.
667
+ *
668
+ * The copy is a plain trailing snapshot. Nothing awaits it, and the claim to
669
+ * reclaim a handed-back lock belonged to the user action that failed, not to
670
+ * whoever happens to save next.
671
+ */
672
+ forkPendingSyncForRetry(settled) {
673
+ const { promise: completed, resolve, reject, } = withResolvers();
674
+ // Whoever picks this up brings its own reporting, through
675
+ // `trackAutosyncUpload`. Until then the promise needs a handler of its
676
+ // own: an unhandled rejection is what the dev server turns into
677
+ // `process.exit`, which is the failure this whole path exists to avoid.
678
+ void completed.catch((error) => {
679
+ logSyncError(this.getLogger(), error, "[sync-service] Retained snapshot failed to synchronize");
680
+ });
681
+ return {
682
+ burstStartedAt: settled.burstStartedAt,
683
+ completed,
684
+ directorySnapshot: settled.directorySnapshot,
685
+ generationNumber: settled.generationNumber,
686
+ performNow: false,
687
+ reclaimAfterHandback: false,
688
+ reject,
689
+ resolve,
690
+ source: settled.source,
691
+ syncTime: settled.syncTime,
692
+ timeoutId: undefined,
693
+ };
694
+ }
489
695
  cancelPendingSync() {
490
696
  if (this.pendingSyncUp) {
491
697
  clearTimeout(this.pendingSyncUp.timeoutId);
@@ -519,11 +725,7 @@ export class SyncService extends TracedEventEmitter {
519
725
  this._autoSyncPaused = false;
520
726
  this.status = SyncServiceStatus.IDLE;
521
727
  }
522
- /**
523
- * Takes a snapshot of an arbitrary local directory and uploads it to DBFS.
524
- * Used by the git deploy flow to upload a worktree snapshot for a main branch commit.
525
- * Does NOT create any internal commits — the caller is responsible for that.
526
- */
728
+ /** Uploads a directory snapshot to DBFS without creating an internal commit. */
527
729
  async uploadSnapshotToDbfs(localDirectoryPath) {
528
730
  const snapshot = await snapshotLocalDirectory(localDirectoryPath);
529
731
  await this._rpcClient.call(async (rpcClient) => {
@@ -645,6 +847,7 @@ export class SyncService extends TracedEventEmitter {
645
847
  return {
646
848
  checkpointSkipped,
647
849
  commitId,
850
+ degradedMode,
648
851
  directoryHash: directorySnapshot.hash,
649
852
  };
650
853
  }
@@ -678,7 +881,14 @@ export class SyncService extends TracedEventEmitter {
678
881
  return this.fsOperation(async () => {
679
882
  const directorySnapshot = await snapshotLocalDirectoryWithDrafts(this.appRootDirPath);
680
883
  return this.syncOperationQueue.enqueue(async () => {
681
- const isLockValid = await this.lockService.ensureLockForWrite();
884
+ // A migration runs off the editor's migrate banner (`/sb-migrate-to-
885
+ // fullstack`), so a user is behind this write and it may take back a
886
+ // lock this session handed over. It fails loudly when it cannot: a
887
+ // migration half applied in silence is worse than one that reports it
888
+ // could not run.
889
+ const isLockValid = await this.lockService.ensureLockForWrite({
890
+ reclaimAfterHandback: true,
891
+ });
682
892
  if (!isLockValid) {
683
893
  throw new Error("Lock is not valid");
684
894
  }
@@ -716,7 +926,14 @@ export class SyncService extends TracedEventEmitter {
716
926
  return this.fsOperation(async () => {
717
927
  const directorySnapshot = await snapshotLocalDirectoryWithDrafts(this.appRootDirPath);
718
928
  return this.syncOperationQueue.enqueue(async () => {
719
- const isLockValid = await this.lockService.ensureLockForWrite();
929
+ // A migration runs off the editor's migrate banner (`/sb-migrate-to-
930
+ // fullstack`), so a user is behind this write and it may take back a
931
+ // lock this session handed over. It fails loudly when it cannot: a
932
+ // migration half applied in silence is worse than one that reports it
933
+ // could not run.
934
+ const isLockValid = await this.lockService.ensureLockForWrite({
935
+ reclaimAfterHandback: true,
936
+ });
720
937
  if (!isLockValid) {
721
938
  throw new Error("Lock is not valid");
722
939
  }
@@ -730,11 +947,6 @@ export class SyncService extends TracedEventEmitter {
730
947
  });
731
948
  });
732
949
  }
733
- /**
734
- * Resolve datasource integration IDs (UUIDs) to plugin IDs.
735
- * Used by migration preflight to classify whether UUID-backed steps map to
736
- * supported SDK integration types.
737
- */
738
950
  async getIntegrationPluginIds(integrationIds) {
739
951
  const uniqueIntegrationIds = [...new Set(integrationIds)];
740
952
  if (uniqueIntegrationIds.length === 0) {
@@ -802,29 +1014,146 @@ export class SyncService extends TracedEventEmitter {
802
1014
  * @returns A promise that resolves when the draft edit or addition is created.
803
1015
  */
804
1016
  async createDraftFile(filePath, draftContent) {
805
- // Normalize path for cross-platform compatibility (Windows)
806
- const normalizedFilePath = normalizePath(filePath);
807
- this.getLogger().info(`[sync-service] Creating draft file: ${normalizedFilePath}`);
1017
+ await this.applyDraftChanges([{ filePath, content: draftContent }]);
1018
+ }
1019
+ async applyDraftChanges(changes) {
1020
+ const normalized = changes.map((change) => {
1021
+ const filePath = normalizePath(change.filePath);
1022
+ const fullPath = path.resolve(this.appRootDirPath, filePath);
1023
+ const relativePath = path.relative(this.appRootDirPath, fullPath);
1024
+ if (relativePath === ".." ||
1025
+ relativePath.startsWith(`..${path.sep}`) ||
1026
+ path.isAbsolute(relativePath)) {
1027
+ throw new Error(`Cannot change a path outside the app root: ${filePath}`);
1028
+ }
1029
+ if (change.content === undefined && relativePath === "") {
1030
+ throw new Error("Cannot delete the app root");
1031
+ }
1032
+ return { ...change, filePath: normalizePath(relativePath) };
1033
+ });
808
1034
  await this.fsOperation(async () => {
809
- const path = await import("node:path");
810
- const fullPath = path.join(this.appRootDirPath, normalizedFilePath);
811
- const type = await createDraft(fullPath, draftContent, this.appRootDirPath);
812
- if (type === "edit") {
813
- this.getLogger().info(`[sync-service] Draft edit created for: ${normalizedFilePath}`);
1035
+ const requestedPaths = new Set();
1036
+ for (const change of normalized) {
1037
+ if (requestedPaths.has(change.filePath)) {
1038
+ throw new Error(`Cannot change ${change.filePath} more than once in the same batch`);
1039
+ }
1040
+ requestedPaths.add(change.filePath);
1041
+ }
1042
+ const expanded = [];
1043
+ const directories = new Set();
1044
+ const realAppRootDirPath = await fs.realpath(this.appRootDirPath);
1045
+ for (const change of normalized) {
1046
+ if (change.content !== undefined) {
1047
+ expanded.push(change);
1048
+ continue;
1049
+ }
1050
+ const fullPath = path.resolve(this.appRootDirPath, change.filePath);
1051
+ let stats;
1052
+ try {
1053
+ stats = await fs.lstat(fullPath);
1054
+ }
1055
+ catch (error) {
1056
+ if (error instanceof Error &&
1057
+ "code" in error &&
1058
+ error.code === "ENOENT") {
1059
+ expanded.push(change);
1060
+ continue;
1061
+ }
1062
+ throw error;
1063
+ }
1064
+ if (stats.isSymbolicLink()) {
1065
+ throw new Error(`Cannot delete symbolic link: ${change.filePath}`);
1066
+ }
1067
+ if (!stats.isFile() && !stats.isDirectory()) {
1068
+ throw new Error(`Cannot delete unsupported file type: ${change.filePath}`);
1069
+ }
1070
+ // `lstat` only describes the last segment of the path. A parent
1071
+ // directory can still be a symbolic link pointing somewhere else, and
1072
+ // deleting through it would remove a file outside the app root.
1073
+ const realPath = await fs.realpath(fullPath);
1074
+ const realRelativePath = path.relative(realAppRootDirPath, realPath);
1075
+ if (realRelativePath === ".." ||
1076
+ realRelativePath.startsWith(`..${path.sep}`) ||
1077
+ path.isAbsolute(realRelativePath)) {
1078
+ throw new Error(`Cannot delete a path outside the app root: ${change.filePath}`);
1079
+ }
1080
+ if (!stats.isDirectory()) {
1081
+ expanded.push(change);
1082
+ continue;
1083
+ }
1084
+ const entries = await fs.readdir(fullPath, {
1085
+ recursive: true,
1086
+ withFileTypes: true,
1087
+ });
1088
+ const offender = entries.find((entry) => !entry.isFile() && !entry.isDirectory());
1089
+ if (offender) {
1090
+ const offenderPath = normalizePath(path.relative(this.appRootDirPath, path.resolve(offender.parentPath, offender.name)));
1091
+ const kind = offender.isSymbolicLink()
1092
+ ? "a symbolic link"
1093
+ : "an unsupported file type";
1094
+ throw new Error(`Cannot delete ${change.filePath}: it contains ${kind} at ${offenderPath}`);
1095
+ }
1096
+ directories.add(fullPath);
1097
+ for (const entry of entries) {
1098
+ const entryPath = path.resolve(entry.parentPath, entry.name);
1099
+ if (entry.isDirectory())
1100
+ directories.add(entryPath);
1101
+ if (entry.isFile()) {
1102
+ expanded.push({
1103
+ filePath: normalizePath(path.relative(this.appRootDirPath, entryPath)),
1104
+ });
1105
+ }
1106
+ }
814
1107
  }
815
- else {
816
- this.getLogger().info(`[sync-service] Draft addition created for: ${normalizedFilePath}`);
1108
+ const seen = new Set();
1109
+ for (const change of expanded) {
1110
+ if (seen.has(change.filePath)) {
1111
+ throw new Error(`Cannot change ${change.filePath} more than once in the same batch`);
1112
+ }
1113
+ seen.add(change.filePath);
817
1114
  }
818
- // Invalidate Vite's module graph for this file
819
- if (this._moduleInvalidationCallback) {
820
- this._moduleInvalidationCallback(normalizedFilePath);
821
- this.getLogger().info(`[sync-service] Triggered Vite module invalidation for: ${normalizedFilePath}`);
1115
+ for (const change of expanded) {
1116
+ if (change.content === undefined)
1117
+ continue;
1118
+ const fullPath = path.resolve(this.appRootDirPath, change.filePath);
1119
+ if ([...directories].some((directory) => fullPath === directory ||
1120
+ fullPath.startsWith(`${directory}${path.sep}`))) {
1121
+ throw new Error(`Cannot write ${change.filePath} because its directory is being deleted`);
1122
+ }
1123
+ }
1124
+ if (this.pageReferenceResolver) {
1125
+ await validatePageReferences({
1126
+ appRoot: this.appRootDirPath,
1127
+ changes: expanded,
1128
+ resolver: this.pageReferenceResolver,
1129
+ });
1130
+ }
1131
+ let mutationStarted = false;
1132
+ try {
1133
+ for (const change of expanded) {
1134
+ const fullPath = path.join(this.appRootDirPath, change.filePath);
1135
+ mutationStarted = true;
1136
+ if (change.content === undefined) {
1137
+ await createDraftDelete(fullPath, this.appRootDirPath);
1138
+ this.getLogger().info(`[sync-service] Draft deletion created for: ${change.filePath}`);
1139
+ }
1140
+ else {
1141
+ const type = await createDraft(fullPath, change.content, this.appRootDirPath);
1142
+ this.getLogger().info(`[sync-service] Draft ${type === "edit" ? "edit" : "addition"} created for: ${change.filePath}`);
1143
+ this._moduleInvalidationCallback?.(change.filePath);
1144
+ }
1145
+ }
1146
+ for (const directory of [...directories].sort((left, right) => right.length - left.length)) {
1147
+ mutationStarted = true;
1148
+ await fs.rmdir(directory);
1149
+ }
1150
+ }
1151
+ finally {
1152
+ if (mutationStarted && (await hasSwapFiles(this.appRootDirPath))) {
1153
+ this.notifyDraftStateChanged(true);
1154
+ }
822
1155
  }
823
1156
  });
824
- this.getLogger().info("[sync-service] Draft file created", {
825
- filePath: normalizedFilePath,
826
- });
827
- this.notifyDraftStateChanged(true);
828
1157
  }
829
1158
  /**
830
1159
  * Creates the marker file only for a draft edit or addition.
@@ -847,11 +1176,7 @@ export class SyncService extends TracedEventEmitter {
847
1176
  * @returns A promise that resolves when the draft deletion is created.
848
1177
  */
849
1178
  async createDraftDelete(filePath) {
850
- await this.fsOperation(async () => {
851
- const fullPath = `${this.appRootDirPath}/${filePath}`;
852
- await createDraftDelete(fullPath, this.appRootDirPath);
853
- });
854
- this.notifyDraftStateChanged(true);
1179
+ await this.applyDraftChanges([{ filePath }]);
855
1180
  }
856
1181
  /**
857
1182
  * Discards all local draft changes by restoring original files and removing draft additions.
@@ -866,7 +1191,11 @@ export class SyncService extends TracedEventEmitter {
866
1191
  this.getLogger().info("[sync-service] Local draft changes discarded");
867
1192
  });
868
1193
  await this.uploadDirectory(source);
869
- await this.uploadDirectoryNowIfNeeded(source);
1194
+ // A reject is a click in the editor, so it may take back a lock this
1195
+ // session handed back (ENG-6808).
1196
+ await this.uploadDirectoryNowIfNeeded(source, {
1197
+ reclaimAfterHandback: true,
1198
+ });
870
1199
  this.notifyDraftStateChanged(false);
871
1200
  }
872
1201
  /**
@@ -889,6 +1218,11 @@ export class SyncService extends TracedEventEmitter {
889
1218
  });
890
1219
  const uploadResult = await this.uploadCommittedDraftForSource(source, checkpointAttemptId);
891
1220
  const result = uploadResult.result;
1221
+ // commitAllDrafts above already removed the local markers, so there is no
1222
+ // draft on disk regardless of how the checkpoint turned out. Report that
1223
+ // before classifying the result: a server left on isDraft=true blocks git
1224
+ // sync until the next reconnect. The rejection below still happens before
1225
+ // the RPC caller advances the chat or waits on this report.
892
1226
  this.notifyDraftStateChanged(false);
893
1227
  this.getLogger().info(`[sync-service] commitLocalDraftChanges upload result commitId=${result.commitId ?? "none"}`);
894
1228
  if (source === "ai:accept") {
@@ -900,6 +1234,13 @@ export class SyncService extends TracedEventEmitter {
900
1234
  commitId: result.commitId,
901
1235
  outcome: this.aiAcceptCheckpointOutcomeFromLog(checkpointLog),
902
1236
  });
1237
+ // A missing commitId is a failed checkpoint unless this drain never
1238
+ // reached the server (localNoPending). Hash-only server responses,
1239
+ // including blocked Superblocks users, have neither commitId nor a
1240
+ // by-design skip reason and must reject.
1241
+ if (checkpointLog.level === "warn" && !result.localNoPending) {
1242
+ throw new Error("AI accept checkpoint upload completed without a commit ID");
1243
+ }
903
1244
  }
904
1245
  if (result.commitId) {
905
1246
  return { commitId: result.commitId };
@@ -913,7 +1254,12 @@ export class SyncService extends TracedEventEmitter {
913
1254
  }
914
1255
  async uploadCommittedDraft(source) {
915
1256
  await this.uploadDirectory(source);
916
- return await this.uploadDirectoryNowIfNeeded(source);
1257
+ // An accept is a click in the editor, so it may take back a lock this
1258
+ // session handed back (ENG-6808). This is the write the reported session
1259
+ // lost: the user came back and hit Accept against a lock nobody held.
1260
+ return await this.uploadDirectoryNowIfNeeded(source, {
1261
+ reclaimAfterHandback: true,
1262
+ });
917
1263
  }
918
1264
  async uploadCommittedDraftWithRetry(source, checkpointAttemptId) {
919
1265
  for (let attempt = 1;; attempt++) {