@songsid/agend 2.1.12 → 2.2.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/README.md +2 -2
  2. package/README.zh-TW.md +2 -2
  3. package/dist/auth/one-time-code.d.ts +19 -0
  4. package/dist/auth/one-time-code.js +49 -0
  5. package/dist/auth/one-time-code.js.map +1 -0
  6. package/dist/auth-api.d.ts +52 -0
  7. package/dist/auth-api.js +346 -0
  8. package/dist/auth-api.js.map +1 -0
  9. package/dist/backend/claude-code.d.ts +4 -0
  10. package/dist/backend/claude-code.js +97 -0
  11. package/dist/backend/claude-code.js.map +1 -1
  12. package/dist/backend/codex.js +59 -30
  13. package/dist/backend/codex.js.map +1 -1
  14. package/dist/backend/kiro-agent.d.ts +66 -0
  15. package/dist/backend/kiro-agent.js +302 -0
  16. package/dist/backend/kiro-agent.js.map +1 -0
  17. package/dist/backend/kiro-identity.d.ts +80 -0
  18. package/dist/backend/kiro-identity.js +537 -0
  19. package/dist/backend/kiro-identity.js.map +1 -0
  20. package/dist/backend/kiro-v2-store-worker.d.ts +1 -0
  21. package/dist/backend/kiro-v2-store-worker.js +87 -0
  22. package/dist/backend/kiro-v2-store-worker.js.map +1 -0
  23. package/dist/backend/kiro-v2-store.d.ts +54 -0
  24. package/dist/backend/kiro-v2-store.js +131 -0
  25. package/dist/backend/kiro-v2-store.js.map +1 -0
  26. package/dist/backend/kiro-v3-identity.d.ts +5 -0
  27. package/dist/backend/kiro-v3-identity.js +22 -0
  28. package/dist/backend/kiro-v3-identity.js.map +1 -1
  29. package/dist/backend/kiro.d.ts +83 -7
  30. package/dist/backend/kiro.js +418 -91
  31. package/dist/backend/kiro.js.map +1 -1
  32. package/dist/backend/types.d.ts +45 -3
  33. package/dist/backend/types.js.map +1 -1
  34. package/dist/channel/adapters/discord.d.ts +1 -0
  35. package/dist/channel/adapters/discord.js +29 -10
  36. package/dist/channel/adapters/discord.js.map +1 -1
  37. package/dist/channel/adapters/telegram.d.ts +1 -0
  38. package/dist/channel/adapters/telegram.js +13 -1
  39. package/dist/channel/adapters/telegram.js.map +1 -1
  40. package/dist/channel/types.d.ts +9 -1
  41. package/dist/classic-channel-manager.d.ts +18 -10
  42. package/dist/classic-channel-manager.js +45 -18
  43. package/dist/classic-channel-manager.js.map +1 -1
  44. package/dist/cli.js +225 -127
  45. package/dist/cli.js.map +1 -1
  46. package/dist/command-table.d.ts +5 -3
  47. package/dist/command-table.js +29 -27
  48. package/dist/command-table.js.map +1 -1
  49. package/dist/config-validator.js +46 -0
  50. package/dist/config-validator.js.map +1 -1
  51. package/dist/config.js +12 -0
  52. package/dist/config.js.map +1 -1
  53. package/dist/connection-secrets.d.ts +6 -0
  54. package/dist/connection-secrets.js +4 -0
  55. package/dist/connection-secrets.js.map +1 -1
  56. package/dist/daemon.d.ts +89 -4
  57. package/dist/daemon.js +661 -77
  58. package/dist/daemon.js.map +1 -1
  59. package/dist/darwin-memory.d.ts +6 -1
  60. package/dist/darwin-memory.js +17 -6
  61. package/dist/darwin-memory.js.map +1 -1
  62. package/dist/delivery-consumed-watch.d.ts +85 -0
  63. package/dist/delivery-consumed-watch.js +244 -0
  64. package/dist/delivery-consumed-watch.js.map +1 -0
  65. package/dist/delivery-outbox.d.ts +61 -0
  66. package/dist/delivery-outbox.js +120 -2
  67. package/dist/delivery-outbox.js.map +1 -1
  68. package/dist/delivery-reconciliation.d.ts +14 -5
  69. package/dist/delivery-reconciliation.js +27 -4
  70. package/dist/delivery-reconciliation.js.map +1 -1
  71. package/dist/delivery-reconciliation.test.js +35 -1
  72. package/dist/delivery-reconciliation.test.js.map +1 -1
  73. package/dist/event-loop-watch.d.ts +28 -0
  74. package/dist/event-loop-watch.js +75 -3
  75. package/dist/event-loop-watch.js.map +1 -1
  76. package/dist/fleet-context.d.ts +22 -2
  77. package/dist/fleet-level-config.d.ts +2 -2
  78. package/dist/fleet-level-config.js +6 -1
  79. package/dist/fleet-level-config.js.map +1 -1
  80. package/dist/fleet-manager.d.ts +293 -12
  81. package/dist/fleet-manager.js +2364 -449
  82. package/dist/fleet-manager.js.map +1 -1
  83. package/dist/host-memory.d.ts +4 -0
  84. package/dist/host-memory.js.map +1 -1
  85. package/dist/instance-config-impact.js +5 -0
  86. package/dist/instance-config-impact.js.map +1 -1
  87. package/dist/instance-lifecycle.d.ts +10 -5
  88. package/dist/instance-lifecycle.js +162 -53
  89. package/dist/instance-lifecycle.js.map +1 -1
  90. package/dist/locale.js +150 -16
  91. package/dist/locale.js.map +1 -1
  92. package/dist/login-controller.d.ts +2 -0
  93. package/dist/login-controller.js +17 -5
  94. package/dist/login-controller.js.map +1 -1
  95. package/dist/memory-pressure.d.ts +7 -8
  96. package/dist/memory-pressure.js +76 -15
  97. package/dist/memory-pressure.js.map +1 -1
  98. package/dist/monotonic-budget.d.ts +2 -0
  99. package/dist/monotonic-budget.js +28 -0
  100. package/dist/monotonic-budget.js.map +1 -0
  101. package/dist/needs-you-hub.d.ts +114 -0
  102. package/dist/needs-you-hub.js +370 -0
  103. package/dist/needs-you-hub.js.map +1 -0
  104. package/dist/needs-you-live.d.ts +116 -0
  105. package/dist/needs-you-live.js +228 -0
  106. package/dist/needs-you-live.js.map +1 -0
  107. package/dist/needs-you.d.ts +132 -0
  108. package/dist/needs-you.js +164 -0
  109. package/dist/needs-you.js.map +1 -0
  110. package/dist/node-version-guard.d.ts +30 -0
  111. package/dist/node-version-guard.js +50 -0
  112. package/dist/node-version-guard.js.map +1 -0
  113. package/dist/operator-control-paths.d.ts +4 -0
  114. package/dist/operator-control-paths.js +24 -0
  115. package/dist/operator-control-paths.js.map +1 -0
  116. package/dist/outbound-handlers.d.ts +2 -0
  117. package/dist/outbound-handlers.js +9 -2
  118. package/dist/outbound-handlers.js.map +1 -1
  119. package/dist/outbound-schemas.d.ts +5 -5
  120. package/dist/profile-control.js +4 -20
  121. package/dist/profile-control.js.map +1 -1
  122. package/dist/public-web-gateway.d.ts +18 -0
  123. package/dist/public-web-gateway.js +141 -0
  124. package/dist/public-web-gateway.js.map +1 -0
  125. package/dist/public-web-link.d.ts +53 -0
  126. package/dist/public-web-link.js +175 -0
  127. package/dist/public-web-link.js.map +1 -0
  128. package/dist/quickstart-api.d.ts +8 -1
  129. package/dist/quickstart-api.js +86 -48
  130. package/dist/quickstart-api.js.map +1 -1
  131. package/dist/quickstart.d.ts +1 -0
  132. package/dist/quickstart.js +6 -2
  133. package/dist/quickstart.js.map +1 -1
  134. package/dist/scheduler/db.d.ts +7 -1
  135. package/dist/scheduler/db.js +38 -0
  136. package/dist/scheduler/db.js.map +1 -1
  137. package/dist/scheduler/index.d.ts +1 -1
  138. package/dist/scheduler/retry-label.d.ts +11 -0
  139. package/dist/scheduler/retry-label.js +24 -0
  140. package/dist/scheduler/retry-label.js.map +1 -0
  141. package/dist/scheduler/scheduler.d.ts +55 -2
  142. package/dist/scheduler/scheduler.js +220 -8
  143. package/dist/scheduler/scheduler.js.map +1 -1
  144. package/dist/scheduler/types.d.ts +22 -0
  145. package/dist/scheduler/types.js.map +1 -1
  146. package/dist/secret-store.d.ts +5 -0
  147. package/dist/secret-store.js +20 -0
  148. package/dist/secret-store.js.map +1 -1
  149. package/dist/service-activation.d.ts +110 -0
  150. package/dist/service-activation.js +302 -0
  151. package/dist/service-activation.js.map +1 -0
  152. package/dist/service-installer.d.ts +7 -0
  153. package/dist/service-installer.js +7 -4
  154. package/dist/service-installer.js.map +1 -1
  155. package/dist/service-restart-selection.d.ts +6 -5
  156. package/dist/service-restart-selection.js +6 -5
  157. package/dist/service-restart-selection.js.map +1 -1
  158. package/dist/settings-api.d.ts +10 -2
  159. package/dist/settings-api.js +175 -72
  160. package/dist/settings-api.js.map +1 -1
  161. package/dist/settings-baseline.d.ts +20 -0
  162. package/dist/settings-baseline.js +104 -0
  163. package/dist/settings-baseline.js.map +1 -0
  164. package/dist/settings-change.d.ts +21 -0
  165. package/dist/settings-change.js +133 -0
  166. package/dist/settings-change.js.map +1 -0
  167. package/dist/settings-confirmation.d.ts +105 -0
  168. package/dist/settings-confirmation.js +232 -0
  169. package/dist/settings-confirmation.js.map +1 -0
  170. package/dist/settings-control.d.ts +41 -0
  171. package/dist/settings-control.js +294 -0
  172. package/dist/settings-control.js.map +1 -0
  173. package/dist/settings-effect.d.ts +24 -0
  174. package/dist/settings-effect.js +169 -0
  175. package/dist/settings-effect.js.map +1 -0
  176. package/dist/settings-http-confirmation.d.ts +29 -0
  177. package/dist/settings-http-confirmation.js +123 -0
  178. package/dist/settings-http-confirmation.js.map +1 -0
  179. package/dist/settings-instance-patch.d.ts +4 -0
  180. package/dist/settings-instance-patch.js +26 -0
  181. package/dist/settings-instance-patch.js.map +1 -0
  182. package/dist/settings-request-capability.d.ts +16 -0
  183. package/dist/settings-request-capability.js +32 -0
  184. package/dist/settings-request-capability.js.map +1 -0
  185. package/dist/settings-transaction.d.ts +65 -0
  186. package/dist/settings-transaction.js +223 -0
  187. package/dist/settings-transaction.js.map +1 -0
  188. package/dist/setup-auth.d.ts +10 -18
  189. package/dist/setup-auth.js +14 -38
  190. package/dist/setup-auth.js.map +1 -1
  191. package/dist/setup-form.d.ts +1 -1
  192. package/dist/setup-form.js +75 -18
  193. package/dist/setup-form.js.map +1 -1
  194. package/dist/setup-guide.d.ts +13 -0
  195. package/dist/setup-guide.js +25 -0
  196. package/dist/setup-guide.js.map +1 -0
  197. package/dist/setup-host.d.ts +16 -0
  198. package/dist/setup-host.js +197 -32
  199. package/dist/setup-host.js.map +1 -1
  200. package/dist/setup-wizard.js +3 -0
  201. package/dist/setup-wizard.js.map +1 -1
  202. package/dist/slash-authz.d.ts +17 -3
  203. package/dist/slash-authz.js +5 -0
  204. package/dist/slash-authz.js.map +1 -1
  205. package/dist/spawn-gate.js +3 -3
  206. package/dist/spawn-gate.js.map +1 -1
  207. package/dist/statusline-watcher.d.ts +5 -0
  208. package/dist/statusline-watcher.js +2 -0
  209. package/dist/statusline-watcher.js.map +1 -1
  210. package/dist/steer-capability.d.ts +6 -0
  211. package/dist/steer-capability.js +8 -0
  212. package/dist/steer-capability.js.map +1 -1
  213. package/dist/sync-work-attribution.d.ts +14 -0
  214. package/dist/sync-work-attribution.js +43 -8
  215. package/dist/sync-work-attribution.js.map +1 -1
  216. package/dist/tips.js +10 -5
  217. package/dist/tips.js.map +1 -1
  218. package/dist/tmux-control.d.ts +35 -5
  219. package/dist/tmux-control.js +291 -57
  220. package/dist/tmux-control.js.map +1 -1
  221. package/dist/tmux-manager.d.ts +20 -6
  222. package/dist/tmux-manager.js +90 -47
  223. package/dist/tmux-manager.js.map +1 -1
  224. package/dist/tmux-read.d.ts +59 -0
  225. package/dist/tmux-read.js +213 -0
  226. package/dist/tmux-read.js.map +1 -0
  227. package/dist/topic-commands.d.ts +21 -10
  228. package/dist/topic-commands.js +191 -58
  229. package/dist/topic-commands.js.map +1 -1
  230. package/dist/transcript-delta-reader.js +2 -1
  231. package/dist/transcript-delta-reader.js.map +1 -1
  232. package/dist/tunnel/cloudflared-install.d.ts +2 -0
  233. package/dist/tunnel/cloudflared-install.js +1 -1
  234. package/dist/tunnel/cloudflared-install.js.map +1 -1
  235. package/dist/tunnel/purpose-lane.d.ts +14 -0
  236. package/dist/tunnel/purpose-lane.js +43 -0
  237. package/dist/tunnel/purpose-lane.js.map +1 -0
  238. package/dist/turn-reply-guard.d.ts +2 -0
  239. package/dist/turn-reply-guard.js +10 -1
  240. package/dist/turn-reply-guard.js.map +1 -1
  241. package/dist/types.d.ts +33 -0
  242. package/dist/ui/app.html +18 -0
  243. package/dist/ui/chat-render.js +478 -0
  244. package/dist/ui/chat-store.js +289 -0
  245. package/dist/ui/chat-strings.js +69 -0
  246. package/dist/ui/chat-thread.js +209 -0
  247. package/dist/ui/chat-tour.js +116 -0
  248. package/dist/ui/fleet-org.js +142 -0
  249. package/dist/ui/panel-chat.js +418 -0
  250. package/dist/ui/panel-fleet.js +413 -0
  251. package/dist/ui/panel-needs.js +202 -0
  252. package/dist/ui/panel-settings.js +750 -0
  253. package/dist/ui/preview.js +266 -0
  254. package/dist/ui/settings-apply.js +239 -0
  255. package/dist/ui/settings-confirm.js +186 -0
  256. package/dist/ui/settings-dialogs.js +546 -0
  257. package/dist/ui/settings-model.js +277 -0
  258. package/dist/ui/settings-strings.js +652 -0
  259. package/dist/ui/settings-wizard.js +140 -0
  260. package/dist/ui/shared/agend-auth.js +93 -0
  261. package/dist/ui/shared/app-ctx.js +80 -0
  262. package/dist/ui/shared/app-html.js +9 -0
  263. package/dist/ui/shared/app-i18n.js +95 -0
  264. package/dist/ui/shared/app-nav.js +73 -0
  265. package/dist/ui/shared/app-needs.js +114 -0
  266. package/dist/ui/shared/app-route.js +73 -0
  267. package/dist/ui/shared/app-session.js +66 -0
  268. package/dist/ui/shared/app-shell.js +344 -0
  269. package/dist/ui/shared/app-store.js +51 -0
  270. package/dist/ui/shared/app-stream.js +201 -0
  271. package/dist/ui/shared/app.css +676 -0
  272. package/dist/ui/shared/app.js +88 -0
  273. package/dist/ui/shared/fonts/OFL.txt +92 -0
  274. package/dist/ui/shared/fonts/README.md +21 -0
  275. package/dist/ui/shared/fonts/inter.woff2 +0 -0
  276. package/dist/ui/shared/htm.module.js +1 -0
  277. package/dist/ui/shared/panel-view.js +655 -0
  278. package/dist/ui/shared/preact-hooks.module.js +2 -0
  279. package/dist/ui/shared/preact.module.js +2 -0
  280. package/dist/ui/shared/signin.js +130 -0
  281. package/dist/ui/shared/theme.js +29 -0
  282. package/dist/ui/shared/tokens.css +153 -0
  283. package/dist/ui/shared/ui-confirm.js +43 -0
  284. package/dist/ui/shared/ui-dialog.js +45 -0
  285. package/dist/ui/shared/ui-icons.js +61 -0
  286. package/dist/ui/shared/ui-menu.js +39 -0
  287. package/dist/ui/shared/ui-states.js +20 -0
  288. package/dist/ui/shared/ui-toast.js +18 -0
  289. package/dist/ui/shared/vendor/htm.LICENSE +202 -0
  290. package/dist/ui/shared/vendor/preact.LICENSE +21 -0
  291. package/dist/ui/shared/vendor/vendor.json +39 -0
  292. package/dist/ui/shared/view-strings.js +309 -0
  293. package/dist/ui/signin.html +51 -0
  294. package/dist/update-check.d.ts +6 -2
  295. package/dist/update-check.js +12 -3
  296. package/dist/update-check.js.map +1 -1
  297. package/dist/update-install.d.ts +71 -0
  298. package/dist/update-install.js +162 -0
  299. package/dist/update-install.js.map +1 -0
  300. package/dist/upgrade-notices.d.ts +17 -0
  301. package/dist/upgrade-notices.js +93 -0
  302. package/dist/upgrade-notices.js.map +1 -0
  303. package/dist/usage/statusline-usage.d.ts +2 -0
  304. package/dist/usage/statusline-usage.js +14 -10
  305. package/dist/usage/statusline-usage.js.map +1 -1
  306. package/dist/view-api.d.ts +6 -1
  307. package/dist/view-api.js +62 -51
  308. package/dist/view-api.js.map +1 -1
  309. package/dist/web-api.d.ts +71 -14
  310. package/dist/web-api.js +492 -77
  311. package/dist/web-api.js.map +1 -1
  312. package/dist/web-auth.d.ts +71 -27
  313. package/dist/web-auth.js +169 -69
  314. package/dist/web-auth.js.map +1 -1
  315. package/dist/web-body.d.ts +3 -0
  316. package/dist/web-body.js +55 -0
  317. package/dist/web-body.js.map +1 -0
  318. package/dist/web-channel-echo.d.ts +9 -0
  319. package/dist/web-channel-echo.js +55 -0
  320. package/dist/web-channel-echo.js.map +1 -0
  321. package/dist/web-chat-history.d.ts +125 -0
  322. package/dist/web-chat-history.js +184 -0
  323. package/dist/web-chat-history.js.map +1 -0
  324. package/dist/web-continuation.d.ts +7 -0
  325. package/dist/web-continuation.js +22 -0
  326. package/dist/web-continuation.js.map +1 -0
  327. package/dist/web-host-guard.d.ts +34 -0
  328. package/dist/web-host-guard.js +57 -1
  329. package/dist/web-host-guard.js.map +1 -1
  330. package/dist/web-login.d.ts +72 -0
  331. package/dist/web-login.js +132 -0
  332. package/dist/web-login.js.map +1 -0
  333. package/dist/web-org.d.ts +55 -0
  334. package/dist/web-org.js +67 -0
  335. package/dist/web-org.js.map +1 -0
  336. package/dist/web-preview.d.ts +77 -0
  337. package/dist/web-preview.js +240 -0
  338. package/dist/web-preview.js.map +1 -0
  339. package/dist/web-request-context.d.ts +10 -0
  340. package/dist/web-request-context.js +13 -0
  341. package/dist/web-request-context.js.map +1 -0
  342. package/dist/web-session.d.ts +167 -0
  343. package/dist/web-session.js +389 -0
  344. package/dist/web-session.js.map +1 -0
  345. package/dist/web-shell-routes.d.ts +59 -0
  346. package/dist/web-shell-routes.js +96 -0
  347. package/dist/web-shell-routes.js.map +1 -0
  348. package/dist/web-upload.d.ts +183 -0
  349. package/dist/web-upload.js +468 -0
  350. package/dist/web-upload.js.map +1 -0
  351. package/dist/website-tips-renderer.d.ts +2 -2
  352. package/dist/website-tips-renderer.js +3 -3
  353. package/dist/website-tips-renderer.js.map +1 -1
  354. package/package.json +8 -5
  355. package/scripts/preinstall-guard.cjs +36 -0
  356. package/dist/ui/dashboard.html +0 -774
  357. package/dist/ui/settings.html +0 -2165
  358. package/dist/ui/view.html +0 -1139
