@maudecode/talaria-web 1.18.0 → 1.20.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 (774) hide show
  1. package/_release.json +8 -8
  2. package/dist/api/auth-raw.js +16 -16
  3. package/dist/api/auth-raw.js.map +1 -1
  4. package/dist/api/automation-raw.d.ts +2 -2
  5. package/dist/api/automation-raw.js +93 -38
  6. package/dist/api/automation-raw.js.map +1 -1
  7. package/dist/api/automation-router.d.ts +99 -17
  8. package/dist/api/automation-router.js +22 -14
  9. package/dist/api/automation-router.js.map +1 -1
  10. package/dist/api/chat-router.d.ts +1433 -37
  11. package/dist/api/chat-router.js +205 -90
  12. package/dist/api/chat-router.js.map +1 -1
  13. package/dist/api/git-router.d.ts +6 -2
  14. package/dist/api/git-router.js +30 -30
  15. package/dist/api/git-router.js.map +1 -1
  16. package/dist/api/raw-routes.d.ts +3 -3
  17. package/dist/api/raw-routes.js +76 -62
  18. package/dist/api/raw-routes.js.map +1 -1
  19. package/dist/api/router.d.ts +5 -5
  20. package/dist/api/router.js +22 -22
  21. package/dist/api/router.js.map +1 -1
  22. package/dist/api/session-visibility.d.ts +4 -4
  23. package/dist/api/session-visibility.js +26 -16
  24. package/dist/api/session-visibility.js.map +1 -1
  25. package/dist/api/sessions-router.d.ts +2635 -151
  26. package/dist/api/sessions-router.js +193 -177
  27. package/dist/api/sessions-router.js.map +1 -1
  28. package/dist/api/settings-router.d.ts +450 -11
  29. package/dist/api/settings-router.js +102 -49
  30. package/dist/api/settings-router.js.map +1 -1
  31. package/dist/api/sse-routes.d.ts +2 -2
  32. package/dist/api/sse-routes.js +102 -80
  33. package/dist/api/sse-routes.js.map +1 -1
  34. package/dist/api/tools-raw.d.ts +5 -0
  35. package/dist/api/tools-raw.js +58 -33
  36. package/dist/api/tools-raw.js.map +1 -1
  37. package/dist/api/tools-router.d.ts +963 -109
  38. package/dist/api/tools-router.js +426 -72
  39. package/dist/api/tools-router.js.map +1 -1
  40. package/dist/api/transcribe-stream.d.ts +16 -0
  41. package/dist/api/transcribe-stream.js +172 -0
  42. package/dist/api/transcribe-stream.js.map +1 -0
  43. package/dist/app.d.ts +4 -1
  44. package/dist/app.js +80 -57
  45. package/dist/app.js.map +1 -1
  46. package/dist/auth/crypto.d.ts +3 -0
  47. package/dist/auth/crypto.js +18 -0
  48. package/dist/auth/crypto.js.map +1 -1
  49. package/dist/auth/gate.js +14 -9
  50. package/dist/auth/gate.js.map +1 -1
  51. package/dist/auth/passkeys.d.ts +15 -14
  52. package/dist/auth/passkeys.js +89 -91
  53. package/dist/auth/passkeys.js.map +1 -1
  54. package/dist/auth/store.d.ts +17 -7
  55. package/dist/auth/store.js +93 -52
  56. package/dist/auth/store.js.map +1 -1
  57. package/dist/bin/talaria-web.js +12 -81
  58. package/dist/bin/talaria-web.js.map +1 -1
  59. package/dist/cli/ctl.js +8 -2
  60. package/dist/cli/ctl.js.map +1 -1
  61. package/dist/cli/dotenv.d.ts +5 -0
  62. package/dist/cli/dotenv.js +14 -2
  63. package/dist/cli/dotenv.js.map +1 -1
  64. package/dist/cli/launcher.js +5 -5
  65. package/dist/cli/launcher.js.map +1 -1
  66. package/dist/cli/main.d.ts +1 -0
  67. package/dist/cli/main.js +90 -0
  68. package/dist/cli/main.js.map +1 -0
  69. package/dist/cli/supervise.d.ts +24 -0
  70. package/dist/cli/supervise.js +147 -6
  71. package/dist/cli/supervise.js.map +1 -1
  72. package/dist/config/agent-config.d.ts +37 -11
  73. package/dist/config/agent-config.js +146 -64
  74. package/dist/config/agent-config.js.map +1 -1
  75. package/dist/config.d.ts +2 -0
  76. package/dist/config.js +14 -5
  77. package/dist/config.js.map +1 -1
  78. package/dist/fs/atomic.d.ts +4 -0
  79. package/dist/fs/atomic.js +19 -2
  80. package/dist/fs/atomic.js.map +1 -1
  81. package/dist/fs/stat.d.ts +7 -0
  82. package/dist/fs/stat.js +27 -0
  83. package/dist/fs/stat.js.map +1 -0
  84. package/dist/http/context.d.ts +28 -9
  85. package/dist/http/context.js +77 -25
  86. package/dist/http/context.js.map +1 -1
  87. package/dist/onboarding.d.ts +2 -2
  88. package/dist/onboarding.js +10 -8
  89. package/dist/onboarding.js.map +1 -1
  90. package/dist/profiles/profiles.d.ts +12 -4
  91. package/dist/profiles/profiles.js +78 -30
  92. package/dist/profiles/profiles.js.map +1 -1
  93. package/dist/projects.d.ts +15 -6
  94. package/dist/projects.js +55 -47
  95. package/dist/projects.js.map +1 -1
  96. package/dist/providers/catalog.d.ts +85 -14
  97. package/dist/providers/catalog.js +502 -146
  98. package/dist/providers/catalog.js.map +1 -1
  99. package/dist/providers/env-file.d.ts +10 -3
  100. package/dist/providers/env-file.js +24 -75
  101. package/dist/providers/env-file.js.map +1 -1
  102. package/dist/providers/runtime-env.d.ts +4 -2
  103. package/dist/providers/runtime-env.js +9 -6
  104. package/dist/providers/runtime-env.js.map +1 -1
  105. package/dist/providers/tables.d.ts +3 -1
  106. package/dist/providers/tables.js +4 -2
  107. package/dist/providers/tables.js.map +1 -1
  108. package/dist/redact.js +56 -23
  109. package/dist/redact.js.map +1 -1
  110. package/dist/release.d.ts +7 -0
  111. package/dist/release.js +19 -3
  112. package/dist/release.js.map +1 -1
  113. package/dist/runtime.d.ts +5 -1
  114. package/dist/runtime.js +303 -96
  115. package/dist/runtime.js.map +1 -1
  116. package/dist/server.js +31 -18
  117. package/dist/server.js.map +1 -1
  118. package/dist/sessions/anchor.d.ts +11 -4
  119. package/dist/sessions/anchor.js +33 -37
  120. package/dist/sessions/anchor.js.map +1 -1
  121. package/dist/sessions/background-events.d.ts +2 -0
  122. package/dist/sessions/background-events.js +10 -0
  123. package/dist/sessions/background-events.js.map +1 -0
  124. package/dist/sessions/background-tasks.d.ts +49 -23
  125. package/dist/sessions/background-tasks.js +161 -89
  126. package/dist/sessions/background-tasks.js.map +1 -1
  127. package/dist/sessions/background-updates.d.ts +15 -8
  128. package/dist/sessions/background-updates.js +101 -25
  129. package/dist/sessions/background-updates.js.map +1 -1
  130. package/dist/sessions/cli-sessions.d.ts +23 -6
  131. package/dist/sessions/cli-sessions.js +84 -84
  132. package/dist/sessions/cli-sessions.js.map +1 -1
  133. package/dist/sessions/completions.d.ts +38 -2
  134. package/dist/sessions/completions.js +348 -77
  135. package/dist/sessions/completions.js.map +1 -1
  136. package/dist/sessions/compress.d.ts +8 -0
  137. package/dist/sessions/compress.js +2 -2
  138. package/dist/sessions/compress.js.map +1 -1
  139. package/dist/sessions/drafts.d.ts +14 -10
  140. package/dist/sessions/drafts.js +38 -43
  141. package/dist/sessions/drafts.js.map +1 -1
  142. package/dist/sessions/export.d.ts +2 -0
  143. package/dist/sessions/export.js +6 -2
  144. package/dist/sessions/export.js.map +1 -1
  145. package/dist/sessions/gateway-sessions.d.ts +86 -0
  146. package/dist/sessions/gateway-sessions.js +914 -0
  147. package/dist/sessions/gateway-sessions.js.map +1 -0
  148. package/dist/sessions/gateway-watcher.d.ts +10 -11
  149. package/dist/sessions/gateway-watcher.js +25 -21
  150. package/dist/sessions/gateway-watcher.js.map +1 -1
  151. package/dist/sessions/goals.d.ts +31 -0
  152. package/dist/sessions/goals.js +75 -0
  153. package/dist/sessions/goals.js.map +1 -0
  154. package/dist/sessions/journal.d.ts +5 -79
  155. package/dist/sessions/journal.js +15 -606
  156. package/dist/sessions/journal.js.map +1 -1
  157. package/dist/sessions/list.d.ts +34 -18
  158. package/dist/sessions/list.js +161 -147
  159. package/dist/sessions/list.js.map +1 -1
  160. package/dist/sessions/merge.d.ts +77 -13
  161. package/dist/sessions/merge.js +315 -65
  162. package/dist/sessions/merge.js.map +1 -1
  163. package/dist/sessions/notices.d.ts +22 -0
  164. package/dist/sessions/notices.js +72 -0
  165. package/dist/sessions/notices.js.map +1 -0
  166. package/dist/sessions/pending.d.ts +10 -4
  167. package/dist/sessions/pending.js +83 -7
  168. package/dist/sessions/pending.js.map +1 -1
  169. package/dist/sessions/prefix-index.d.ts +85 -0
  170. package/dist/sessions/prefix-index.js +243 -0
  171. package/dist/sessions/prefix-index.js.map +1 -0
  172. package/dist/sessions/recovery.d.ts +4 -7
  173. package/dist/sessions/recovery.js +100 -283
  174. package/dist/sessions/recovery.js.map +1 -1
  175. package/dist/sessions/relay.d.ts +20 -20
  176. package/dist/sessions/relay.js +205 -127
  177. package/dist/sessions/relay.js.map +1 -1
  178. package/dist/sessions/service.d.ts +251 -78
  179. package/dist/sessions/service.js +1510 -564
  180. package/dist/sessions/service.js.map +1 -1
  181. package/dist/sessions/session.d.ts +20 -2
  182. package/dist/sessions/session.js +33 -5
  183. package/dist/sessions/session.js.map +1 -1
  184. package/dist/sessions/shares.d.ts +7 -6
  185. package/dist/sessions/shares.js +22 -45
  186. package/dist/sessions/shares.js.map +1 -1
  187. package/dist/sessions/source-kind.js +1 -0
  188. package/dist/sessions/source-kind.js.map +1 -1
  189. package/dist/sessions/state-db-identity.d.ts +18 -0
  190. package/dist/sessions/state-db-identity.js +67 -0
  191. package/dist/sessions/state-db-identity.js.map +1 -0
  192. package/dist/sessions/state-db-worker.js +24 -5
  193. package/dist/sessions/state-db-worker.js.map +1 -1
  194. package/dist/sessions/state-db.d.ts +157 -21
  195. package/dist/sessions/state-db.js +1145 -204
  196. package/dist/sessions/state-db.js.map +1 -1
  197. package/dist/sessions/state-saved.d.ts +1 -1
  198. package/dist/sessions/state-saved.js +35 -11
  199. package/dist/sessions/state-saved.js.map +1 -1
  200. package/dist/sessions/storage/backend.d.ts +171 -0
  201. package/dist/sessions/storage/backend.js +19 -0
  202. package/dist/sessions/storage/backend.js.map +1 -0
  203. package/dist/sessions/storage/conformance.d.ts +20 -0
  204. package/dist/sessions/storage/conformance.js +237 -0
  205. package/dist/sessions/storage/conformance.js.map +1 -0
  206. package/dist/sessions/storage/document.d.ts +16 -0
  207. package/dist/sessions/storage/document.js +44 -0
  208. package/dist/sessions/storage/document.js.map +1 -0
  209. package/dist/sessions/storage/json-documents.d.ts +25 -0
  210. package/dist/sessions/storage/json-documents.js +277 -0
  211. package/dist/sessions/storage/json-documents.js.map +1 -0
  212. package/dist/sessions/storage/json-files.d.ts +23 -0
  213. package/dist/sessions/storage/json-files.js +244 -0
  214. package/dist/sessions/storage/json-files.js.map +1 -0
  215. package/dist/sessions/storage/json-installation.d.ts +3 -0
  216. package/dist/sessions/storage/json-installation.js +31 -0
  217. package/dist/sessions/storage/json-installation.js.map +1 -0
  218. package/dist/sessions/storage/json-journal.d.ts +113 -0
  219. package/dist/sessions/storage/json-journal.js +756 -0
  220. package/dist/sessions/storage/json-journal.js.map +1 -0
  221. package/dist/sessions/storage/json-recovery.d.ts +4 -0
  222. package/dist/sessions/storage/json-recovery.js +195 -0
  223. package/dist/sessions/storage/json-recovery.js.map +1 -0
  224. package/dist/sessions/storage/json.d.ts +82 -0
  225. package/dist/sessions/storage/json.js +350 -0
  226. package/dist/sessions/storage/json.js.map +1 -0
  227. package/dist/sessions/storage/row.d.ts +6 -0
  228. package/dist/sessions/storage/row.js +49 -0
  229. package/dist/sessions/storage/row.js.map +1 -0
  230. package/dist/sessions/storage/search-text.d.ts +4 -0
  231. package/dist/sessions/storage/search-text.js +16 -0
  232. package/dist/sessions/storage/search-text.js.map +1 -0
  233. package/dist/sessions/store.d.ts +67 -73
  234. package/dist/sessions/store.js +341 -607
  235. package/dist/sessions/store.js.map +1 -1
  236. package/dist/sessions/streams.d.ts +12 -7
  237. package/dist/sessions/streams.js +58 -15
  238. package/dist/sessions/streams.js.map +1 -1
  239. package/dist/sessions/titles.js +1 -1
  240. package/dist/sessions/titles.js.map +1 -1
  241. package/dist/sessions/todo.d.ts +5 -0
  242. package/dist/sessions/todo.js +28 -21
  243. package/dist/sessions/todo.js.map +1 -1
  244. package/dist/sessions/tool-display.d.ts +8 -5
  245. package/dist/sessions/tool-display.js +28 -16
  246. package/dist/sessions/tool-display.js.map +1 -1
  247. package/dist/sessions/turn-context.d.ts +10 -1
  248. package/dist/sessions/turn-context.js +13 -6
  249. package/dist/sessions/turn-context.js.map +1 -1
  250. package/dist/sessions/turn.d.ts +110 -22
  251. package/dist/sessions/turn.js +918 -380
  252. package/dist/sessions/turn.js.map +1 -1
  253. package/dist/sessions/wakeup-pause.d.ts +8 -13
  254. package/dist/sessions/wakeup-pause.js +31 -49
  255. package/dist/sessions/wakeup-pause.js.map +1 -1
  256. package/dist/sessions/window.js +13 -12
  257. package/dist/sessions/window.js.map +1 -1
  258. package/dist/settings.d.ts +19 -7
  259. package/dist/settings.js +179 -183
  260. package/dist/settings.js.map +1 -1
  261. package/dist/sidecar/client.d.ts +41 -4
  262. package/dist/sidecar/client.js +192 -42
  263. package/dist/sidecar/client.js.map +1 -1
  264. package/dist/sidecar/discover.d.ts +2 -1
  265. package/dist/sidecar/discover.js +93 -10
  266. package/dist/sidecar/discover.js.map +1 -1
  267. package/dist/sidecar/env.d.ts +4 -0
  268. package/dist/sidecar/env.js +3 -0
  269. package/dist/sidecar/env.js.map +1 -0
  270. package/dist/sidecar/fake.d.ts +6 -2
  271. package/dist/sidecar/fake.js +34 -4
  272. package/dist/sidecar/fake.js.map +1 -1
  273. package/dist/spa.d.ts +9 -4
  274. package/dist/spa.js +36 -12
  275. package/dist/spa.js.map +1 -1
  276. package/dist/startup-timing.d.ts +66 -0
  277. package/dist/startup-timing.js +129 -0
  278. package/dist/startup-timing.js.map +1 -0
  279. package/dist/test/env-file.d.ts +5 -0
  280. package/dist/test/env-file.js +88 -0
  281. package/dist/test/env-file.js.map +1 -0
  282. package/dist/test/fs-handles.d.ts +11 -0
  283. package/dist/test/fs-handles.js +31 -0
  284. package/dist/test/fs-handles.js.map +1 -0
  285. package/dist/test/harness.d.ts +1 -0
  286. package/dist/test/harness.js +1 -1
  287. package/dist/test/harness.js.map +1 -1
  288. package/dist/test/passkeys.d.ts +22 -0
  289. package/dist/test/passkeys.js +62 -0
  290. package/dist/test/passkeys.js.map +1 -0
  291. package/dist/test/storage.d.ts +15 -0
  292. package/dist/test/storage.js +26 -0
  293. package/dist/test/storage.js.map +1 -0
  294. package/dist/tools/commands.d.ts +6 -1
  295. package/dist/tools/commands.js +8 -2
  296. package/dist/tools/commands.js.map +1 -1
  297. package/dist/tools/connectors.d.ts +33 -0
  298. package/dist/tools/connectors.js +121 -0
  299. package/dist/tools/connectors.js.map +1 -0
  300. package/dist/tools/crons.d.ts +46 -5
  301. package/dist/tools/crons.js +100 -26
  302. package/dist/tools/crons.js.map +1 -1
  303. package/dist/tools/dashboard-plugins.d.ts +34 -5
  304. package/dist/tools/dashboard-plugins.js +146 -14
  305. package/dist/tools/dashboard-plugins.js.map +1 -1
  306. package/dist/tools/extensions.d.ts +5 -4
  307. package/dist/tools/extensions.js +213 -207
  308. package/dist/tools/extensions.js.map +1 -1
  309. package/dist/tools/gateway-restart.d.ts +82 -0
  310. package/dist/tools/gateway-restart.js +222 -0
  311. package/dist/tools/gateway-restart.js.map +1 -0
  312. package/dist/tools/health.d.ts +28 -4
  313. package/dist/tools/health.js +171 -50
  314. package/dist/tools/health.js.map +1 -1
  315. package/dist/tools/hygiene.d.ts +4 -3
  316. package/dist/tools/hygiene.js +23 -9
  317. package/dist/tools/hygiene.js.map +1 -1
  318. package/dist/tools/insights.d.ts +1 -1
  319. package/dist/tools/insights.js +2 -2
  320. package/dist/tools/insights.js.map +1 -1
  321. package/dist/tools/kanban.d.ts +4 -2
  322. package/dist/tools/kanban.js +28 -19
  323. package/dist/tools/kanban.js.map +1 -1
  324. package/dist/tools/mcp-health.js +74 -24
  325. package/dist/tools/mcp-health.js.map +1 -1
  326. package/dist/tools/mcp.js +19 -1
  327. package/dist/tools/mcp.js.map +1 -1
  328. package/dist/tools/memory-masks.d.ts +5 -0
  329. package/dist/tools/memory-masks.js +256 -0
  330. package/dist/tools/memory-masks.js.map +1 -0
  331. package/dist/tools/memory.d.ts +29 -3
  332. package/dist/tools/memory.js +69 -27
  333. package/dist/tools/memory.js.map +1 -1
  334. package/dist/tools/plugin-oauth.d.ts +14 -0
  335. package/dist/tools/plugin-oauth.js +53 -0
  336. package/dist/tools/plugin-oauth.js.map +1 -0
  337. package/dist/tools/plugin-settings.d.ts +15 -0
  338. package/dist/tools/plugin-settings.js +90 -0
  339. package/dist/tools/plugin-settings.js.map +1 -0
  340. package/dist/tools/prune-advice.d.ts +50 -0
  341. package/dist/tools/prune-advice.js +114 -0
  342. package/dist/tools/prune-advice.js.map +1 -0
  343. package/dist/tools/skills.d.ts +13 -5
  344. package/dist/tools/skills.js +53 -19
  345. package/dist/tools/skills.js.map +1 -1
  346. package/dist/tools/update-notifications.d.ts +102 -17
  347. package/dist/tools/update-notifications.js +400 -82
  348. package/dist/tools/update-notifications.js.map +1 -1
  349. package/dist/tools/update-rollback.d.ts +11 -0
  350. package/dist/tools/update-rollback.js +28 -0
  351. package/dist/tools/update-rollback.js.map +1 -0
  352. package/dist/tools/updates.d.ts +100 -33
  353. package/dist/tools/updates.js +1085 -345
  354. package/dist/tools/updates.js.map +1 -1
  355. package/dist/util.d.ts +2 -0
  356. package/dist/util.js +5 -0
  357. package/dist/util.js.map +1 -1
  358. package/dist/workspace/git.d.ts +16 -1
  359. package/dist/workspace/git.js +291 -11
  360. package/dist/workspace/git.js.map +1 -1
  361. package/dist/workspace/media.d.ts +12 -9
  362. package/dist/workspace/media.js +98 -38
  363. package/dist/workspace/media.js.map +1 -1
  364. package/dist/workspace/preview.js +4 -14
  365. package/dist/workspace/preview.js.map +1 -1
  366. package/dist/workspace/rollback.d.ts +8 -4
  367. package/dist/workspace/rollback.js +22 -12
  368. package/dist/workspace/rollback.js.map +1 -1
  369. package/dist/workspace/workspaces.d.ts +15 -19
  370. package/dist/workspace/workspaces.js +55 -127
  371. package/dist/workspace/workspaces.js.map +1 -1
  372. package/dist/workspace/worktrees.js +170 -32
  373. package/dist/workspace/worktrees.js.map +1 -1
  374. package/package.json +6 -4
  375. package/sidecar/agent_dependency.json +4 -4
  376. package/sidecar/agent_support.json +29 -0
  377. package/sidecar/scripts/record-fixtures.py +12 -3
  378. package/sidecar/scripts/replay_sidecar.py +8 -0
  379. package/sidecar/scripts/suite_runner.py +458 -0
  380. package/sidecar/scripts/test.sh +11 -5
  381. package/sidecar/talaria_sidecar/__init__.py +6 -1
  382. package/sidecar/talaria_sidecar/__main__.py +13 -4
  383. package/sidecar/talaria_sidecar/agent_mutation.py +1111 -0
  384. package/sidecar/talaria_sidecar/agent_services.py +1116 -0
  385. package/sidecar/talaria_sidecar/context_detail.py +59 -0
  386. package/sidecar/talaria_sidecar/dependency_sync.py +85 -0
  387. package/sidecar/talaria_sidecar/home.py +133 -14
  388. package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
  389. package/sidecar/talaria_sidecar/methods/aux.py +211 -41
  390. package/sidecar/talaria_sidecar/methods/chat.py +1378 -200
  391. package/sidecar/talaria_sidecar/methods/checkpoints.py +29 -4
  392. package/sidecar/talaria_sidecar/methods/commands.py +483 -13
  393. package/sidecar/talaria_sidecar/methods/config.py +195 -6
  394. package/sidecar/talaria_sidecar/methods/connectors.py +375 -0
  395. package/sidecar/talaria_sidecar/methods/cron.py +374 -36
  396. package/sidecar/talaria_sidecar/methods/cron_execution.py +284 -0
  397. package/sidecar/talaria_sidecar/methods/dashboard_plugins.py +301 -0
  398. package/sidecar/talaria_sidecar/methods/gateway.py +215 -55
  399. package/sidecar/talaria_sidecar/methods/gateway_sessions.py +418 -0
  400. package/sidecar/talaria_sidecar/methods/goals.py +81 -41
  401. package/sidecar/talaria_sidecar/methods/kanban.py +82 -30
  402. package/sidecar/talaria_sidecar/methods/memory.py +84 -0
  403. package/sidecar/talaria_sidecar/methods/oauth.py +144 -13
  404. package/sidecar/talaria_sidecar/methods/plugin_catalog.py +198 -0
  405. package/sidecar/talaria_sidecar/methods/plugin_oauth.py +234 -0
  406. package/sidecar/talaria_sidecar/methods/process.py +229 -16
  407. package/sidecar/talaria_sidecar/methods/profiles.py +48 -9
  408. package/sidecar/talaria_sidecar/methods/providers.py +196 -26
  409. package/sidecar/talaria_sidecar/methods/runtime.py +71 -1
  410. package/sidecar/talaria_sidecar/methods/skills.py +83 -5
  411. package/sidecar/talaria_sidecar/methods/state_db.py +405 -112
  412. package/sidecar/talaria_sidecar/methods/stt.py +159 -78
  413. package/sidecar/talaria_sidecar/methods/text.py +14 -1
  414. package/sidecar/talaria_sidecar/methods/toolsets.py +51 -0
  415. package/sidecar/talaria_sidecar/methods/usage.py +113 -14
  416. package/sidecar/talaria_sidecar/methods/worktree.py +145 -19
  417. package/sidecar/talaria_sidecar/methods/write_approval.py +181 -0
  418. package/sidecar/talaria_sidecar/rpc.py +70 -7
  419. package/sidecar/talaria_sidecar/runtime.py +122 -14
  420. package/sidecar/talaria_sidecar/speed.py +47 -0
  421. package/sidecar/talaria_sidecar/startup_timing.py +119 -0
  422. package/static/dist/FILES.txt +242 -215
  423. package/static/dist/THIRD_PARTY_LICENSES.md +6 -22
  424. package/static/dist/assets/AppShell-3isgvoro.js +8 -0
  425. package/static/dist/assets/Button-f4n6dapf.js +1 -0
  426. package/static/dist/assets/ChatPage-k7b6xfax.js +4 -0
  427. package/static/dist/assets/ConnectTargets-juqvt5lm.js +1 -0
  428. package/static/dist/assets/Dialog-hta5utr3.js +1 -0
  429. package/static/dist/assets/ExtensionRoute-zrhjn0wm.js +1 -0
  430. package/static/dist/assets/Field-b6ez48oz.js +1 -0
  431. package/static/dist/assets/HubRoute-iwvso2tz.js +2 -0
  432. package/static/dist/assets/LinkCheckContext-ix6zqqkk.js +1 -0
  433. package/static/dist/assets/Markdown-jdeth7zq.js +2 -0
  434. package/static/dist/assets/MarkdownRenderer-fbswsgld.js +302 -0
  435. package/static/dist/assets/MarkdownRenderer-i9o6nr38.css +1 -0
  436. package/static/dist/assets/OAuthSignIn-bpd8hom3.js +1 -0
  437. package/static/dist/assets/ResourceSwitch-odi6dy2o.js +1 -0
  438. package/static/dist/assets/RightPanel-eu6enlo6.js +1 -0
  439. package/static/dist/assets/Select-kr07928w.js +1 -0
  440. package/static/dist/assets/SessionListPanel-etb2i54l.js +1 -0
  441. package/static/dist/assets/SettingsLayout-eu3xxp23.js +1 -0
  442. package/static/dist/assets/States-icab19yg.js +1 -0
  443. package/static/dist/assets/TerminalPanel-hngmsr8b.js +16 -0
  444. package/static/dist/assets/Toaster-nb2uzjw1.js +1 -0
  445. package/static/dist/assets/_app-ga1moszl.js +1 -0
  446. package/static/dist/assets/_app.ext._extensionId-na62087t.js +1 -0
  447. package/static/dist/assets/_app.index-jypgdqoi.js +1 -0
  448. package/static/dist/assets/_app.insights-kabejdtm.js +1 -0
  449. package/static/dist/assets/_app.kanban-m19kfcmx.js +1 -0
  450. package/static/dist/assets/_app.logs-iw4m96sz.js +1 -0
  451. package/static/dist/assets/_app.memory-jq5frwcg.js +1 -0
  452. package/static/dist/assets/_app.profiles-kwa3m7rp.js +1 -0
  453. package/static/dist/assets/_app.session._sessionId-o5lyeipj.js +1 -0
  454. package/static/dist/assets/_app.settings-ckl8hdll.js +1 -0
  455. package/static/dist/assets/_app.settings._section-dtdjk98a.js +3 -0
  456. package/static/dist/assets/_app.skills-n016rihd.js +1 -0
  457. package/static/dist/assets/_app.tasks-n8zuqsqe.js +1 -0
  458. package/static/dist/assets/_app.todos-btew5v2s.js +1 -0
  459. package/static/dist/assets/_app.workspaces-lnfk95ch.js +1 -0
  460. package/static/dist/assets/abnfDiagram-VCTEODGH-dttheegb.js +1 -0
  461. package/static/dist/assets/{angular-html-nu6l31w3.js → angular-html-l28bd5fo.js} +1 -1
  462. package/static/dist/assets/{angular-ts-fbx4f8mm.js → angular-ts-ffzx1vj0.js} +1 -1
  463. package/static/dist/assets/{apl-bx2xr7dh.js → apl-b7h7mmy4.js} +1 -1
  464. package/static/dist/assets/{arc-7popfhbk.js → arc-fk1b21lm.js} +1 -1
  465. package/static/dist/assets/architecture-7GRP2DOG-ic435919.js +1 -0
  466. package/static/dist/assets/architectureDiagram-5GKGNRK7-8r3glgiq.js +36 -0
  467. package/static/dist/assets/archive-il7c6053.js +1 -0
  468. package/static/dist/assets/{astro-g77i97cb.js → astro-k117fy0t.js} +1 -1
  469. package/static/dist/assets/back-gu3nu279.js +1 -0
  470. package/static/dist/assets/{blade-boo0u92y.js → blade-npefsmn8.js} +1 -1
  471. package/static/dist/assets/{blockDiagram-I7D4REHJ-jzthp1wa.js → blockDiagram-I7D4REHJ-jenyos6c.js} +4 -4
  472. package/static/dist/assets/{c-cat0w3sc.js → c-2umg6kbn.js} +1 -1
  473. package/static/dist/assets/{c4Diagram-7LVT6UL2-mwr67mt8.js → c4Diagram-7LVT6UL2-nzlx7yc4.js} +1 -1
  474. package/static/dist/assets/cancel-bsnfo36u.js +1 -0
  475. package/static/dist/assets/channel-dsbtk01y.js +1 -0
  476. package/static/dist/assets/{chunk-4HAMMTFA-gpq1y228.js → chunk-4HAMMTFA-g0cru63b.js} +20 -20
  477. package/static/dist/assets/{chunk-75Z2AOVW-ktje58c4.js → chunk-75Z2AOVW-7rkswkjw.js} +1 -1
  478. package/static/dist/assets/chunk-DU6HZSFF-fn73jzdo.js +128 -0
  479. package/static/dist/assets/{chunk-F27PBJKO-cmev85ti.js → chunk-F27PBJKO-oey8e72m.js} +1 -1
  480. package/static/dist/assets/{chunk-FOHPRMQF-opuflp73.js → chunk-FOHPRMQF-ipewusgj.js} +8 -8
  481. package/static/dist/assets/{chunk-GMAD6QVW-htjgt22y.js → chunk-GMAD6QVW-l2a32wiy.js} +6 -6
  482. package/static/dist/assets/{chunk-GVQU2GXP-qndvoxl7.js → chunk-GVQU2GXP-o9dwzh8h.js} +1 -1
  483. package/static/dist/assets/{chunk-IMKFNOWR-e5bkd5q6.js → chunk-IMKFNOWR-eblwj20b.js} +5 -5
  484. package/static/dist/assets/{chunk-L3NEJ4N5-mj1155r6.js → chunk-L3NEJ4N5-o2mufspb.js} +1 -1
  485. package/static/dist/assets/chunk-OSK3NFVY-ijp9pw29.js +10 -0
  486. package/static/dist/assets/{chunk-P2QGCYS3-f7t54xfm.js → chunk-P2QGCYS3-hs9hqwl4.js} +1 -1
  487. package/static/dist/assets/{chunk-POPQ4Y6H-i927v9oa.js → chunk-POPQ4Y6H-bp9rsqez.js} +1 -1
  488. package/static/dist/assets/{chunk-PWAF6VOD-m84lmnyt.js → chunk-PWAF6VOD-brkprov6.js} +1 -1
  489. package/static/dist/assets/{chunk-SHT3W25Y-mi0tlcu9.js → chunk-SHT3W25Y-gmpv0pxz.js} +13 -13
  490. package/static/dist/assets/{chunk-SVP7TREG-dnau702j.js → chunk-SVP7TREG-oldm08fu.js} +4 -4
  491. package/static/dist/assets/{chunk-TICWLB2K-edem1gkg.js → chunk-TICWLB2K-ooqilfv7.js} +6 -6
  492. package/static/dist/assets/{chunk-XXDRQBXY-n40pf9vy.js → chunk-XXDRQBXY-jkmnqu65.js} +1 -1
  493. package/static/dist/assets/classDiagram-ZZMXUADV-fvgcv24g.js +1 -0
  494. package/static/dist/assets/classDiagram-v2-VYDZK3BY-fvgcv24g.js +1 -0
  495. package/static/dist/assets/{cobol-go622aep.js → cobol-ssf9hkh7.js} +1 -1
  496. package/static/dist/assets/{coffee-jwo1xjhq.js → coffee-lsi9fwae.js} +1 -1
  497. package/static/dist/assets/copied-bz3gkazh.js +1 -0
  498. package/static/dist/assets/copy-jo006coq.js +1 -0
  499. package/static/dist/assets/cose-bilkent-JH36ORCC-layy2r27.js +1 -0
  500. package/static/dist/assets/{cpp-nre7j0kq.js → cpp-lfrh4y55.js} +1 -1
  501. package/static/dist/assets/{crystal-lviso2je.js → crystal-efh4pte7.js} +1 -1
  502. package/static/dist/assets/{css-ywz9j4hb.js → css-i82qdo89.js} +1 -1
  503. package/static/dist/assets/cynefin-OW5HDTMX-d47op7qs.js +1 -0
  504. package/static/dist/assets/cynefinDiagram-5FMLGOSQ-doa7594y.js +62 -0
  505. package/static/dist/assets/cytoscape.esm-fjn2algp.js +321 -0
  506. package/static/dist/assets/dagre-GXQ25YYZ-os768dp4.js +4 -0
  507. package/static/dist/assets/dagre-o5jmjz46.js +1 -0
  508. package/static/dist/assets/{diagram-S7CK7UJ4-l8abxaes.js → diagram-S7CK7UJ4-fyi81nxd.js} +4 -4
  509. package/static/dist/assets/diagram-UQ7AKVKN-j3exggaq.js +41 -0
  510. package/static/dist/assets/{diagram-VSXAHHWV-fnky6lvc.js → diagram-VSXAHHWV-omo1en2l.js} +1 -1
  511. package/static/dist/assets/diagram-VX7I27RA-oro2cyu0.js +24 -0
  512. package/static/dist/assets/diagram-Z3DM3KII-byt9vdb7.js +24 -0
  513. package/static/dist/assets/dist-crc46di9.js +7 -0
  514. package/static/dist/assets/dist-etpoyscq.js +44 -0
  515. package/static/dist/assets/{dist-c4c2m3kv.js → dist-nb670eu6.js} +1 -1
  516. package/static/dist/assets/ebnfDiagram-PWID7BFC-ee3haugh.js +1 -0
  517. package/static/dist/assets/{edge-dvv35byi.js → edge-lrob635n.js} +1 -1
  518. package/static/dist/assets/{elixir-i2f1ki1g.js → elixir-kdpxeq93.js} +1 -1
  519. package/static/dist/assets/{elm-cpepyjm4.js → elm-c8irj9uh.js} +1 -1
  520. package/static/dist/assets/erDiagram-RLTQ6QDP-d1e93yff.js +99 -0
  521. package/static/dist/assets/{erb-idlyfheg.js → erb-flwsv146.js} +1 -1
  522. package/static/dist/assets/eventmodeling-NTZA5JFV-i1ihb40c.js +1 -0
  523. package/static/dist/assets/flowDiagram-HODETNUW-jahfpiod.js +1 -0
  524. package/static/dist/assets/folder-plus-ivoglq98.js +1 -0
  525. package/static/dist/assets/{ganttDiagram-EL5Y4UJY-ngbsyp37.js → ganttDiagram-EL5Y4UJY-m7g0jud4.js} +1 -1
  526. package/static/dist/assets/{git-rebase-d9lpfpbn.js → git-rebase-bli19aoq.js} +1 -1
  527. package/static/dist/assets/gitGraph-4MIJSDKK-hzdj07yy.js +1 -0
  528. package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-eravdti4.js → gitGraphDiagram-WWUBYQGX-dm9agbna.js} +1 -1
  529. package/static/dist/assets/{glimmer-js-39i0urfb.js → glimmer-js-d4icg38p.js} +1 -1
  530. package/static/dist/assets/{glimmer-ts-gdtd6n73.js → glimmer-ts-bk75772u.js} +1 -1
  531. package/static/dist/assets/{glsl-o1djm5pp.js → glsl-lqormp2b.js} +1 -1
  532. package/static/dist/assets/{graphql-kj6rz55t.js → graphql-hw6s5qzd.js} +1 -1
  533. package/static/dist/assets/{hack-pe1qqr2i.js → hack-ozrv6l4d.js} +1 -1
  534. package/static/dist/assets/{haml-cknnbcqs.js → haml-gytfosdt.js} +1 -1
  535. package/static/dist/assets/{handlebars-hwq64orh.js → handlebars-b6g7jffd.js} +1 -1
  536. package/static/dist/assets/highlighted-body-KPVGNVTW-mmgz2czu.js +1 -0
  537. package/static/dist/assets/host-fr6lni1p.js +1 -0
  538. package/static/dist/assets/{html-derivative-h2i2pyjc.js → html-derivative-f1tqcroo.js} +1 -1
  539. package/static/dist/assets/{html-ha05xtvt.js → html-on6oj6yj.js} +1 -1
  540. package/static/dist/assets/{http-cf94zu27.js → http-odhufsl7.js} +1 -1
  541. package/static/dist/assets/{hurl-lslax568.js → hurl-mu680336.js} +1 -1
  542. package/static/dist/assets/index-73649w7o.js +10 -0
  543. package/static/dist/assets/index-lhf1fsdf.css +2 -0
  544. package/static/dist/assets/info-A6RAGUB7-lll1dqmm.js +1 -0
  545. package/static/dist/assets/infoDiagram-27XIBGKW-k7esc639.js +2 -0
  546. package/static/dist/assets/{ishikawaDiagram-5VMMS53U-i6ypq351.js → ishikawaDiagram-5VMMS53U-lgr6k9wo.js} +6 -6
  547. package/static/dist/assets/{java-ndjf2o89.js → java-nxbe8uju.js} +1 -1
  548. package/static/dist/assets/{javascript-coftlwy3.js → javascript-lz3kxcyz.js} +1 -1
  549. package/static/dist/assets/{jinja-etei9wy7.js → jinja-l5pk84gt.js} +1 -1
  550. package/static/dist/assets/{jison-hyekgft1.js → jison-jdutdwyy.js} +1 -1
  551. package/static/dist/assets/{journeyDiagram-3NMN7TZE-lduf1z95.js → journeyDiagram-3NMN7TZE-ndmbxuqk.js} +1 -1
  552. package/static/dist/assets/{json-lok0yaho.js → json-dnge4ssf.js} +1 -1
  553. package/static/dist/assets/{jsx-b64w0nbx.js → jsx-c3lwhkep.js} +1 -1
  554. package/static/dist/assets/{jsx-runtime-eu92a5pu.js → jsx-runtime-o40ukmjl.js} +1 -1
  555. package/static/dist/assets/{julia-jxxzz2yn.js → julia-hwd4rrcb.js} +1 -1
  556. package/static/dist/assets/{just-mc29iw9t.js → just-nka3oxdx.js} +1 -1
  557. package/static/dist/assets/{kanban-definition-UXKFOSKX-e18bu4pg.js → kanban-definition-UXKFOSKX-mjxym0jp.js} +28 -28
  558. package/static/dist/assets/katex-cbdd1ohw.css +1 -0
  559. package/static/dist/assets/katex-pbij2oo6.js +257 -0
  560. package/static/dist/assets/{latex-pafa03ft.js → latex-iqmr7e40.js} +1 -1
  561. package/static/dist/assets/lazyRouteComponent-cwb6l7xh.js +1 -0
  562. package/static/dist/assets/{line-meizqsvn.js → line-mtr5k6il.js} +1 -1
  563. package/static/dist/assets/linear-jlwrvvy6.js +1 -0
  564. package/static/dist/assets/link-iadw4ghw.js +2 -0
  565. package/static/dist/assets/{liquid-hz5c2pov.js → liquid-okmk9tbp.js} +1 -1
  566. package/static/dist/assets/locale-cs-n41hptse.js +5 -0
  567. package/static/dist/assets/locale-de-k7ht47ew.js +5 -0
  568. package/static/dist/assets/locale-es-ih078lco.js +5 -0
  569. package/static/dist/assets/locale-fr-ojabnf41.js +5 -0
  570. package/static/dist/assets/locale-it-kb96jufy.js +5 -0
  571. package/static/dist/assets/locale-ja-jpsp3xs1.js +7 -0
  572. package/static/dist/assets/locale-ko-cpzuy0n7.js +5 -0
  573. package/static/dist/assets/locale-pl-dbap4leu.js +5 -0
  574. package/static/dist/assets/locale-pt-nci7w72x.js +5 -0
  575. package/static/dist/assets/locale-ru-soghe4cs.js +7 -0
  576. package/static/dist/assets/locale-tr-ks00mu68.js +5 -0
  577. package/static/dist/assets/locale-vi-ij7twfm6.js +5 -0
  578. package/static/dist/assets/locale-zh-Hant-feakwr7a.js +5 -0
  579. package/static/dist/assets/locale-zh-ren0vjyv.js +7 -0
  580. package/static/dist/assets/login-lkhtzfcl.js +1 -0
  581. package/static/dist/assets/{lua-leyf8mlb.js → lua-krx4xj5h.js} +1 -1
  582. package/static/dist/assets/{marko-cn1fzpdv.js → marko-gbvpfehz.js} +1 -1
  583. package/static/dist/assets/{mdc-huhy88df.js → mdc-c21zlyi5.js} +1 -1
  584. package/static/dist/assets/mermaid-HWGCJPDP-jx0q5dm2.js +1 -0
  585. package/static/dist/assets/mermaid-parser.core-k91vdnvi.js +7 -0
  586. package/static/dist/assets/micromark-factory-space-fnmnpi6s.js +1 -0
  587. package/static/dist/assets/{mindmap-definition-YA3MSWOX-27b26eok.js → mindmap-definition-YA3MSWOX-ehztfea4.js} +28 -28
  588. package/static/dist/assets/{nginx-e7lvhsmn.js → nginx-f3d68ed4.js} +1 -1
  589. package/static/dist/assets/{nim-dun5d8mk.js → nim-dw0jcy70.js} +1 -1
  590. package/static/dist/assets/onboarding-f67z0a2r.js +1 -0
  591. package/static/dist/assets/packet-AYTQ26CC-ok2xrblz.js +1 -0
  592. package/static/dist/assets/pcmTap.worklet-74EKjWAZ.js +1 -0
  593. package/static/dist/assets/pegDiagram-XKGWAZYB-ec3rpj0m.js +1 -0
  594. package/static/dist/assets/{perl-bj3oqtet.js → perl-3c0r45c8.js} +1 -1
  595. package/static/dist/assets/{php-buzp12r0.js → php-rznhidvp.js} +1 -1
  596. package/static/dist/assets/pie-WAS4IAKB-iwkir23u.js +1 -0
  597. package/static/dist/assets/pieDiagram-E7YTZNPT-fcpqpekh.js +39 -0
  598. package/static/dist/assets/{pug-mjad7he9.js → pug-f2le4f3y.js} +1 -1
  599. package/static/dist/assets/{qml-d1gdp10s.js → qml-e2wbq9cr.js} +1 -1
  600. package/static/dist/assets/{quadrantDiagram-AXDQQJYC-hhue7d8o.js → quadrantDiagram-AXDQQJYC-lqsync43.js} +1 -1
  601. package/static/dist/assets/queryKeys-ed4n2egc.js +9 -0
  602. package/static/dist/assets/{r-o3r9c7ao.js → r-epfzuw6q.js} +1 -1
  603. package/static/dist/assets/radar-RG4KPBEZ-hje4jbk6.js +1 -0
  604. package/static/dist/assets/railroad-74A4TZTK-gll9n6qi.js +1 -0
  605. package/static/dist/assets/railroad-abnf-HS5TGJTU-o6h6lpn4.js +1 -0
  606. package/static/dist/assets/railroad-ebnf-LZEXJU2U-juaefj6o.js +1 -0
  607. package/static/dist/assets/railroad-peg-WCYAUIDC-cgfhvpah.js +1 -0
  608. package/static/dist/assets/railroadDiagram-O6MQD6OU-lp3w7awf.js +1 -0
  609. package/static/dist/assets/{razor-ilc585yr.js → razor-pea1u1d0.js} +1 -1
  610. package/static/dist/assets/refresh-cw-iccm7a4e.js +1 -0
  611. package/static/dist/assets/refresh-i9ya9dvl.js +1 -0
  612. package/static/dist/assets/{regexp-mpjtcvwm.js → regexp-hzte0a5x.js} +1 -1
  613. package/static/dist/assets/registry-g7w3353u.js +1 -0
  614. package/static/dist/assets/requirementDiagram-BXWQKSXE-ha8eibcf.js +84 -0
  615. package/static/dist/assets/rolldown-runtime-bnnw9q4a.js +1 -0
  616. package/static/dist/assets/{rst-jrkwjdyi.js → rst-m6tugj5o.js} +1 -1
  617. package/static/dist/assets/{ruby-n4tjcbpz.js → ruby-l19vq3w1.js} +1 -1
  618. package/static/dist/assets/runtime-nlxpnegr.js +1 -0
  619. package/static/dist/assets/{sankeyDiagram-P5KCCOFB-fqb8xcjf.js → sankeyDiagram-P5KCCOFB-oxf31pr9.js} +1 -1
  620. package/static/dist/assets/{sas-hjo6y93k.js → sas-ktdxo397.js} +1 -1
  621. package/static/dist/assets/{scss-ncidjv48.js → scss-kn3oxfwf.js} +1 -1
  622. package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-dlcz6qjz.js → sequenceDiagram-WJ2MYXX4-jiq8lhyo.js} +11 -11
  623. package/static/dist/assets/settings_section_extensions_title-fadc7o65.js +1 -0
  624. package/static/dist/assets/share._token-o76ki0ly.js +2 -0
  625. package/static/dist/assets/{shellscript-m8whjpqd.js → shellscript-l100jtrg.js} +1 -1
  626. package/static/dist/assets/{shellsession-i5bj6syz.js → shellsession-m5qukwow.js} +1 -1
  627. package/static/dist/assets/sizeCapture-INFHLROL-dk1io13w.js +1 -0
  628. package/static/dist/assets/sliders-horizontal-ddcs88av.js +1 -0
  629. package/static/dist/assets/{soy-hhh36ysq.js → soy-mexlscur.js} +1 -1
  630. package/static/dist/assets/{sql-j21bzcl6.js → sql-o0ho5suh.js} +1 -1
  631. package/static/dist/assets/src-h89s0hs4.js +1 -0
  632. package/static/dist/assets/{stata-bbu2onh1.js → stata-be9ck1q3.js} +1 -1
  633. package/static/dist/assets/stateDiagram-D77RDMKH-f3fslnon.js +1 -0
  634. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-hlt6yphw.js +1 -0
  635. package/static/dist/assets/{surrealql-l5vczuco.js → surrealql-gk1xkkyy.js} +1 -1
  636. package/static/dist/assets/{svelte-idn422g6.js → svelte-k6ecjilf.js} +1 -1
  637. package/static/dist/assets/swimlanes-42K2YHIH-k1hmk7jf.js +1 -0
  638. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-hd5t9tnt.js +8 -0
  639. package/static/dist/assets/{templ-e22qou3c.js → templ-gf3bftk6.js} +1 -1
  640. package/static/dist/assets/{tex-deqpc5ee.js → tex-d21fqv1z.js} +1 -1
  641. package/static/dist/assets/{timeline-definition-24CTP7MA-b1vl2zil.js → timeline-definition-24CTP7MA-n3ncfy9r.js} +8 -8
  642. package/static/dist/assets/toast-liparz2j.js +56 -0
  643. package/static/dist/assets/treeView-Q6P3EWNA-l3y2pejt.js +1 -0
  644. package/static/dist/assets/treemap-WGGIJYW6-jown9axh.js +1 -0
  645. package/static/dist/assets/{ts-tags-ctnibc27.js → ts-tags-n0g61gr6.js} +1 -1
  646. package/static/dist/assets/{tsx-i163n32g.js → tsx-cvv7wv6q.js} +1 -1
  647. package/static/dist/assets/{twig-x0fi7wm8.js → twig-n1f67204.js} +1 -1
  648. package/static/dist/assets/{typescript-g18c34xz.js → typescript-og7o55cx.js} +1 -1
  649. package/static/dist/assets/untitled-mj86r4gz.js +1 -0
  650. package/static/dist/assets/useDebounced-g8r0ggve.js +1 -0
  651. package/static/dist/assets/useForm-o4uu74mx.js +1 -0
  652. package/static/dist/assets/{useParams-b4dq6n4q.js → useParams-ftsveu84.js} +1 -1
  653. package/static/dist/assets/{useSearch-ipfmv3he.js → useSearch-crqueqga.js} +1 -1
  654. package/static/dist/assets/vennDiagram-4TSXK5OY-dh59uq1c.js +34 -0
  655. package/static/dist/assets/{vue-mwu5l1bb.js → vue-ej4xpwqw.js} +1 -1
  656. package/static/dist/assets/{vue-html-nskj3du0.js → vue-html-d48xsjuo.js} +1 -1
  657. package/static/dist/assets/{vue-vine-cqe77smq.js → vue-vine-pev88ip6.js} +1 -1
  658. package/static/dist/assets/wardley-WFR3VGLG-e6d74b0j.js +1 -0
  659. package/static/dist/assets/wardleyDiagram-VM6X3IG4-hnofh78f.js +78 -0
  660. package/static/dist/assets/with-selector-nu1b5z0v.js +1 -0
  661. package/static/dist/assets/{xml-gau7gvrv.js → xml-ifxp6gqb.js} +1 -1
  662. package/static/dist/assets/{xsl-ew09zq8b.js → xsl-fug2cckc.js} +1 -1
  663. package/static/dist/assets/{xychartDiagram-S5SC5T6Z-e49523ab.js → xychartDiagram-S5SC5T6Z-bpymjelb.js} +6 -6
  664. package/static/dist/assets/{yaml-ofnrxtvf.js → yaml-fjmafnlo.js} +1 -1
  665. package/static/dist/extension-sdk.js +1 -1
  666. package/static/dist/index.html +1 -1
  667. package/static/dist/sw.js +180 -136
  668. package/static/dist/assets/AppShell-g3bkgnvt.js +0 -1
  669. package/static/dist/assets/ChatPage-oo7bdan6.js +0 -25
  670. package/static/dist/assets/Dialog-g355qxhp.js +0 -1
  671. package/static/dist/assets/ExtensionRoute-i2y9fvjm.js +0 -1
  672. package/static/dist/assets/HubRoute-lmkrbw47.js +0 -2
  673. package/static/dist/assets/Markdown-j4kut4j2.js +0 -598
  674. package/static/dist/assets/Markdown-lwviulmg.css +0 -1
  675. package/static/dist/assets/RightPanel-ohlsdzfl.js +0 -1
  676. package/static/dist/assets/Select-kddq75qc.js +0 -1
  677. package/static/dist/assets/SessionListPanel-lu3oy0hw.js +0 -1
  678. package/static/dist/assets/SettingsLayout-o9bv4mgb.js +0 -1
  679. package/static/dist/assets/States-eq6fe29m.js +0 -1
  680. package/static/dist/assets/Toaster-pc247u9f.js +0 -1
  681. package/static/dist/assets/_app-f8n1i0m1.js +0 -1
  682. package/static/dist/assets/_app.ext._extensionId-gfbzpoou.js +0 -1
  683. package/static/dist/assets/_app.index-bzzv80hk.js +0 -1
  684. package/static/dist/assets/_app.insights-dzixbryi.js +0 -1
  685. package/static/dist/assets/_app.kanban-c6hep2us.js +0 -1
  686. package/static/dist/assets/_app.logs-i69qds98.js +0 -1
  687. package/static/dist/assets/_app.memory-kl4vf0gd.js +0 -1
  688. package/static/dist/assets/_app.profiles-b8zinmyp.js +0 -1
  689. package/static/dist/assets/_app.session._sessionId-fn1bhde6.js +0 -1
  690. package/static/dist/assets/_app.settings-cg8plwmf.js +0 -1
  691. package/static/dist/assets/_app.settings._section-bkwbi9zn.js +0 -3
  692. package/static/dist/assets/_app.skills-cs47sbvs.js +0 -1
  693. package/static/dist/assets/_app.tasks-dwvk64tc.js +0 -1
  694. package/static/dist/assets/_app.todos-c8xjm8p3.js +0 -1
  695. package/static/dist/assets/_app.workspaces-ehvd1gds.js +0 -1
  696. package/static/dist/assets/abnfDiagram-VCTEODGH-c8kdixke.js +0 -1
  697. package/static/dist/assets/architecture-7GRP2DOG-6crvnylg.js +0 -1
  698. package/static/dist/assets/architectureDiagram-5GKGNRK7-cku06x1p.js +0 -36
  699. package/static/dist/assets/back-h7aun7ew.js +0 -1
  700. package/static/dist/assets/cancel-fhi0vcnu.js +0 -1
  701. package/static/dist/assets/channel-b2zo73z1.js +0 -1
  702. package/static/dist/assets/chunk-DU6HZSFF-ofajdixh.js +0 -128
  703. package/static/dist/assets/chunk-OSK3NFVY-e7gkikcw.js +0 -10
  704. package/static/dist/assets/classDiagram-ZZMXUADV-oe99st8x.js +0 -1
  705. package/static/dist/assets/classDiagram-v2-VYDZK3BY-oe99st8x.js +0 -1
  706. package/static/dist/assets/copied-hcplv46p.js +0 -1
  707. package/static/dist/assets/copy-magtofyp.js +0 -1
  708. package/static/dist/assets/cose-bilkent-JH36ORCC-eclcy6ja.js +0 -1
  709. package/static/dist/assets/cynefin-OW5HDTMX-siqlulkl.js +0 -1
  710. package/static/dist/assets/cynefinDiagram-5FMLGOSQ-i648tah9.js +0 -62
  711. package/static/dist/assets/cytoscape.esm-bqu53u12.js +0 -321
  712. package/static/dist/assets/dagre-GXQ25YYZ-mwn6uc17.js +0 -4
  713. package/static/dist/assets/dagre-lkoknbjn.js +0 -1
  714. package/static/dist/assets/diagram-UQ7AKVKN-mekq7zgq.js +0 -41
  715. package/static/dist/assets/diagram-VX7I27RA-xe6r5qfw.js +0 -24
  716. package/static/dist/assets/diagram-Z3DM3KII-nq07dg7k.js +0 -24
  717. package/static/dist/assets/ebnfDiagram-PWID7BFC-c62ipm3g.js +0 -1
  718. package/static/dist/assets/endpoints-klbxod4t.js +0 -64
  719. package/static/dist/assets/erDiagram-RLTQ6QDP-h39kmnzy.js +0 -99
  720. package/static/dist/assets/eventmodeling-NTZA5JFV-obfede4u.js +0 -1
  721. package/static/dist/assets/flowDiagram-HODETNUW-kdpsp1je.js +0 -1
  722. package/static/dist/assets/gitGraph-4MIJSDKK-fr2i7caa.js +0 -1
  723. package/static/dist/assets/highlighted-body-KPVGNVTW-jwpop9p8.js +0 -1
  724. package/static/dist/assets/host-i828xuo7.js +0 -1
  725. package/static/dist/assets/index-eh8zilnv.css +0 -2
  726. package/static/dist/assets/index-r648f4o1.js +0 -11
  727. package/static/dist/assets/info-A6RAGUB7-h11x0dxo.js +0 -1
  728. package/static/dist/assets/infoDiagram-27XIBGKW-dfh47dw9.js +0 -2
  729. package/static/dist/assets/lazyRouteComponent-e1y4t2i9.js +0 -1
  730. package/static/dist/assets/linear-becwlnhk.js +0 -1
  731. package/static/dist/assets/link-oot2yn73.js +0 -1
  732. package/static/dist/assets/login-gu28l9ou.js +0 -1
  733. package/static/dist/assets/mermaid-HWGCJPDP-mtwrj8fn.js +0 -1
  734. package/static/dist/assets/mermaid-parser.core-ntne45i9.js +0 -7
  735. package/static/dist/assets/not-found-ccnnac39.js +0 -1
  736. package/static/dist/assets/onboarding-lkphzu1s.js +0 -1
  737. package/static/dist/assets/onboarding_model_label-ftpjdwqk.js +0 -1
  738. package/static/dist/assets/packet-AYTQ26CC-l6jdka8p.js +0 -1
  739. package/static/dist/assets/pegDiagram-XKGWAZYB-g1er3bxj.js +0 -1
  740. package/static/dist/assets/pie-WAS4IAKB-i0ylcrt9.js +0 -1
  741. package/static/dist/assets/pieDiagram-E7YTZNPT-fbz1e55z.js +0 -39
  742. package/static/dist/assets/radar-RG4KPBEZ-f9b70aa4.js +0 -1
  743. package/static/dist/assets/railroad-74A4TZTK-lgsuoeda.js +0 -1
  744. package/static/dist/assets/railroad-abnf-HS5TGJTU-nf9d6pdx.js +0 -1
  745. package/static/dist/assets/railroad-ebnf-LZEXJU2U-f7ummkt5.js +0 -1
  746. package/static/dist/assets/railroad-peg-WCYAUIDC-oczq5tqs.js +0 -1
  747. package/static/dist/assets/railroadDiagram-O6MQD6OU-eqpy9fnz.js +0 -1
  748. package/static/dist/assets/refresh-abtu93ke.js +0 -1
  749. package/static/dist/assets/refresh-cw-hlwb02qv.js +0 -1
  750. package/static/dist/assets/requirementDiagram-BXWQKSXE-ogf6puhi.js +0 -84
  751. package/static/dist/assets/rolldown-runtime-b9miacsj.js +0 -1
  752. package/static/dist/assets/root-mbklpuv4.js +0 -1
  753. package/static/dist/assets/saved-girnabkf.js +0 -1
  754. package/static/dist/assets/settings_section_extensions_title-h14cwk6f.js +0 -1
  755. package/static/dist/assets/share._token-kl0r7ido.js +0 -2
  756. package/static/dist/assets/sizeCapture-INFHLROL-g612l48j.js +0 -1
  757. package/static/dist/assets/sliders-horizontal-n1zm34qq.js +0 -1
  758. package/static/dist/assets/src-hjyib4mu.js +0 -1
  759. package/static/dist/assets/sse-iwku0sjo.js +0 -1
  760. package/static/dist/assets/stateDiagram-D77RDMKH-fyoznpcs.js +0 -1
  761. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-ixhx8r46.js +0 -1
  762. package/static/dist/assets/swimlanes-42K2YHIH-oefldhfy.js +0 -1
  763. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-h9ersk2e.js +0 -8
  764. package/static/dist/assets/todoStore-jjr5yr78.js +0 -1
  765. package/static/dist/assets/treeView-Q6P3EWNA-f7hcci5o.js +0 -1
  766. package/static/dist/assets/treemap-WGGIJYW6-nl85071a.js +0 -1
  767. package/static/dist/assets/units-kgxzxfhi.js +0 -1
  768. package/static/dist/assets/untitled-cikfjntn.js +0 -1
  769. package/static/dist/assets/useForm-kqoqg0k9.js +0 -1
  770. package/static/dist/assets/useLocale-o59tbiea.js +0 -1
  771. package/static/dist/assets/vennDiagram-4TSXK5OY-h5hzbk0n.js +0 -34
  772. package/static/dist/assets/wardley-WFR3VGLG-jftvpm3e.js +0 -1
  773. package/static/dist/assets/wardleyDiagram-VM6X3IG4-hedh85ge.js +0 -78
  774. /package/static/dist/assets/{ChatPage-gmu5c7lf.css → TerminalPanel-gmu5c7lf.css} +0 -0