@@ -1,7 +1,41 @@
1
+ import { normalizeSettingsInstancePatch, removesInstanceOverride } from "./settings-instance-patch.js";
2
+ import { settingsRequestExecution, settingsWrite } from "./settings-request-capability.js";
3
+ import { noteSettingsWrite, settingsUndo, undoSettingsPaths } from "./settings-transaction.js";
4
+ import { readBoundedWebBody } from "./web-body.js";
5
+ import { validPublicLinkPatch } from "./public-web-link.js";
6
+ import { permitWebContinuation } from "./web-continuation.js";
7
+ /**
8
+ * Settings Web API (`/settings`) — CRUD over fleet.yaml + classicBot.yaml.
9
+ *
10
+ * GET /settings → static page
11
+ * GET /api/settings/fleet → fleet config as JSON
12
+ * GET /api/settings/classic → classicBot.yaml as JSON
13
+ * PUT /api/settings/fleet/defaults → merge fleet defaults
14
+ * PUT /api/settings/fleet/channels → replace channels[]
15
+ * POST /api/settings/fleet/instances/:name → create instance
16
+ * PATCH/api/settings/fleet/instances/:name → merge into instance
17
+ * DELETE /api/settings/fleet/instances/:name → remove instance
18
+ * PUT /api/settings/classic/defaults → merge classic defaults
19
+ * PATCH/api/settings/classic/channels/:key → update + restart a Classic channel
20
+ * POST /api/settings/reload → SIGHUP hot-reload
21
+ * POST /api/settings/instances/:name/pause → manually pause a running instance
22
+ * POST /api/settings/instances/:name/wake → manually wake a paused instance
23
+ * GET /api/settings/status-emojis → status-emoji keys, built-ins, pickable sets
24
+ * POST /api/settings/status-emojis/preview → resolve a map exactly as the bots will
25
+ * GET /api/settings/status-emojis/guild-emojis?channel=<id>[&refresh=1] → a Discord bot's servers' emojis
26
+ *
27
+ * Auth: all routes require the web.token — enforced by the global web-token gate
28
+ * in fleet-manager BEFORE this handler runs (settings paths are not exempt), so
29
+ * no per-route auth is repeated here.
30
+ *
31
+ * Writes are validated first (config-validator): any error → 400 and nothing is
32
+ * written; warnings are non-blocking and returned alongside the result.
33
+ */
34
+ import { serveAppShell } from "./web-api.js";
35
+ import { isSettingsPage } from "./web-shell-routes.js";
1
36
  import { readFileSync, writeFileSync, existsSync, renameSync, unlinkSync, statSync } from "node:fs";
2
37
  import { randomBytes } from "node:crypto";
3
- import { join, dirname } from "node:path";
4
- import { fileURLToPath } from "node:url";
38
+ import { join } from "node:path";
5
39
  import yaml from "js-yaml";
6
40
  import { KNOWN_BACKENDS, validateFleetConfig, validateClassicBotConfig } from "./config-validator.js";
7
41
  import { clearPausedMarker } from "./pause-marker.js";
@@ -11,7 +45,6 @@ import { handleQuickstartRequest } from "./quickstart-api.js";
11
45
  import { requestSessionBinding, } from "./connection-secrets.js";
12
46
  import { providerRegistryEnvKeys, isReservedProviderEnvKey } from "./provider-secret-registry.js";
13
47
  import { STATUS_EMOJI_CONFIG_KEYS, STATUS_EMOJI_SUGGESTIONS, TELEGRAM_REACTION_EMOJIS, builtinStatusEmojis, customEmojiValue, emojiImageUrl, previewStatusEmojis, } from "./status-emojis.js";
14
- const __dirname = dirname(fileURLToPath(import.meta.url));
15
48
  function json(res, code, body, noStore = false) {
16
49
  res.writeHead(code, {
17
50
  "Content-Type": "application/json",
@@ -20,21 +53,7 @@ function json(res, code, body, noStore = false) {
20
53
  res.end(JSON.stringify(body));
21
54
  }
22
55
  function readBody(req, maxBytes) {
23
- return new Promise((resolve, reject) => {
24
- const chunks = [];
25
- let size = 0;
26
- req.on("data", (c) => {
27
- size += c.length;
28
- if (size > maxBytes) {
29
- reject(new Error("payload too large"));
30
- req.destroy();
31
- return;
32
- }
33
- chunks.push(c);
34
- });
35
- req.on("end", () => resolve(Buffer.concat(chunks)));
36
- req.on("error", reject);
37
- });
56
+ return readBoundedWebBody(req, maxBytes);
38
57
  }
39
58
  /**
40
59
  * Channel/group identifiers are opaque provider coordinates. JSON numbers
@@ -52,7 +71,7 @@ function normalizeChannelIdValue(value, path) {
52
71
  return { ok: false, error: `${path} must be a string (or a safe integer)` };
53
72
  }
54
73
  export function isSettingsPath(path) {
55
- return path === "/settings" || path.startsWith("/api/settings/");
74
+ return isSettingsPage(path) || path.startsWith("/api/settings/");
56
75
  }
57
76
  const classicPath = (ctx) => join(ctx.dataDir, "classicBot.yaml");
58
77
  class ClassicConfigParseError extends Error {
@@ -78,6 +97,7 @@ function writeClassicAtomic(ctx, classic) {
78
97
  const target = classicPath(ctx);
79
98
  const temp = `${target}.tmp-${process.pid}-${randomBytes(6).toString("hex")}`;
80
99
  const mode = existsSync(target) ? statSync(target).mode : 0o600;
100
+ noteSettingsWrite(target, readClassic(ctx), classic);
81
101
  try {
82
102
  writeFileSync(temp, yaml.dump(classic, { lineWidth: -1 }), { encoding: "utf-8", mode });
83
103
  renameSync(temp, target);
@@ -118,16 +138,13 @@ export function handleSettingsRequest(req, res, url, ctx) {
118
138
  if (!isSettingsPath(path))
119
139
  return false;
120
140
  const method = req.method ?? "GET";
121
- // ── Static page ──
122
- if (method === "GET" && path === "/settings") {
123
- try {
124
- const html = readFileSync(join(__dirname, "ui", "settings.html"), "utf-8");
125
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
126
- res.end(html);
127
- }
128
- catch {
129
- json(res, 500, { error: "settings.html not found" });
141
+ // ── The page: the app shell, which mounts the Settings panel (#1408 step 3) ──
142
+ if (isSettingsPage(path)) {
143
+ if (method !== "GET" && method !== "HEAD") {
144
+ json(res, 405, { error: "Method not allowed" });
145
+ return true;
130
146
  }
147
+ serveAppShell(req, res, ctx, "full");
131
148
  return true;
132
149
  }
133
150
  // ── Reads ──
@@ -174,6 +191,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
174
191
  }
175
192
  if (method === "POST" && path === "/api/settings/status-emojis/preview") {
176
193
  readBody(req, 64 * 1024).then(buf => {
194
+ if (!permitWebContinuation(req, res, ctx))
195
+ return;
177
196
  let body;
178
197
  try {
179
198
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -279,6 +298,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
279
298
  }
280
299
  const specId = decodeURIComponent(providerSecretVerifyMatch[1]);
281
300
  readBody(req, 16 * 1024).then(async (buf) => {
301
+ if (!permitWebContinuation(req, res, ctx))
302
+ return;
282
303
  let body;
283
304
  try {
284
305
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -314,6 +335,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
314
335
  }
315
336
  const specId = decodeURIComponent(providerSecretApplyMatch[1]);
316
337
  readBody(req, 16 * 1024).then(buf => {
338
+ if (!permitWebContinuation(req, res, ctx))
339
+ return;
317
340
  let body;
318
341
  try {
319
342
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -328,7 +351,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
328
351
  return json(res, 400, { error: "verification_id required" }, true);
329
352
  if (!/^[A-Za-z0-9_.:-]{8,128}$/.test(key))
330
353
  return json(res, 400, { error: "Idempotency-Key required" }, true);
331
- const result = ctx.startProviderSecretApply({ specId, verificationId, sessionBinding: requestSessionBinding(req), idempotencyKey: key });
354
+ const result = ctx.startProviderSecretApply({ specId, verificationId, sessionBinding: requestSessionBinding(req), idempotencyKey: key, execution: settingsRequestExecution(req) });
332
355
  if ("error" in result)
333
356
  return json(res, 422, { ok: false, error: "provider secret apply rejected" }, true);
334
357
  if ("busy" in result)
@@ -364,6 +387,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
364
387
  }
365
388
  const connectionId = decodeURIComponent(secretVerifyMatch[1]);
366
389
  readBody(req, 16 * 1024).then(async (buf) => {
390
+ if (!permitWebContinuation(req, res, ctx))
391
+ return;
367
392
  let body;
368
393
  try {
369
394
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -407,6 +432,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
407
432
  }
408
433
  const connectionId = decodeURIComponent(secretApplyMatch[1]);
409
434
  readBody(req, 16 * 1024).then(buf => {
435
+ if (!permitWebContinuation(req, res, ctx))
436
+ return;
410
437
  let body;
411
438
  try {
412
439
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -425,7 +452,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
425
452
  connectionId,
426
453
  verificationId,
427
454
  sessionBinding: requestSessionBinding(req),
428
- idempotencyKey: key,
455
+ idempotencyKey: key, execution: settingsRequestExecution(req),
429
456
  });
430
457
  if ("error" in result)
431
458
  return json(res, 422, { ok: false, error: "secret apply rejected" }, true);
@@ -468,6 +495,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
468
495
  }
469
496
  const connectionId = decodeURIComponent(bindingVerifyMatch[1]);
470
497
  readBody(req, 16 * 1024).then(async (buf) => {
498
+ if (!permitWebContinuation(req, res, ctx))
499
+ return;
471
500
  let body;
472
501
  try {
473
502
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -511,6 +540,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
511
540
  }
512
541
  const connectionId = decodeURIComponent(bindingApplyMatch[1]);
513
542
  readBody(req, 16 * 1024).then(buf => {
543
+ if (!permitWebContinuation(req, res, ctx))
544
+ return;
514
545
  let body;
515
546
  try {
516
547
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -526,7 +557,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
526
557
  if (!/^[A-Za-z0-9_.:-]{8,128}$/.test(key))
527
558
  return json(res, 400, { error: "Idempotency-Key required" }, true);
528
559
  const result = ctx.startConnectionBindingApply({
529
- connectionId, verificationId, sessionBinding: requestSessionBinding(req), idempotencyKey: key,
560
+ connectionId, verificationId, sessionBinding: requestSessionBinding(req), idempotencyKey: key, execution: settingsRequestExecution(req),
530
561
  });
531
562
  if ("error" in result)
532
563
  return json(res, 422, { ok: false, error: "binding apply rejected" }, true);
@@ -572,6 +603,53 @@ export function handleSettingsRequest(req, res, url, ctx) {
572
603
  .catch(err => json(res, 409, { error: err.message }));
573
604
  return true;
574
605
  }
606
+ // One hot web toggle; an unrelated Settings edit never materializes defaults.
607
+ if (method === "PUT" && path === "/api/settings/fleet/web") {
608
+ if (!cfg) {
609
+ json(res, 503, { error: "fleet not loaded" });
610
+ return true;
611
+ }
612
+ readBody(req, 4096).then(buf => {
613
+ if (!permitWebContinuation(req, res, ctx))
614
+ return;
615
+ let body;
616
+ try {
617
+ body = JSON.parse(buf.toString("utf-8"));
618
+ }
619
+ catch {
620
+ return json(res, 400, { error: "invalid JSON" });
621
+ }
622
+ if (!body || typeof body !== "object" || Array.isArray(body)
623
+ || Object.keys(body).some(key => key !== "echo_to_channel" && key !== "public_link"))
624
+ return json(res, 400, { error: "unknown web field" });
625
+ const patch = body;
626
+ if (patch.echo_to_channel !== undefined && typeof patch.echo_to_channel !== "boolean")
627
+ return json(res, 400, { error: "echo_to_channel must be a boolean" });
628
+ if (patch.public_link !== undefined && !validPublicLinkPatch(patch.public_link))
629
+ return json(res, 400, { error: "invalid public_link settings" });
630
+ const changes = [];
631
+ const web = { ...cfg.web };
632
+ if (typeof patch.echo_to_channel === "boolean") {
633
+ web.echo_to_channel = patch.echo_to_channel;
634
+ changes.push({ path: ["web", "echo_to_channel"], value: patch.echo_to_channel });
635
+ }
636
+ if (validPublicLinkPatch(patch.public_link)) {
637
+ if (Object.keys(patch.public_link).length)
638
+ web.public_link = { ...web.public_link, ...patch.public_link };
639
+ for (const [key, value] of Object.entries(patch.public_link))
640
+ changes.push({ path: ["web", "public_link", key], value });
641
+ }
642
+ if (!changes.length)
643
+ return json(res, 200, { ok: true });
644
+ const before = validateFleetConfig(cfg);
645
+ const after = validateFleetConfig({ ...cfg, web });
646
+ if (rejectIfWorse(res, before, after))
647
+ return;
648
+ settingsWrite(req, () => { cfg.web = web; ctx.saveFleetConfig(changes); });
649
+ json(res, 200, { ok: true, warnings: saveWarnings(before, after) });
650
+ }).catch(() => json(res, 400, { error: "bad request" }));
651
+ return true;
652
+ }
575
653
  // ── Fleet defaults ──
576
654
  if (method === "PUT" && path === "/api/settings/fleet/defaults") {
577
655
  if (!cfg) {
@@ -579,6 +657,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
579
657
  return true;
580
658
  }
581
659
  readBody(req, 512 * 1024).then(buf => {
660
+ if (!permitWebContinuation(req, res, ctx))
661
+ return;
582
662
  let body;
583
663
  try {
584
664
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -597,8 +677,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
597
677
  const after = validateFleetConfig({ ...cfg, defaults: merged });
598
678
  if (rejectIfWorse(res, before, after))
599
679
  return;
600
- cfg.defaults = merged;
601
- ctx.saveFleetConfig();
680
+ settingsWrite(req, () => { cfg.defaults = merged; ctx.saveFleetConfig(); });
602
681
  json(res, 200, { ok: true, warnings: saveWarnings(before, after) });
603
682
  }).catch(() => json(res, 400, { error: "bad request" }));
604
683
  return true;
@@ -610,6 +689,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
610
689
  return true;
611
690
  }
612
691
  readBody(req, 512 * 1024).then(buf => {
692
+ if (!permitWebContinuation(req, res, ctx))
693
+ return;
613
694
  let body;
614
695
  try {
615
696
  body = JSON.parse(buf.toString("utf-8") || "[]");
@@ -680,9 +761,11 @@ export function handleSettingsRequest(req, res, url, ctx) {
680
761
  const after = validateFleetConfig(next);
681
762
  if (rejectIfWorse(res, before, after))
682
763
  return;
683
- cfg.channels = normalizedBody;
684
- delete cfg.channel;
685
- ctx.saveFleetConfig();
764
+ settingsWrite(req, () => {
765
+ cfg.channels = normalizedBody;
766
+ delete cfg.channel;
767
+ ctx.saveFleetConfig();
768
+ });
686
769
  json(res, 200, { ok: true, warnings: saveWarnings(before, after) });
687
770
  }).catch(() => json(res, 400, { error: "bad request" }));
688
771
  return true;
@@ -690,6 +773,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
690
773
  // ── Classic defaults ──
691
774
  if (method === "PUT" && path === "/api/settings/classic/defaults") {
692
775
  readBody(req, 512 * 1024).then(buf => {
776
+ if (!permitWebContinuation(req, res, ctx))
777
+ return;
693
778
  let body;
694
779
  try {
695
780
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -717,7 +802,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
717
802
  return;
718
803
  classic.defaults = merged;
719
804
  try {
720
- writeClassicAtomic(ctx, classic);
805
+ settingsWrite(req, () => writeClassicAtomic(ctx, classic));
721
806
  }
722
807
  catch (err) {
723
808
  ctx.logger.warn({ err }, "settings: failed to atomically update classicBot.yaml");
@@ -744,6 +829,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
744
829
  return true;
745
830
  }
746
831
  readBody(req, 512 * 1024).then(async (buf) => {
832
+ if (!permitWebContinuation(req, res, ctx))
833
+ return;
747
834
  let body;
748
835
  try {
749
836
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -761,6 +848,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
761
848
  "context_lines",
762
849
  "tool_progress",
763
850
  "reply_completion_guard",
851
+ "web_echo",
764
852
  ]);
765
853
  const unknown = Object.keys(body).filter(field => !allowed.has(field));
766
854
  if (unknown.length)
@@ -787,6 +875,9 @@ export function handleSettingsRequest(req, res, url, ctx) {
787
875
  && typeof body.reply_completion_guard !== "boolean") {
788
876
  return json(res, 400, { error: "reply_completion_guard must be a boolean" });
789
877
  }
878
+ if (body.web_echo !== undefined && typeof body.web_echo !== "boolean") {
879
+ return json(res, 400, { error: "web_echo must be a boolean" });
880
+ }
790
881
  let classic;
791
882
  try {
792
883
  classic = readClassic(ctx);
@@ -797,7 +888,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
797
888
  const channels = classic.channels;
798
889
  if (!channels || typeof channels !== "object" || Array.isArray(channels))
799
890
  return json(res, 404, { error: "classic channel not found" });
800
- const current = channels[key];
891
+ const current = Object.hasOwn(channels, key) ? channels[key] : undefined;
801
892
  if (!current || typeof current !== "object" || Array.isArray(current))
802
893
  return json(res, 404, { error: "classic channel not found" });
803
894
  const previous = structuredClone(classic);
@@ -816,29 +907,42 @@ export function handleSettingsRequest(req, res, url, ctx) {
816
907
  if (rejectIfWorse(res, before, after))
817
908
  return;
818
909
  try {
819
- writeClassicAtomic(ctx, classic);
910
+ settingsWrite(req, () => writeClassicAtomic(ctx, classic), false);
820
911
  }
821
912
  catch (err) {
822
913
  ctx.logger.warn({ err, key }, "settings: failed to atomically update classic channel");
823
914
  return json(res, 500, { error: "failed to write classicBot.yaml" });
824
915
  }
916
+ const undo = settingsUndo(classicPath(ctx), previous, classic, Object.keys(body).map(field => ["channels", key, field]));
917
+ const execution = settingsRequestExecution(req);
825
918
  const instanceName = typeof merged.instanceName === "string" ? merged.instanceName : undefined;
919
+ const restore = instanceName ? ctx.captureClassicSettingsRestoration?.(instanceName, Object.keys(body), execution) : undefined;
826
920
  try {
827
921
  if (instanceName && ctx.restartClassicInstanceFromSettings) {
828
- await ctx.restartClassicInstanceFromSettings(instanceName, Object.keys(body));
922
+ if (execution)
923
+ await ctx.restartClassicInstanceFromSettings(instanceName, Object.keys(body), execution);
924
+ else
925
+ await ctx.restartClassicInstanceFromSettings(instanceName, Object.keys(body));
926
+ execution?.assert();
829
927
  }
830
928
  }
831
929
  catch (err) {
832
930
  // Keep disk and runtime consistent if the requested restart fails.
833
931
  try {
834
- writeClassicAtomic(ctx, previous);
932
+ const retained = undoSettingsPaths(classicPath(ctx), readClassic(ctx), undo);
933
+ writeClassicAtomic(ctx, retained.value);
934
+ if (retained.conflicts)
935
+ throw new Error("concurrent Classic edit retained");
835
936
  }
836
937
  catch (rollbackErr) {
837
938
  ctx.logger.error({ err: rollbackErr, key }, "settings: failed to roll back classic channel update");
838
939
  }
839
940
  if (instanceName && ctx.restartClassicInstanceFromSettings) {
840
941
  try {
841
- await ctx.restartClassicInstanceFromSettings(instanceName, Object.keys(body));
942
+ if (restore)
943
+ await restore();
944
+ else
945
+ await ctx.restartClassicInstanceFromSettings(instanceName, Object.keys(body));
842
946
  }
843
947
  catch (recoveryErr) {
844
948
  ctx.logger.error({ err: recoveryErr, key, instanceName }, "settings: failed to restore classic instance after rollback");
@@ -847,6 +951,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
847
951
  ctx.logger.warn({ err, key, instanceName }, "settings: classic channel restart failed; config rolled back");
848
952
  return json(res, 409, { error: `classic instance restart failed: ${err.message}` });
849
953
  }
954
+ execution?.complete();
850
955
  ctx.logger.info({ key, instanceName }, "settings: updated classic channel");
851
956
  const hotOnly = Object.keys(body).length > 0
852
957
  && Object.keys(body).every(field => CLASSIC_HOT_CONFIG_KEYS.has(field));
@@ -871,6 +976,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
871
976
  return true;
872
977
  }
873
978
  readBody(req, 64 * 1024).then(buf => {
979
+ if (!permitWebContinuation(req, res, ctx))
980
+ return;
874
981
  let body = {};
875
982
  try {
876
983
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -919,6 +1026,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
919
1026
  return true;
920
1027
  }
921
1028
  readBody(req, 64 * 1024).then(async (buf) => {
1029
+ if (!permitWebContinuation(req, res, ctx))
1030
+ return;
922
1031
  let body = {};
923
1032
  try {
924
1033
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -983,9 +1092,6 @@ export function handleSettingsRequest(req, res, url, ctx) {
983
1092
  }
984
1093
  // ── Instances (create / patch / delete) ──
985
1094
  const validName = (n) => !!n && /^[^\\/\x00]+$/.test(n);
986
- const nullableInstanceOverrides = new Set(["model", "auto_pause_after", "hang_detector", "agent_mode", "tool_set", "tool_progress", "reply_completion_guard", "log_level", "lightweight", "model_failover", "display_name", "status_emojis", "cross_instance_visibility"]);
987
- const removesInstanceOverride = (key, value) => nullableInstanceOverrides.has(key)
988
- && (value === null || (key === "model" && typeof value === "string" && value.trim() === ""));
989
1095
  const rawInstancePatches = (name, patch) => {
990
1096
  const changes = [];
991
1097
  for (const [key, value] of Object.entries(patch)) {
@@ -1008,33 +1114,24 @@ export function handleSettingsRequest(req, res, url, ctx) {
1008
1114
  }
1009
1115
  const base = (exists ? cfg.instances[name] : {});
1010
1116
  const patch = body;
1011
- const mergedInst = { ...base, ...patch };
1012
- if (patch.hang_detector && typeof patch.hang_detector === "object" && !Array.isArray(patch.hang_detector)) {
1013
- const hangPatch = patch.hang_detector;
1014
- const mergedHang = { ...(base.hang_detector ?? {}), ...hangPatch };
1015
- // Nested null removes only the timeout override while preserving any
1016
- // independently configured `enabled` override.
1017
- if (hangPatch.timeout_minutes === null)
1018
- delete mergedHang.timeout_minutes;
1019
- if (Object.keys(mergedHang).length)
1020
- mergedInst.hang_detector = mergedHang;
1021
- else
1022
- delete mergedInst.hang_detector;
1117
+ let mergedInst;
1118
+ try {
1119
+ mergedInst = normalizeSettingsInstancePatch(base, patch);
1023
1120
  }
1024
- // JSON has no `undefined`; null is the PATCH sentinel for removing an
1025
- // optional override so the instance inherits the fleet default again.
1026
- for (const key of nullableInstanceOverrides) {
1027
- if (removesInstanceOverride(key, patch[key]))
1028
- delete mergedInst[key];
1121
+ catch {
1122
+ json(res, 400, { error: "unsupported_instance_null" });
1123
+ return;
1029
1124
  }
1030
1125
  const before = validateFleetConfig(cfg);
1031
1126
  const after = validateFleetConfig({ ...cfg, instances: { ...cfg.instances, [name]: mergedInst } });
1032
1127
  if (rejectIfWorse(res, before, after))
1033
1128
  return;
1034
- cfg.instances[name] = mergedInst;
1035
- if (!exists)
1036
- clearPausedMarker(join(ctx.dataDir, "instances", name));
1037
- ctx.saveFleetConfig(rawInstancePatches(name, patch));
1129
+ settingsWrite(req, () => {
1130
+ Object.defineProperty(cfg.instances, name, { value: mergedInst, writable: true, enumerable: true, configurable: true });
1131
+ if (!exists)
1132
+ clearPausedMarker(join(ctx.dataDir, "instances", name));
1133
+ ctx.saveFleetConfig(rawInstancePatches(name, patch));
1134
+ });
1038
1135
  json(res, 200, { ok: true, warnings: saveWarnings(before, after) });
1039
1136
  };
1040
1137
  // POST /api/settings/fleet/instances — create, name taken from the body.
@@ -1044,6 +1141,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
1044
1141
  return true;
1045
1142
  }
1046
1143
  readBody(req, 512 * 1024).then(buf => {
1144
+ if (!permitWebContinuation(req, res, ctx))
1145
+ return;
1047
1146
  let body;
1048
1147
  try {
1049
1148
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -1056,7 +1155,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
1056
1155
  const name = typeof body.name === "string" ? body.name.trim() : "";
1057
1156
  if (!validName(name))
1058
1157
  return json(res, 400, { error: "missing or invalid instance name (provide `name` in the body)" });
1059
- if (cfg.instances[name])
1158
+ if (Object.hasOwn(cfg.instances, name))
1060
1159
  return json(res, 409, { error: "instance already exists" });
1061
1160
  const { name: _n, ...instBody } = body;
1062
1161
  commitInstance(name, false, instBody);
@@ -1076,19 +1175,21 @@ export function handleSettingsRequest(req, res, url, ctx) {
1076
1175
  return true;
1077
1176
  }
1078
1177
  if (method === "DELETE") {
1079
- if (!cfg.instances[name]) {
1178
+ if (!Object.hasOwn(cfg.instances, name)) {
1080
1179
  json(res, 404, { error: "instance not found" });
1081
1180
  return true;
1082
1181
  }
1083
1182
  // DELETE never blocks on validation; surface any resulting warnings.
1084
- delete cfg.instances[name];
1085
- clearPausedMarker(join(ctx.dataDir, "instances", name));
1086
- ctx.saveFleetConfig();
1183
+ settingsWrite(req, () => {
1184
+ delete cfg.instances[name];
1185
+ clearPausedMarker(join(ctx.dataDir, "instances", name));
1186
+ ctx.saveFleetConfig();
1187
+ });
1087
1188
  json(res, 200, { ok: true, warnings: validateFleetConfig(cfg).warnings });
1088
1189
  return true;
1089
1190
  }
1090
1191
  if (method === "POST" || method === "PATCH") {
1091
- const exists = !!cfg.instances[name];
1192
+ const exists = Object.hasOwn(cfg.instances, name);
1092
1193
  if (method === "POST" && exists) {
1093
1194
  json(res, 409, { error: "instance already exists" });
1094
1195
  return true;
@@ -1098,6 +1199,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
1098
1199
  return true;
1099
1200
  }
1100
1201
  readBody(req, 512 * 1024).then(buf => {
1202
+ if (!permitWebContinuation(req, res, ctx))
1203
+ return;
1101
1204
  let body;
1102
1205
  try {
1103
1206
  body = JSON.parse(buf.toString("utf-8") || "{}");