@@ -0,0 +1,1116 @@
1
+ """Service identities and backend recovery extending agent_mutation's native lease.
2
+
3
+ Bare/Windows gateways keep the native journal. A checkout-locked companion owns
4
+ POSIX supervisor obligations and verifies actual gateway/backend readiness.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import base64
9
+ import contextlib
10
+ import hashlib
11
+ import http.client
12
+ import ipaddress
13
+ import json
14
+ import math
15
+ import os
16
+ from pathlib import Path
17
+ import plistlib
18
+ import re
19
+ import shlex
20
+ import signal
21
+ import stat
22
+ import subprocess
23
+ import sys
24
+ import tempfile
25
+ import threading
26
+ import time
27
+
28
+ STOP_SECONDS = 30.0
29
+ READY_SECONDS = 30.0
30
+ SNAPSHOT_ATTEMPTS = 4
31
+ # Bounds a native drain configured to wait indefinitely; the retained debt outlives it.
32
+ GATEWAY_DRAIN_CEILING = 4 * 3600.0
33
+ # One orphan-recovery attempt must fit its 660-second recovery RPC; its debt drives the next attempt.
34
+ RECOVERY_DRAIN_SECONDS = 300.0
35
+
36
+
37
+ @contextlib.contextmanager
38
+ def _native_home(home):
39
+ """Resolve native per-profile configuration for one home in this single-purpose worker."""
40
+ previous = os.environ.get('HERMES_HOME')
41
+ os.environ['HERMES_HOME'] = str(home)
42
+ try:
43
+ from hermes_constants import reset_hermes_home_override, set_hermes_home_override
44
+ except ImportError: # Older Agents resolve the home from the environment alone.
45
+ set_hermes_home_override = None
46
+ token = set_hermes_home_override(home) if set_hermes_home_override else None
47
+ try:
48
+ yield
49
+ finally:
50
+ if token is not None:
51
+ reset_hermes_home_override(token)
52
+ if previous is None:
53
+ os.environ.pop('HERMES_HOME', None)
54
+ else:
55
+ os.environ['HERMES_HOME'] = previous
56
+
57
+
58
+ def drain_budget(pause, homes, ceiling=GATEWAY_DRAIN_CEILING):
59
+ """One shared drain budget: the longest native after-turn + stop-drain budget among the homes, bounded."""
60
+ budget = 0.0
61
+ for home in sorted({str(home) for home in homes}):
62
+ with _native_home(home):
63
+ native = float(pause._drain_budget())
64
+ # A native wait-indefinitely budget (inf or NaN) takes the ceiling before any comparison.
65
+ budget = max(budget, native if math.isfinite(native) else ceiling)
66
+ return min(budget, ceiling)
67
+
68
+
69
+ @contextlib.contextmanager
70
+ def gateway_fence(entry):
71
+ from gateway import status
72
+ home = Path(entry['home'])
73
+ path = status._get_gateway_lock_path(home / 'gateway.pid')
74
+ with os.fdopen(os.open(path, os.O_RDWR | os.O_NOFOLLOW | os.O_NONBLOCK), 'r+') as fence:
75
+ info = os.fstat(fence.fileno())
76
+ if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
77
+ raise RuntimeError(f'{_name(entry)} gateway lock is not an exclusive regular file; recovery remains owed.')
78
+ if not status._try_acquire_file_lock(fence):
79
+ yield None
80
+ return
81
+ try:
82
+ # A live, explicitly non-gateway owner keeps native stale-PID cleanup
83
+ # from replacing this held lock inode. The companion debt owns recovery.
84
+ opened, current = os.fstat(fence.fileno()), path.stat()
85
+ if (opened.st_dev, opened.st_ino) != (current.st_dev, current.st_ino):
86
+ raise RuntimeError(f'{_name(entry)} gateway runtime fence changed; recovery remains owed.')
87
+ marker = {**status._build_pid_record(), 'kind': 'talaria-update-fence', 'hermes_home': str(home)}
88
+ if marker.get('start_time') is None:
89
+ raise RuntimeError(f'{_name(entry)} updater birth identity is unverified; recovery remains owed.')
90
+ fence.seek(0)
91
+ fence.truncate()
92
+ json.dump(marker, fence)
93
+ fence.flush()
94
+ os.fsync(fence.fileno())
95
+ yield fence
96
+ finally:
97
+ status._release_file_lock(fence)
98
+
99
+
100
+ def _verify_fence(entry, fence):
101
+ from gateway import status
102
+ path = status._get_gateway_lock_path(Path(entry['home']) / 'gateway.pid')
103
+ try:
104
+ opened, current = os.fstat(fence.fileno()), path.stat()
105
+ if (opened.st_dev, opened.st_ino) != (current.st_dev, current.st_ino) or current.st_nlink != 1:
106
+ raise ValueError('replaced or linked inode')
107
+ marker = json.loads(path.read_text())
108
+ if (marker.get('kind') != 'talaria-update-fence' or marker.get('pid') != os.getpid()
109
+ or marker.get('start_time') != status.get_process_start_time(os.getpid())):
110
+ raise ValueError('changed owner')
111
+ except (OSError, ValueError, TypeError, AttributeError) as exc:
112
+ raise RuntimeError(f'{_name(entry)} gateway runtime fence changed; recovery remains owed.') from exc
113
+
114
+
115
+ def recovery_path(root, home):
116
+ key = hashlib.sha256(os.fsencode(Path(root).resolve())).hexdigest()
117
+ return Path(home) / '.talaria-agent-services' / (key + '.json')
118
+
119
+
120
+ def _process_table():
121
+ result = subprocess.run(['ps', '-A', '-o', 'pid=,ppid='], capture_output=True, text=True, timeout=10, check=True)
122
+ parents = {}
123
+ for line in result.stdout.splitlines():
124
+ fields = line.split()
125
+ if len(fields) != 2 or not all(f.isdigit() for f in fields):
126
+ raise RuntimeError('The process ancestry inventory is unreadable.')
127
+ parents[int(fields[0])] = int(fields[1])
128
+ if not parents:
129
+ raise RuntimeError('The process ancestry inventory is empty.')
130
+ return parents
131
+
132
+
133
+ def _ancestors(pid, parents):
134
+ result = []
135
+ while pid > 1:
136
+ if pid in result or pid not in parents:
137
+ raise RuntimeError(f'Process {pid} ancestry could not be proved.')
138
+ result.append(pid)
139
+ pid = parents[pid]
140
+ return result
141
+
142
+
143
+ def _descendants(root, parents):
144
+ found, pending = set(), [root]
145
+ while pending:
146
+ pid = pending.pop()
147
+ if pid in found:
148
+ raise RuntimeError('The process inventory contains an ancestry cycle.')
149
+ found.add(pid)
150
+ pending.extend(p for p, parent in parents.items() if parent == pid)
151
+ return found
152
+
153
+
154
+ def _name(entry):
155
+ return entry.get('label') or entry.get('unit') or f"PID {entry['pid']}"
156
+
157
+
158
+ def _service_key(entry):
159
+ return (entry['kind'], entry.get('domain') or entry.get('scope'), entry.get('label') or entry.get('unit'))
160
+
161
+
162
+ def snapshot(entry, *, records=None, expected=None):
163
+ """Attest a complete live tree, retrying only proved exits/new discoveries."""
164
+ from gateway.status import get_process_start_time, _pid_exists
165
+ service, pid = entry.get('service_pid', entry['pid']), entry['pid']
166
+ anchors = {service, pid}
167
+ observed = dict(expected or {})
168
+ seen_parents = {int(p): proof['parent'] for p, proof in observed.items() if 'parent' in proof}
169
+ departed = set()
170
+
171
+ def exited(p):
172
+ if _pid_exists(p) is False:
173
+ if p in anchors:
174
+ raise RuntimeError(f'{_name(entry)} anchor process {p} disappeared during discovery; source was not changed.')
175
+ departed.add(p)
176
+ return True
177
+ return False
178
+
179
+ def inventory():
180
+ parents = _process_table()
181
+ if service not in _ancestors(pid, parents):
182
+ raise RuntimeError(f'{_name(entry)} does not own backend/gateway PID {pid}.')
183
+ if service in _ancestors(os.getpid(), parents):
184
+ raise RuntimeError(f'{_name(entry)} owns the updater or an ancestor; source was not changed.')
185
+ for p, parent in seen_parents.items():
186
+ if parents.get(p) != parent:
187
+ if p not in parents and exited(p):
188
+ continue
189
+ raise RuntimeError(f'{_name(entry)} process {p} identity/ancestry changed during discovery; source was not changed.')
190
+ descendants = _descendants(service, parents)
191
+ seen_parents.update({p: parents[p] for p in descendants})
192
+ # Zombies may remain in ps. Recheck departed PIDs at every inventory so
193
+ # neither a recycled PID nor unknown liveness can be silently excluded.
194
+ if any(_pid_exists(p) is not False for p in departed):
195
+ raise RuntimeError(f'{_name(entry)} a departed descendant PID became live or unreadable; source was not changed.')
196
+ return parents, descendants - departed
197
+
198
+ def read(p, parents):
199
+ if exited(p):
200
+ return None
201
+ proof = {'birth': get_process_start_time(p), 'parent': parents[p]}
202
+ if records is not None:
203
+ proof['ct'] = records.identity(p)['ct']
204
+ if proof['birth'] is None or (records is not None and proof['ct'] is None):
205
+ if exited(p):
206
+ return None
207
+ raise RuntimeError(f'{_name(entry)} process {p} birth identity is live or unreadable; source was not changed.')
208
+ if any(proof.get(key) != value for key, value in observed.get(str(p), {}).items()):
209
+ raise RuntimeError(f'{_name(entry)} process {p} identity/ancestry changed during discovery; source was not changed.')
210
+ observed[str(p)] = proof
211
+ return proof
212
+
213
+ for _ in range(SNAPSHOT_ATTEMPTS):
214
+ parents, live = inventory()
215
+ proofs = {str(p): read(p, parents) for p in sorted(live)}
216
+ after, remaining = inventory()
217
+ for p in sorted(live & remaining):
218
+ read(p, after)
219
+ _, final = inventory()
220
+ if live & departed or remaining != live or final != live:
221
+ continue
222
+ return proofs
223
+ raise RuntimeError(f'{_name(entry)} process inventory kept changing after {SNAPSHOT_ATTEMPTS} samples; retry when descendants settle. Source was not changed and recovery remains owed.')
224
+
225
+
226
+ def _gateway_home_matches(home, runtime, pause):
227
+ """Keep native identity policy, decoding only native-recognized inline launchers."""
228
+ from gateway import status
229
+ home = Path(home).resolve()
230
+ observed = pause._home_of(runtime[0])
231
+ command = status._read_process_cmdline(runtime[0])
232
+ if not observed or Path(observed).resolve() != home or not command:
233
+ return False
234
+ try:
235
+ from gateway.status_inline_source import command_line_runs_inline_source, inline_bootstrap_argv
236
+ except ImportError:
237
+ return False # Older Agents cannot prove this launcher; retain recovery debt.
238
+ try:
239
+ tokens = [t.strip("\"'").replace('\\', '/') for t in shlex.split(command, posix=False)]
240
+ except ValueError:
241
+ return False
242
+ if not command_line_runs_inline_source(tokens):
243
+ return pause._gateway_on_home(str(home), set()) == runtime[0]
244
+ argv = inline_bootstrap_argv(tokens)
245
+ if argv is None:
246
+ return False
247
+ for index, arg in enumerate(argv):
248
+ if arg.startswith('HERMES_HOME='):
249
+ if not status._same_hermes_home(arg.removeprefix('HERMES_HOME='), home):
250
+ return False
251
+ argv[index] = 'HERMES_HOME=' + str(home)
252
+ effective = ' '.join(argv)
253
+ if (not status.looks_like_gateway_command_line(effective)
254
+ or not status._command_line_belongs_to_profile(effective, home)
255
+ or status._process_is_stopped(runtime[0])):
256
+ return False
257
+ profile = status.profile_flag_value(effective)
258
+ if profile is not None and profile != (status._profile_name_for_home(home) or 'default'):
259
+ return False
260
+ pid_path = home / 'gateway.pid'
261
+ records = [status._read_gateway_lock_record(status._get_gateway_lock_path(pid_path))]
262
+ if pid_path.exists():
263
+ records.append(status._read_pid_record(pid_path))
264
+ if any(not record or status.recorded_gateway_home_conflicts(record, expected_home=home) for record in records):
265
+ return False
266
+ return status.get_running_pid_identity_strict(pid_path) == runtime
267
+
268
+
269
+ def gateway_entries(entries, homes, pause, *, allow_starting=False):
270
+ if not entries:
271
+ return []
272
+ from gateway.status import get_running_pid_identity_strict
273
+ found = {}
274
+ for original in entries:
275
+ entry = dict(original)
276
+ home = Path(entry['home']).resolve() if entry.get('home') else None
277
+ if home not in homes:
278
+ raise RuntimeError(f'{_name(entry)} gateway owning profile is unknown or outside this Web instance.')
279
+ runtime = get_running_pid_identity_strict(home / 'gateway.pid')
280
+ if runtime is None and allow_starting:
281
+ continue # Lock admission can precede PID publication; callers retain custody while waiting.
282
+ if runtime is None or not _gateway_home_matches(home, runtime, pause):
283
+ raise RuntimeError(f'{_name(entry)} has no verified gateway in its owning profile.')
284
+ entry.update(service_pid=entry['pid'], pid=runtime[0], ct=runtime[1], home=str(home))
285
+ entry['processes'] = snapshot(entry, expected={str(entry['pid']): {'birth': entry['ct']}})
286
+ key = _service_key(entry)
287
+ if key in found and found[key] != entry:
288
+ raise RuntimeError(f'{_name(entry)} has conflicting discovery identities.')
289
+ found[key] = entry
290
+ if len({e['pid'] for e in found.values()}) != len(found):
291
+ raise RuntimeError('More than one service claims the same gateway process.')
292
+ return list(found.values())
293
+
294
+
295
+ def _plist(domain, label, argv, dashboard):
296
+ matches = []
297
+ for kind, directory in dashboard._launchd_plist_dirs():
298
+ if (domain == 'system') != (kind == 'daemon'):
299
+ continue
300
+ path = directory / (label + '.plist')
301
+ try:
302
+ data = plistlib.loads(path.read_bytes())
303
+ except FileNotFoundError:
304
+ continue
305
+ if data.get('Label') == label and data.get('ProgramArguments') == argv:
306
+ matches.append(path.resolve())
307
+ if len(set(matches)) != 1:
308
+ raise RuntimeError(f'{label} original launchd configuration could not be proved.')
309
+ return str(matches[0])
310
+
311
+
312
+ def _backend_identity(row, homes, identity):
313
+ from hermes_cli import dashboard_procs, update_inventory, update_receipt
314
+ if row.get('create_time') is None or identity._pid_alive_matches(row['pid'], row['create_time'], strict=True) is not True:
315
+ raise RuntimeError(f"Agent backend PID {row['pid']} birth identity is unverified.")
316
+ env = dashboard_procs._pid_environ(row['pid'])
317
+ if env is None:
318
+ raise RuntimeError(f"Agent backend PID {row['pid']} ownership environment is unreadable.")
319
+ # Native is_desktop_owned_backend is bound to the caller's environment, not this target PID.
320
+ desktop = env.get('HERMES_DESKTOP') == '1' and bool(env.get('HERMES_DASHBOARD_SESSION_TOKEN'))
321
+ if desktop or row.get('isolated') or update_inventory._is_desktop_ssh_ledger_entry(row):
322
+ raise RuntimeError(f"Agent backend PID {row['pid']} is Desktop/isolated-owned; its owner must stop it.")
323
+ observed = dashboard_procs._hermes_home_for_pid(row['pid'])
324
+ home = Path(observed).resolve() if observed else None
325
+ if home not in homes or (row.get('hermes_home') and Path(row['hermes_home']).resolve() != home):
326
+ raise RuntimeError(f"Agent backend PID {row['pid']} has an unknown or foreign owning profile.")
327
+ profiles = {name for name, path in update_receipt._profile_homes() if Path(path).resolve() == home}
328
+ if len(profiles) != 1 or (row.get('profile') and row['profile'] not in profiles):
329
+ raise RuntimeError(f"Agent backend PID {row['pid']} has conflicting or ambiguous profile ownership.")
330
+ if identity._pid_alive_matches(row['pid'], row['create_time'], strict=True) is not True:
331
+ raise RuntimeError(f"Agent backend PID {row['pid']} birth identity changed during discovery.")
332
+ return home, profiles.pop()
333
+
334
+
335
+ def backend_entry(row, homes, pause, identity):
336
+ from hermes_cli import main_dashboard as dashboard
337
+ from hermes_cli import update_inventory
338
+ home, profile = _backend_identity(row, homes, identity)
339
+ parents = _process_table()
340
+ ancestors = _ancestors(row['pid'], parents)
341
+ jobs = dashboard._loaded_launchd_backend_jobs()
342
+ # Native ownership also admits argv-only matches; this lease requires a live ancestry witness.
343
+ candidates = [job for job in jobs if job[3] in ancestors]
344
+ owner = dashboard._launchd_job_owning_backend(row['pid'], shlex.split(row.get('argv') or ''), candidates, ancestors=ancestors)
345
+ entry = dict(pid=row['pid'], purpose=row['purpose'], home=str(home), profile=profile,
346
+ host=row.get('host'), port=row.get('port'))
347
+ if owner:
348
+ domain, label, service_pid = owner
349
+ if len({(j[0], j[1]) for j in candidates}) != 1:
350
+ raise RuntimeError(f"Backend PID {row['pid']} has ambiguous launchd ownership.")
351
+ argv = next(j[2] for j in candidates if j[:2] == owner[:2])
352
+ entry.update(kind='launchd', domain=domain, label=label, service_pid=service_pid,
353
+ plist=_plist(domain, label, argv, dashboard))
354
+ else:
355
+ service_pid = next((p for p in ancestors if dashboard._get_systemd_service_for_pid(p)), None)
356
+ if service_pid is None:
357
+ raise RuntimeError(f"Agent backend PID {row['pid']} has no manageable supervised service; stop it through its owner.")
358
+ unit = dashboard._get_systemd_service_for_pid(service_pid)
359
+ cgroup = dashboard._get_pid_cgroup_path(service_pid)
360
+ scope = dashboard._extract_scope_from_cgroup(cgroup or '')
361
+ if scope not in {'user', 'system'} or pause._manage_cmd(scope) is None:
362
+ raise RuntimeError(f'{unit} scope/management authority is unverified.')
363
+ from hermes_cli.update_fleet_scope import systemd_unit_hermes_home
364
+ scope_cmd = ['systemctl', '--user'] if scope == 'user' else ['systemctl']
365
+ declared_home = systemd_unit_hermes_home(scope_cmd, unit)
366
+ if (not update_inventory._serve_unit_matches_profile(entry['profile'], unit)
367
+ or not declared_home or Path(declared_home).resolve() != home):
368
+ raise RuntimeError(f'{unit} does not prove ownership of this backend profile; source was not changed.')
369
+ entry.update(kind='systemd', unit=unit, scope=scope, cgroup=cgroup, service_pid=service_pid)
370
+ _endpoint(entry)
371
+ entry['processes'] = snapshot(entry)
372
+ if identity._pid_alive_matches(row['pid'], row['create_time'], strict=True) is not True:
373
+ raise RuntimeError(f"Agent backend PID {row['pid']} birth identity changed during discovery.")
374
+ return entry
375
+
376
+
377
+ def _endpoint(entry):
378
+ host, port = entry.get('host'), entry.get('port')
379
+ if not isinstance(host, str) or not isinstance(port, int) or isinstance(port, bool) or not 0 < port <= 65535:
380
+ raise RuntimeError(f'{_name(entry)} backend endpoint is unknown.')
381
+ if host == 'localhost':
382
+ return '127.0.0.1', port
383
+ try:
384
+ address = ipaddress.ip_address(host)
385
+ except ValueError as exc:
386
+ raise RuntimeError(f'{_name(entry)} backend bind address is not a verified IP address.') from exc
387
+ return ('::1' if address.version == 6 else '127.0.0.1') if address.is_unspecified else host, port
388
+
389
+
390
+ def _service_pid(entry):
391
+ if entry['kind'] == 'launchd':
392
+ from hermes_cli.gateway import _launchd_print_service_pid
393
+ loaded, pid = _launchd_print_service_pid(entry['domain'], entry['label'])
394
+ return loaded, pid
395
+ from hermes_cli.update_cmd_fleet import _systemctl, _unit_main_pid
396
+ cmd = ['systemctl', '--user'] if entry['scope'] == 'user' else ['systemctl']
397
+ state = _systemctl(cmd + ['show', entry['unit'], '--property=ActiveState', '--value'], timeout=10)
398
+ if state.returncode:
399
+ raise RuntimeError(f'{_name(entry)} supervisor state is unreadable.')
400
+ active = state.stdout.strip() not in {'inactive', 'failed'}
401
+ return active, _unit_main_pid(cmd, entry['unit']) or None
402
+
403
+
404
+ def verify_processes(entry, records, *, stopping=False, fenced=False):
405
+ from gateway.status import get_process_start_time, _pid_exists
406
+ parents = _process_table()
407
+ departed = set()
408
+ for pid, expected in entry['processes'].items():
409
+ pid = int(pid)
410
+ if _pid_exists(pid) is False:
411
+ if stopping:
412
+ departed.add(pid)
413
+ continue
414
+ raise RuntimeError(f'{_name(entry)} process {pid} disappeared before its stop.')
415
+ if (get_process_start_time(pid) != expected['birth'] or records.identity(pid)['ct'] != expected.get('ct')
416
+ or parents.get(pid) != expected['parent']):
417
+ if stopping and _pid_exists(pid) is False:
418
+ departed.add(pid)
419
+ continue
420
+ raise RuntimeError(f'{_name(entry)} process {pid} identity/ancestry changed; source was not changed.')
421
+ _, service_pid = _service_pid(entry)
422
+ if service_pid and service_pid != entry['service_pid']:
423
+ if fenced:
424
+ return False
425
+ raise RuntimeError(f'{_name(entry)} supervisor respawned during quiescence; source was not changed.')
426
+ parents = _process_table()
427
+ if any(_pid_exists(pid) is not False for pid in departed):
428
+ raise RuntimeError(f'{_name(entry)} process identity/ancestry changed after departure; source was not changed.')
429
+ for pid, expected in entry['processes'].items():
430
+ if parents.get(int(pid)) != expected['parent']:
431
+ if stopping and _pid_exists(int(pid)) is False:
432
+ continue
433
+ raise RuntimeError(f'{_name(entry)} process {pid} identity/ancestry changed; source was not changed.')
434
+ if service_pid:
435
+ for pid in _descendants(service_pid, parents):
436
+ if str(pid) not in entry['processes']:
437
+ if fenced:
438
+ return False
439
+ raise RuntimeError(f'{_name(entry)} acquired an unrecorded descendant {pid}; source was not changed.')
440
+ return True
441
+
442
+
443
+ def bind_processes(entry, records):
444
+ if entry.get('processes'):
445
+ entry['processes'] = snapshot(entry, records=records, expected=entry['processes'])
446
+ verify_processes(entry, records)
447
+
448
+
449
+ def service_entry(entry):
450
+ return {**entry, 'pid': entry.get('service_pid', entry['pid']), 'runtime_pid': entry['pid']}
451
+
452
+
453
+ def wait_stopped(entry, records, stopped, *, writers_only=False):
454
+ pids = {pid: p for pid, p in entry['processes'].items()
455
+ if not writers_only or int(pid) != entry['service_pid'] or int(pid) == entry['pid']}
456
+ deadline = time.monotonic() + STOP_SECONDS
457
+ while any(records.identity_is_live({'pid': int(pid), 'ct': p['ct']}) for pid, p in pids.items()):
458
+ if stopped.wait(.05) or time.monotonic() >= deadline:
459
+ raise RuntimeError(f'{_name(entry)} graceful stop has not completed; source was not changed and recovery remains owed.')
460
+ if not writers_only and any(_service_pid(entry)):
461
+ raise RuntimeError(f'{_name(entry)} supervisor is still active; source was not changed and recovery remains owed.')
462
+
463
+
464
+ def verify_listener(entry):
465
+ """A legacy HTTP response has no PID identity; require OS socket ownership as well."""
466
+ _endpoint(entry)
467
+ pid, port = entry['pid'], entry['port']
468
+ if sys.platform.startswith('linux'):
469
+ owned = set()
470
+ for path in Path(f'/proc/{pid}/fd').iterdir():
471
+ with contextlib.suppress(FileNotFoundError):
472
+ owned.add(os.readlink(path))
473
+ listeners = set()
474
+ for table in ('tcp', 'tcp6'):
475
+ try:
476
+ rows = Path('/proc/net/' + table).read_text().splitlines()[1:]
477
+ except FileNotFoundError:
478
+ if table == 'tcp6':
479
+ continue
480
+ raise
481
+ for line in rows:
482
+ fields = line.split()
483
+ if int(fields[1].split(':')[1], 16) == port and fields[3] == '0A':
484
+ listeners.add('socket:[' + fields[9] + ']')
485
+ matched = bool(listeners) and listeners <= owned
486
+ elif sys.platform == 'darwin':
487
+ result = subprocess.run(['/usr/sbin/lsof', '-nP', f'-iTCP:{port}', '-sTCP:LISTEN', '-Fp'],
488
+ capture_output=True, text=True, timeout=10, check=False)
489
+ matched = result.returncode == 0 and {int(line[1:]) for line in result.stdout.splitlines() if line.startswith('p')} == {pid}
490
+ else:
491
+ matched = False
492
+ if not matched:
493
+ raise RuntimeError(f'{_name(entry)} legacy listener ownership could not be proved; source was not changed.')
494
+
495
+
496
+ def _auth(entry):
497
+ from gateway import host_rendezvous as host
498
+ from hermes_cli.dashboard_procs import _pid_environ
499
+ env = _pid_environ(entry['pid']) or {}
500
+ token = env.get('HERMES_DASHBOARD_SESSION_TOKEN') or ''
501
+ record = host.read_record(host.ROLE_SERVE)
502
+ if (record is not None and record.pid == entry['pid'] and record.home
503
+ and Path(record.home).resolve() == Path(entry['home']).resolve()
504
+ and record.port == entry['port'] and record.host == entry['host']
505
+ and host.liveness_is_proven(record) and host.record_token_is_consistent(record)):
506
+ token = host.read_token(host.ROLE_SERVE)
507
+ return token
508
+
509
+
510
+ def _local_session(entry):
511
+ """Use the owning user's existing native basic-auth key for a short-lived local lifecycle call."""
512
+ from hermes_cli.dashboard_procs import _pid_environ
513
+ try:
514
+ from plugins.dashboard_auth import basic
515
+ except ImportError:
516
+ return None
517
+ from .home import scoped_home
518
+ from agent import secret_scope
519
+ from hermes_cli.env_loader import hydrate_profile_secret_sources
520
+ env = _pid_environ(entry['pid'])
521
+ if env is None:
522
+ return None
523
+ home = Path(entry['home'])
524
+ with scoped_home(home):
525
+ hydrate_profile_secret_sources(home)
526
+ # The launch scope also contains updater credentials; use only the owner's file-backed scope.
527
+ scope = secret_scope.build_profile_secret_scope(home)
528
+ scope.update({name: env[name] for name in ('HERMES_DASHBOARD_BASIC_AUTH_SECRET', 'HERMES_DASHBOARD_BASIC_AUTH_USERNAME')
529
+ if env.get(name, '').strip()})
530
+ token = secret_scope.set_secret_scope(scope)
531
+ # Config ${...} references must also refuse launch-environment fallback, including on the main profile.
532
+ multiplex = secret_scope.set_multiplex_context(True)
533
+ try:
534
+ section = basic._load_config_basic_auth_section()
535
+ finally:
536
+ secret_scope.reset_multiplex_context(multiplex)
537
+ secret_scope.reset_secret_scope(token)
538
+
539
+ def setting(name, key):
540
+ return str(scope.get(name) or '').strip() or str(section.get(key) or '').strip()
541
+
542
+ secret = setting('HERMES_DASHBOARD_BASIC_AUTH_SECRET', 'secret')
543
+ username = setting('HERMES_DASHBOARD_BASIC_AUTH_USERNAME', 'username')
544
+ if not secret or not username:
545
+ return None # A process-random key is not reproducible; never manufacture a replacement key.
546
+ # Native _resolve_secret reads os.environ directly. Match its base64/hex/raw order without mutating it.
547
+ key = secret.encode('utf-8')
548
+ for decoder in (base64.b64decode, bytes.fromhex):
549
+ try:
550
+ decoded = decoder(secret)
551
+ if len(decoded) >= 16:
552
+ key = decoded
553
+ break
554
+ except (ValueError, TypeError):
555
+ pass
556
+ return basic._sign({'sub': username, 'kind': 'access', 'exp': int(time.time()) + 60}, key)
557
+
558
+
559
+ class BackendLifecycleUnsupported(RuntimeError):
560
+ """Terminal capability refusal, distinct from retryable recovery readiness."""
561
+
562
+
563
+ def backend_request(entry, action=None, permit=None):
564
+ host, port = _endpoint(entry)
565
+ token = _auth(entry)
566
+ headers = {'X-Hermes-Session-Token': token, 'Authorization': 'Bearer ' + token}
567
+
568
+ def request(path, body=None, *, authenticated=True, limit=65536):
569
+ for attempt in range(2):
570
+ connection = http.client.HTTPConnection(host, port, timeout=2)
571
+ try:
572
+ sent = {**headers, 'Content-Type': 'application/json'} if authenticated else {}
573
+ connection.request('GET' if body is None else 'POST', path, body, sent)
574
+ response = connection.getresponse()
575
+ data = response.read(limit + 1)
576
+ if authenticated and response.status == 401 and attempt == 0:
577
+ credential = _local_session(entry)
578
+ if credential:
579
+ headers['Authorization'] = 'Bearer ' + credential
580
+ continue
581
+ if authenticated and response.status in {401, 403}:
582
+ raise RuntimeError(f'{_name(entry)} refused authenticated backend lifecycle access (HTTP {response.status}); source was not changed.')
583
+ if len(data) > limit:
584
+ raise ValueError('oversized response')
585
+ return response.status, json.loads(data) if response.status == 200 else None
586
+ finally:
587
+ connection.close()
588
+
589
+ def verified_identity(status, result):
590
+ return (status == 200 and isinstance(result, dict) and result.get('pid') == entry['pid']
591
+ and result.get('role') == 'serve' and result.get('ok') is True)
592
+
593
+ def capability_check(identity_status, identity):
594
+ # The schema belongs to the running endpoint, never the checkout on disk.
595
+ status, schema = request('/openapi.json', limit=4 * 1024 * 1024)
596
+ if (status != 200 or not isinstance(schema, dict) or not isinstance(schema.get('paths'), dict)
597
+ or not isinstance(schema.get('openapi'), str) or not schema['openapi'].startswith('3.')):
598
+ raise RuntimeError(f'{_name(entry)} backend lifecycle/readiness schema could not be verified.')
599
+ paths = schema['paths']
600
+ for path, method in (('/api/host/identity', 'get'), ('/api/health/retirement', 'post')):
601
+ route = paths.get(path, {})
602
+ if not isinstance(route, dict) or (method in route and not isinstance(route[method], dict)):
603
+ raise ValueError('malformed lifecycle schema')
604
+ missing_identity = 'get' not in paths.get('/api/host/identity', {})
605
+ missing_retirement = 'post' not in paths.get('/api/health/retirement', {})
606
+ if not missing_identity and not missing_retirement:
607
+ return
608
+ if verified_identity(identity_status, identity):
609
+ if missing_identity or not verified_identity(*request('/api/host/identity')):
610
+ raise RuntimeError(f'{_name(entry)} backend lifecycle identity/schema changed or disagrees.')
611
+ elif identity_status in {404, 405, 501} and missing_identity:
612
+ challenge, _ = request('/api/sessions?limit=1', authenticated=False)
613
+ accepted, sessions = request('/api/sessions?limit=1')
614
+ if (challenge not in {401, 403} or accepted != 200 or not isinstance(sessions, dict)
615
+ or not isinstance(sessions.get('sessions'), list)
616
+ or request('/api/host/identity')[0] != identity_status):
617
+ return
618
+ else:
619
+ return
620
+ name = ''.join(c for c in str(_name(entry)) if c.isprintable())
621
+ unverified = ' Backend listener identity remains unverified.' if missing_identity else ''
622
+ raise BackendLifecycleUnsupported(
623
+ f'{name} running backend lifecycle APIs are unsupported for safe Agent update custody.' + unverified
624
+ + ' Finish active work, then have the owner gracefully refresh this service through its original supervisor.'
625
+ + ' Verify the successor lifecycle APIs and intended installation/profile/PID, then retry the Agent update.'
626
+ + ' Any retained service recovery remains owed.')
627
+
628
+ try:
629
+ body = None if action is None else json.dumps(dict(action=action, **({'token': permit} if permit else {})))
630
+ status, result = request('/api/host/identity' if action is None else '/api/health/retirement', body)
631
+ if action is None:
632
+ if status == 200 and not verified_identity(status, result):
633
+ raise RuntimeError(f'{_name(entry)} endpoint is not the expected backend process.')
634
+ capability_check(status, result)
635
+ elif status in {404, 405, 501}:
636
+ capability_check(*request('/api/host/identity'))
637
+ if status != 200:
638
+ raise RuntimeError(f'{_name(entry)} backend lifecycle/readiness could not be verified (HTTP {status}).')
639
+ if not isinstance(result, dict):
640
+ raise ValueError('not an object')
641
+ return result
642
+ except (OSError, ValueError, TypeError, http.client.HTTPException) as exc:
643
+ raise RuntimeError(f'{_name(entry)} backend lifecycle/readiness could not be verified.') from exc
644
+
645
+
646
+ def _config_stamp(entry):
647
+ if entry['kind'] == 'launchd':
648
+ return hashlib.sha256(Path(entry['plist']).read_bytes()).hexdigest()
649
+ from hermes_cli.update_cmd_fleet import _systemctl
650
+ cmd = ['systemctl', '--user'] if entry['scope'] == 'user' else ['systemctl']
651
+ result = _systemctl(cmd + ['cat', entry['unit']], timeout=10)
652
+ if result.returncode:
653
+ raise RuntimeError(f'{_name(entry)} original service configuration is unreadable.')
654
+ return hashlib.sha256(result.stdout.encode()).hexdigest()
655
+
656
+
657
+ def _start(entry, pause):
658
+ if _config_stamp(entry) != entry['config_stamp']:
659
+ raise RuntimeError(f'{_name(entry)} service configuration changed; backend recovery remains owed.')
660
+ if entry['kind'] == 'launchd':
661
+ # The native bootstrap helper boots out a concurrently loaded replacement on EIO.
662
+ # This lease must adopt that replacement instead of killing its newly admitted work.
663
+ if not _service_pid(entry)[0]:
664
+ bootstrapped = subprocess.run(['launchctl', 'bootstrap', entry['domain'], entry['plist']],
665
+ stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=30, check=False)
666
+ if bootstrapped.returncode and not _service_pid(entry)[0]:
667
+ raise RuntimeError(f'{_name(entry)} supervisor failed to load its original configuration.')
668
+ result = subprocess.run(['launchctl', 'kickstart', f"{entry['domain']}/{entry['label']}"],
669
+ stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=30, check=False)
670
+ else:
671
+ from hermes_cli.update_cmd_fleet import _systemctl
672
+ cmd = pause._manage_cmd(entry['scope'])
673
+ if cmd is None:
674
+ raise RuntimeError(f'{_name(entry)} cannot be resumed with current service privileges.')
675
+ result = _systemctl(cmd + ['start', entry['unit']], timeout=30)
676
+ if result.returncode:
677
+ raise RuntimeError(f'{_name(entry)} supervisor failed to start its backend.')
678
+
679
+
680
+ class GatewayDraining(RuntimeError):
681
+ """An acknowledged native drain is still within its native budget: progress, not a recovery failure."""
682
+
683
+
684
+ class ServiceRecovery:
685
+ """Companion to the native pause token; all calls hold its existing checkout lock."""
686
+ def __init__(self, root, home, records, identity, pause):
687
+ self.root, self.records, self.identity, self.pause = root, records, identity, pause
688
+ self.path = recovery_path(root, home)
689
+ self._refresh_starts = set() # Finally recovery must not retry a failed bootstrap in this worker.
690
+ try:
691
+ self.state = json.loads(self.path.read_text())
692
+ except FileNotFoundError:
693
+ self.state = {'schema': 1, 'root': str(root), 'entries': [], 'tree': {}, 'sync_failed': False}
694
+ except (OSError, ValueError) as exc:
695
+ raise RuntimeError('Backend recovery evidence is unreadable; source was not changed.') from exc
696
+ if (not isinstance(self.state, dict) or self.state.get('schema') != 1 or self.state.get('root') != str(root)
697
+ or not isinstance(self.state.get('entries'), list) or not isinstance(self.state.get('tree'), dict)
698
+ or ('sync_failed' in self.state and not isinstance(self.state['sync_failed'], bool))):
699
+ raise RuntimeError('Backend recovery evidence has an unknown identity or format; source was not changed.')
700
+ if self.state['entries'] and (not self.state['tree'].get('baselines') or any(
701
+ not isinstance(e, dict) or e.get('purpose') not in {'gateway', 'dashboard', 'serve'}
702
+ or e.get('phase') not in {'recorded', 'retiring', 'stopped', 'refreshing'}
703
+ or (e.get('phase') == 'refreshing' and e.get('legacy_refresh') is not True) or not isinstance(e.get('processes'), dict)
704
+ for e in self.state['entries'])):
705
+ raise RuntimeError('Service recovery lacks complete source/process custody evidence; source was not changed.')
706
+
707
+ transaction = self.state.get('transaction')
708
+ if transaction is not None and (
709
+ not isinstance(transaction, dict) or not re.fullmatch(r'[a-f0-9]{40}', str(transaction.get('revision', '')))
710
+ or not isinstance(transaction.get('homes'), list) or not transaction['homes']
711
+ or any(not isinstance(home, str) or not Path(home).is_absolute() for home in transaction['homes'])
712
+ or any(key in transaction and not isinstance(transaction[key], bool) for key in ('dependencies_verified',))
713
+ or ('runtime_revision' in transaction and not re.fullmatch(r'[a-f0-9]{40}', str(transaction['runtime_revision'])))):
714
+ raise RuntimeError('Service recovery transaction has an unknown identity or format; its evidence is retained.')
715
+
716
+ def save(self, token=None):
717
+ if not self.state['entries'] and not self.state.get('sync_failed') and not self.state.get('transaction'):
718
+ if self.path.exists():
719
+ self.path.unlink()
720
+ self._sync_directory()
721
+ return
722
+ if token is not None:
723
+ self.state['tree'] = json.loads(json.dumps({key: token[key] for key in ('pause_id', 'baselines') if key in token}))
724
+ self.path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
725
+ fd, temporary = tempfile.mkstemp(dir=self.path.parent, prefix=self.path.name + '.')
726
+ try:
727
+ with os.fdopen(fd, 'w') as handle:
728
+ json.dump(self.state, handle)
729
+ handle.flush()
730
+ os.fsync(handle.fileno())
731
+ os.replace(temporary, self.path)
732
+ self._sync_directory()
733
+ finally:
734
+ with contextlib.suppress(FileNotFoundError):
735
+ os.unlink(temporary)
736
+
737
+ def _sync_directory(self):
738
+ if os.name != 'nt':
739
+ fd = os.open(self.path.parent, os.O_RDONLY)
740
+ try:
741
+ os.fsync(fd)
742
+ finally:
743
+ os.close(fd)
744
+
745
+ def prepare(self, entries, token):
746
+ for entry in entries:
747
+ old = next((e for e in self.state['entries'] if _service_key(e) == _service_key(entry)), None)
748
+ if old is not None:
749
+ if old['pid'] != entry['pid']:
750
+ raise RuntimeError(f'{_name(entry)} has unresolved prior backend recovery debt.')
751
+ continue
752
+ self.state['entries'].append({**entry, 'phase': 'recorded', 'config_stamp': _config_stamp(entry)})
753
+ self.save(token)
754
+ gateways = [e for e in self.state['entries'] if e['purpose'] == 'gateway']
755
+ if gateways:
756
+ # Persist custody here before retiring overlapping native obligations. Native replay
757
+ # cannot distinguish a wrapper from its gateway and can rewrite the configured launcher.
758
+ keys = {_service_key(e) for e in gateways}
759
+ pids = {pid for e in gateways for pid in e['processes']}
760
+ transferred = [e for e in token.get('posix_units', []) if _service_key(e) in keys]
761
+ pids.update(str(e['pid']) for e in transferred)
762
+ token['posix_units'] = [e for e in token.get('posix_units', []) if _service_key(e) not in keys]
763
+ token['unmapped'] = [e for e in token.get('unmapped', []) if str(e['pid']) not in pids]
764
+ token['unmapped_pids'] = [e['pid'] for e in token['unmapped']]
765
+ token['identities'] = {pid: ct for pid, ct in token.get('identities', {}).items() if str(pid) not in pids}
766
+ token['resume_needed'] = bool(token['posix_units'] or token['unmapped'] or token.get('profiles') or token.get('services'))
767
+ self.records.write(token)
768
+ self.save(token)
769
+
770
+ def stop(self, entry, stopped):
771
+ owed = next(e for e in self.state['entries'] if _service_key(e) == _service_key(entry))
772
+ verify_processes(owed, self.records)
773
+ backend_request(owed)
774
+ permit = backend_request(owed, 'prepare')
775
+ if permit.get('ok') is not True or permit.get('idle') is not True or not isinstance(permit.get('token'), str):
776
+ if owed['phase'] == 'recorded':
777
+ self.state['entries'].remove(owed) # The native refusal created no retirement permit or stop.
778
+ self.save()
779
+ raise RuntimeError(f'{_name(owed)} has active work or cannot prove idle; source was not changed.')
780
+ try:
781
+ owed['phase'] = 'retiring'
782
+ self.save() # A lost commit reply still owes a full stop/resume of this incarnation.
783
+ except BaseException:
784
+ backend_request(owed, 'cancel', permit['token'])
785
+ raise
786
+ verify_processes(owed, self.records)
787
+ if backend_request(owed, 'commit', permit['token']).get('ok') is not True:
788
+ raise RuntimeError(f'{_name(owed)} did not commit its backend retirement; recovery remains owed.')
789
+ owed['backend_committed'] = True
790
+ self.save()
791
+ self._stop_backend_supervisor(owed, stopped)
792
+
793
+ def _stop_backend_supervisor(self, owed, stopped, *, stopping=False):
794
+ # Disarm through the original manager before its KeepAlive can admit a successor.
795
+ owed['backend_stop_requested'] = True
796
+ self.save()
797
+ verify_processes(owed, self.records, stopping=stopping)
798
+ if _config_stamp(owed) != owed['config_stamp']:
799
+ raise RuntimeError(f'{_name(owed)} service configuration changed before backend stop; recovery remains owed.')
800
+ if stopped.is_set():
801
+ raise RuntimeError(f'{_name(owed)} backend stop was cancelled; recovery remains owed.')
802
+ self.pause._disarm(service_entry(owed))
803
+ wait_stopped(owed, self.records, stopped)
804
+ verify_processes(owed, self.records, stopping=True)
805
+ owed['phase'] = 'stopped'
806
+ self.save()
807
+
808
+ def refresh(self, entry, stopped, ledger):
809
+ owed = next(e for e in self.state['entries'] if _service_key(e) == _service_key(entry))
810
+ if owed.get('legacy_refresh'):
811
+ raise RuntimeError(f'{_name(entry)} legacy shutdown remains owed; no signal was replayed.')
812
+ if _config_stamp(owed) != owed['config_stamp']:
813
+ raise RuntimeError(f'{_name(entry)} configuration changed before legacy refresh.')
814
+ verify_processes(owed, self.records)
815
+ verify_listener(owed)
816
+ owed.update(legacy_refresh=True, phase='refreshing')
817
+ self.save() # Older readers reject this phase and retain debt; no reader replays its signal.
818
+ verify_processes(owed, self.records)
819
+ verify_listener(owed)
820
+ if _config_stamp(owed) != owed['config_stamp']:
821
+ raise RuntimeError(f'{_name(owed)} configuration changed before legacy refresh.')
822
+ verify_processes(owed, self.records)
823
+ try:
824
+ if stopped.is_set():
825
+ raise InterruptedError('Legacy refresh was cancelled before interruption.')
826
+ os.kill(owed['pid'], signal.SIGTERM)
827
+ except ProcessLookupError:
828
+ pass # The consented incarnation already exited; only a verified successor can discharge debt.
829
+ except (PermissionError, InterruptedError) as exc:
830
+ self.state['entries'].remove(owed)
831
+ self.save() # The OS definitively refused delivery, so a new confirmation may retry it.
832
+ raise RuntimeError(f'{_name(owed)} graceful refresh was cancelled or its signal was refused; source was not changed.') from exc
833
+ self._refresh_successor(owed, stopped, ledger)
834
+
835
+ def _refresh_successor(self, owed, stopped, ledger):
836
+ wait_stopped(owed, self.records, stopped, writers_only=True)
837
+ deadline = time.monotonic() + READY_SECONDS
838
+ started = False
839
+ while True:
840
+ candidate = self._ready(owed, ledger)
841
+ if candidate:
842
+ if any(self.records.identity_is_live({'pid': int(pid), 'ct': p['ct']})
843
+ for pid, p in owed['processes'].items()
844
+ if int(pid) != owed['service_pid'] or int(pid) == owed['pid']):
845
+ raise RuntimeError(f'{_name(owed)} old writer descendants remain; recovery remains owed.')
846
+ owed.clear()
847
+ owed.update(candidate, phase='recorded', config_stamp=_config_stamp(candidate))
848
+ self.save()
849
+ return
850
+ if not _service_pid(owed)[1] and not started:
851
+ key = _service_key(owed)
852
+ if key in self._refresh_starts:
853
+ raise RuntimeError(f'{_name(owed)} legacy refresh start was already attempted; retry Agent update recovery. Source was not changed and recovery remains owed.')
854
+ whole, why = self.records.tree_is_whole(self.state['tree'], self.root)
855
+ if not whole or self.state.get('sync_failed'):
856
+ raise RuntimeError('Legacy refresh source/dependency recovery remains owed: ' + why)
857
+ self._refresh_starts.add(key)
858
+ _start(owed, self.pause)
859
+ started = True
860
+ if stopped.wait(.05) or time.monotonic() >= deadline:
861
+ raise RuntimeError(f'{_name(owed)} legacy refresh readiness is unverified; source was not changed and recovery remains owed.')
862
+
863
+ def gateway_requested(self, entry, *, acknowledged=False):
864
+ if not entry.get('processes'):
865
+ return
866
+ owed = next(e for e in self.state['entries'] if _service_key(e) == _service_key(entry))
867
+ owed['phase'] = 'retiring'
868
+ if acknowledged:
869
+ owed.update(drain_acknowledged=True, drain_acknowledged_at=time.time())
870
+ self.save()
871
+
872
+ def _gateway_candidate(self, entry):
873
+ units, jobs = self.pause._discover_systemd(), self.pause._discover_launchd()
874
+ matches = [e for e in units + jobs if _service_key(e) == _service_key(entry)]
875
+ if not matches:
876
+ return None
877
+ if len(matches) != 1 or not matches[0].get('home') or Path(matches[0]['home']).resolve() != Path(entry['home']).resolve():
878
+ raise RuntimeError(f'{_name(entry)} successor supervisor/profile is unverified; recovery remains owed.')
879
+ candidates = gateway_entries(matches, {Path(entry['home']).resolve()}, self.pause, allow_starting=True)
880
+ if not candidates:
881
+ return None
882
+ candidate = candidates[0]
883
+ if (_config_stamp(candidate) != entry['config_stamp']
884
+ or candidate.get('profile') != entry.get('profile')):
885
+ raise RuntimeError(f'{_name(entry)} successor configuration/profile changed; recovery remains owed.')
886
+ bind_processes(candidate, self.records)
887
+ parents = {int(pid): p['parent'] for pid, p in candidate['processes'].items()}
888
+ chain, pid = set(), candidate['pid']
889
+ while True:
890
+ chain.add(pid)
891
+ if pid == candidate['service_pid']:
892
+ break
893
+ pid = parents[pid]
894
+ if chain | _descendants(candidate['pid'], parents) != set(parents):
895
+ raise RuntimeError(f'{_name(entry)} successor has unrelated descendants; recovery remains owed.')
896
+ for pid, process in candidate['processes'].items():
897
+ old = entry['processes'].get(pid) or entry.get('retired_processes', {}).get(pid)
898
+ if old is not None and old != process:
899
+ raise RuntimeError(f'{_name(entry)} successor reused a recorded process identity; recovery remains owed.')
900
+ return candidate
901
+
902
+ def _gateway_writers_live(self, entry):
903
+ processes = {**entry.get('retired_processes', {}), **entry['processes']}
904
+ return any(self.records.identity_is_live({'pid': int(pid), 'ct': p['ct']})
905
+ for pid, p in processes.items()
906
+ if int(pid) != entry['service_pid'] or int(pid) == entry['pid'])
907
+
908
+ def gateway_deadline(self, homes=None, ceiling=GATEWAY_DRAIN_CEILING):
909
+ if homes is None:
910
+ homes = [e['home'] for e in self.state['entries'] if e['purpose'] == 'gateway']
911
+ return time.monotonic() + drain_budget(self.pause, homes, ceiling)
912
+
913
+ def _drain_progressing(self, entry):
914
+ """Live recorded writers still finishing an acknowledged drain within the native budget."""
915
+ if not entry.get('drain_acknowledged') or not self._gateway_writers_live(entry):
916
+ return False
917
+ now = time.time()
918
+ if not isinstance(entry.get('drain_acknowledged_at'), (int, float)):
919
+ entry['drain_acknowledged_at'] = now # Older custody has no time; its native budget starts when first observed.
920
+ self.save()
921
+ return 0 <= now - entry['drain_acknowledged_at'] < drain_budget(self.pause, [entry['home']])
922
+
923
+ def settle_gateway(self, entry, stopped, *, deadline=None):
924
+ owed = next(e for e in self.state['entries'] if _service_key(e) == _service_key(entry))
925
+ self._settle_gateway(owed, stopped, deadline=deadline)
926
+
927
+ def _settle_gateway(self, entry, stopped=None, *, deadline=None, ready=None):
928
+ """Stop the recorded gateway; True when ``ready`` adopts a verified successor after the drain instead."""
929
+ if not self._gateway_writers_live(entry) and not any(_service_pid(entry)):
930
+ if _config_stamp(entry) != entry['config_stamp']:
931
+ raise RuntimeError(f'{_name(entry)} service configuration changed; recovery remains owed.')
932
+ entry['phase'] = 'stopped'
933
+ self.save()
934
+ return
935
+ stopped = stopped or threading.Event()
936
+ if deadline is None:
937
+ deadline = self.gateway_deadline([entry['home']], RECOVERY_DRAIN_SECONDS)
938
+ while True:
939
+ if stopped.is_set() or time.monotonic() >= deadline:
940
+ if not stopped.is_set() and self._drain_progressing(entry):
941
+ raise GatewayDraining(f'{_name(entry)} gateway drain remains owed while its acknowledged native drain finishes; recovery will retry.')
942
+ raise RuntimeError(f'{_name(entry)} gateway drain remains owed; retry Agent update recovery.')
943
+ if _config_stamp(entry) != entry['config_stamp']:
944
+ raise RuntimeError(f'{_name(entry)} service configuration changed; recovery remains owed.')
945
+ if self._gateway_writers_live(entry):
946
+ if not entry.get('drain_acknowledged'):
947
+ raise RuntimeError(f'{_name(entry)} gateway drain remains owed; retry Agent update recovery.')
948
+ stopped.wait(.05)
949
+ continue
950
+ if ready is not None and ready():
951
+ return True # The original drain finished under its supervisor; never drain or stop a ready successor.
952
+ entry['runtime_fence_requested'] = True
953
+ self.save()
954
+ with gateway_fence(entry) as fence:
955
+ if fence is not None:
956
+ while True:
957
+ _verify_fence(entry, fence)
958
+ if stopped.is_set() or time.monotonic() >= deadline:
959
+ raise RuntimeError(f'{_name(entry)} gateway startup contenders remain; recovery remains owed.')
960
+ _, service_pid = _service_pid(entry)
961
+ parents = _process_table()
962
+ # Pre-fence starters may still be in native stale-file cleanup.
963
+ # Wait for every unrecorded process to exit, never stop them.
964
+ pending = service_pid and (service_pid != entry['service_pid'] or any(
965
+ str(pid) not in entry['processes'] for pid in _descendants(service_pid, parents)))
966
+ if pending:
967
+ stopped.wait(.05)
968
+ continue
969
+ if not verify_processes(entry, self.records, stopping=True, fenced=True):
970
+ stopped.wait(.05)
971
+ continue
972
+ if self._gateway_writers_live(entry) or _config_stamp(entry) != entry['config_stamp']:
973
+ raise RuntimeError(f'{_name(entry)} gateway custody changed; recovery remains owed.')
974
+ _verify_fence(entry, fence)
975
+ self.pause._disarm(service_entry(entry))
976
+ wait_stopped(entry, self.records, stopped)
977
+ _verify_fence(entry, fence)
978
+ entry['phase'] = 'stopped'
979
+ self.save()
980
+ return
981
+ if not entry.get('drain_acknowledged'):
982
+ raise RuntimeError(f'{_name(entry)} successor appeared without an acknowledged drain; recovery remains owed.')
983
+ candidate = self._gateway_candidate(entry)
984
+ if candidate is not None:
985
+ attempts = entry.get('settle_attempts', 0) + 1
986
+ if attempts > 8:
987
+ raise RuntimeError(f'{_name(entry)} repeatedly respawned during drain; recovery remains owed.')
988
+ retired = {**entry.get('retired_processes', {}), **entry['processes']}
989
+ entry.update(candidate, retired_processes=retired, phase='retiring',
990
+ drain_acknowledged=False, drain_acknowledged_at=None, settle_attempts=attempts)
991
+ self.save() # Successor custody precedes its cooperative lifecycle request.
992
+ verify_processes(entry, self.records)
993
+ if _config_stamp(entry) != entry['config_stamp']:
994
+ raise RuntimeError(f'{_name(entry)} service configuration changed; recovery remains owed.')
995
+ if not self.pause._ask_to_drain(entry):
996
+ raise RuntimeError(f'{_name(entry)} successor did not acknowledge cooperative drain; recovery remains owed.')
997
+ entry.update(drain_acknowledged=True, drain_acknowledged_at=time.time())
998
+ self.save()
999
+ stopped.wait(.05)
1000
+
1001
+ def resume(self, ledger, *, gateway_deadline=None):
1002
+ if not self.state['entries']:
1003
+ return
1004
+ whole, why = self.records.tree_is_whole(self.state['tree'], self.root)
1005
+ if not whole or self.state.get('sync_failed'):
1006
+ raise RuntimeError('Backend recovery remains owed until source/dependency repair completes: ' + why)
1007
+ failures, draining = [], True
1008
+ for entry in list(self.state['entries']):
1009
+ try:
1010
+ if self._ready(entry, ledger):
1011
+ self.state['entries'].remove(entry)
1012
+ self.save()
1013
+ continue
1014
+ if entry.get('legacy_refresh'):
1015
+ # EOF/crash recovery may adopt/start a successor, but never replay interruption consent.
1016
+ if self.records.identity_is_live({'pid': entry['pid'], 'ct': entry['processes'][str(entry['pid'])]['ct']}):
1017
+ raise RuntimeError(f'{_name(entry)} legacy shutdown remains owed; no signal was replayed.')
1018
+ self._refresh_successor(entry, threading.Event(), ledger)
1019
+ self.state['entries'].remove(entry)
1020
+ self.save()
1021
+ continue
1022
+ if entry['purpose'] == 'gateway':
1023
+ if gateway_deadline is None:
1024
+ gateway_deadline = self.gateway_deadline(ceiling=RECOVERY_DRAIN_SECONDS)
1025
+ if self._settle_gateway(entry, deadline=gateway_deadline, ready=lambda entry=entry: self._ready(entry, ledger)):
1026
+ self.state['entries'].remove(entry)
1027
+ self.save()
1028
+ continue
1029
+ if not _service_pid(entry)[1]:
1030
+ _start(entry, self.pause)
1031
+ self._wait_ready(entry, ledger)
1032
+ continue
1033
+ original_live = self.records.identity_is_live({'pid': entry['pid'], 'ct': entry['processes'][str(entry['pid'])]['ct']})
1034
+ if original_live:
1035
+ if entry['phase'] == 'recorded':
1036
+ raise RuntimeError(f'{_name(entry)} uncommitted retirement could not yet be cancelled; recovery remains owed.')
1037
+ self.stop(entry, threading.Event())
1038
+ elif any(self.records.identity_is_live({'pid': int(pid), 'ct': p['ct']}) for pid, p in entry['processes'].items()):
1039
+ verify_processes(entry, self.records, stopping=True)
1040
+ if any(self.records.identity_is_live({'pid': int(pid), 'ct': p['ct']}) for pid, p in entry['processes'].items() if int(pid) != entry['service_pid']):
1041
+ raise RuntimeError(f'{_name(entry)} still has live writer descendants; recovery remains owed.')
1042
+ self._stop_backend_supervisor(entry, threading.Event(), stopping=True)
1043
+ _, active = _service_pid(entry)
1044
+ if not active:
1045
+ _start(entry, self.pause)
1046
+ self._wait_ready(entry, ledger)
1047
+ except Exception as exc:
1048
+ draining = draining and isinstance(exc, GatewayDraining)
1049
+ failures.append(str(exc))
1050
+ if failures:
1051
+ # Sibling obligations recover independently; only an all-draining remainder is progress.
1052
+ raise (GatewayDraining if draining else RuntimeError)('; '.join(failures))
1053
+
1054
+ def _wait_ready(self, entry, ledger):
1055
+ deadline = time.monotonic() + READY_SECONDS
1056
+ while True:
1057
+ if self._ready(entry, ledger):
1058
+ self.state['entries'].remove(entry)
1059
+ self.save()
1060
+ return
1061
+ if time.monotonic() >= deadline:
1062
+ raise RuntimeError(f'{_name(entry)} readiness is unverified at its original endpoint; recovery remains owed.')
1063
+ time.sleep(.1)
1064
+
1065
+ def _ready(self, entry, ledger):
1066
+ if _config_stamp(entry) != entry['config_stamp']:
1067
+ raise RuntimeError(f'{_name(entry)} service configuration changed; backend recovery remains owed.')
1068
+ if entry['purpose'] == 'gateway':
1069
+ from gateway.status import get_running_pid_identity_strict
1070
+ runtime = get_running_pid_identity_strict(Path(entry['home']) / 'gateway.pid')
1071
+ if runtime is None:
1072
+ return False
1073
+ if entry['phase'] != 'recorded' and self._gateway_writers_live(entry):
1074
+ return False
1075
+ candidate = self._gateway_candidate(entry)
1076
+ if candidate is None:
1077
+ return False
1078
+ ready = _gateway_home_matches(entry['home'], (candidate['pid'], candidate['ct']), self.pause)
1079
+ verify_processes(candidate, self.records)
1080
+ return ready
1081
+ if entry.get('legacy_refresh') and any(self.records.identity_is_live({'pid': int(pid), 'ct': p['ct']})
1082
+ for pid, p in entry['processes'].items()
1083
+ if int(pid) != entry['service_pid'] or int(pid) == entry['pid']):
1084
+ return False
1085
+ candidates = [row for row, live in ledger(self.root, self.identity) if live is True
1086
+ and row.get('purpose') == entry['purpose']
1087
+ and row.get('host') == entry['host'] and row.get('port') == entry['port']]
1088
+ if len(candidates) != 1:
1089
+ return False
1090
+ row = candidates[0]
1091
+ if (row['pid'] == entry['pid'] and entry['phase'] != 'recorded'
1092
+ and self.records.identity_is_live({'pid': entry['pid'], 'ct': entry['processes'][str(entry['pid'])]['ct']})):
1093
+ return False # A committed retirement on the original process closes admission permanently.
1094
+ candidate = backend_entry(row, {Path(entry['home']).resolve()}, self.pause, self.identity)
1095
+ if (candidate['profile'] != entry['profile'] or _service_key(candidate) != _service_key(entry)
1096
+ or _config_stamp(candidate) != entry['config_stamp']):
1097
+ return False
1098
+ bind_processes(candidate, self.records)
1099
+ try:
1100
+ if entry.get('legacy_refresh'):
1101
+ verify_listener(candidate)
1102
+ backend_request(candidate)
1103
+ if entry['phase'] == 'recorded' and row['pid'] == entry['pid']:
1104
+ permit = backend_request(candidate, 'prepare')
1105
+ if permit.get('ok') is not True or permit.get('idle') is not True or not isinstance(permit.get('token'), str):
1106
+ return False
1107
+ if backend_request(candidate, 'cancel', permit['token']).get('ok') is not True:
1108
+ return False
1109
+ verify_processes(candidate, self.records)
1110
+ if self.identity._pid_alive_matches(row['pid'], row['create_time'], strict=True) is not True:
1111
+ return False
1112
+ except BackendLifecycleUnsupported:
1113
+ raise
1114
+ except RuntimeError:
1115
+ return False
1116
+ return candidate