@sayknow-cli/coding-agent 0.4.7 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (428) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/dist/types/capability/index.d.ts +10 -20
  3. package/dist/types/capability/types.d.ts +3 -0
  4. package/dist/types/cli/args.d.ts +0 -2
  5. package/dist/types/cli/read-cli.d.ts +1 -0
  6. package/dist/types/cli.d.ts +9 -1
  7. package/dist/types/commands/daemon.d.ts +2 -2
  8. package/dist/types/commands/deep-interview.d.ts +9 -0
  9. package/dist/types/commands/harness.d.ts +22 -0
  10. package/dist/types/commands/read.d.ts +6 -0
  11. package/dist/types/commands/sdk.d.ts +20 -2
  12. package/dist/types/config/keybindings.d.ts +34 -4
  13. package/dist/types/config/model-profile-contract.d.ts +40 -0
  14. package/dist/types/config/model-profiles.d.ts +2 -3
  15. package/dist/types/config/model-registry.d.ts +5 -0
  16. package/dist/types/config/provider-auth-health.d.ts +14 -0
  17. package/dist/types/config/provider-ranking.d.ts +59 -0
  18. package/dist/types/config/settings-schema.d.ts +156 -10
  19. package/dist/types/config/settings.d.ts +3 -0
  20. package/dist/types/edit/modes/patch.d.ts +13 -0
  21. package/dist/types/edit/path-mutation-lock.d.ts +15 -0
  22. package/dist/types/eval/py/executor.d.ts +1 -1
  23. package/dist/types/eval/py/runner-artifact.d.ts +8 -0
  24. package/dist/types/eval/py/tool-bridge.d.ts +1 -2
  25. package/dist/types/extensibility/extensions/types.d.ts +43 -2
  26. package/dist/types/extensibility/shared-events.d.ts +6 -0
  27. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  28. package/dist/types/main.d.ts +33 -2
  29. package/dist/types/memories/index.d.ts +2 -0
  30. package/dist/types/modes/acp/acp-agent.d.ts +22 -9
  31. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  32. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  33. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  34. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  35. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  36. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  37. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
  38. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  39. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  40. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  41. package/dist/types/modes/components/tool-status-header.d.ts +4 -2
  42. package/dist/types/modes/components/welcome.d.ts +5 -0
  43. package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
  44. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  45. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  46. package/dist/types/modes/interactive-mode.d.ts +13 -2
  47. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  48. package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
  49. package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
  50. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
  51. package/dist/types/modes/types.d.ts +6 -0
  52. package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
  53. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  54. package/dist/types/runtime/memory-domain.d.ts +9 -0
  55. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  56. package/dist/types/runtime/memory-guard.d.ts +39 -0
  57. package/dist/types/runtime/memory-limit.d.ts +11 -0
  58. package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
  59. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +1 -1
  60. package/dist/types/runtime-mcp/manager.d.ts +9 -1
  61. package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
  62. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
  63. package/dist/types/runtime-mcp/redaction.d.ts +2 -0
  64. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  65. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  66. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  67. package/dist/types/sdk/broker/broker.d.ts +2 -0
  68. package/dist/types/sdk/broker/lifecycle.d.ts +13 -0
  69. package/dist/types/sdk/bus/chat-daemon-control.d.ts +9 -1
  70. package/dist/types/sdk/bus/config-commands.d.ts +2 -2
  71. package/dist/types/sdk/bus/config.d.ts +2 -0
  72. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  73. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  74. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  75. package/dist/types/sdk/bus/index.d.ts +52 -4
  76. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  77. package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
  78. package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
  79. package/dist/types/sdk/bus/notification-service.d.ts +34 -0
  80. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
  81. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +106 -0
  82. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  83. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  84. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +33 -9
  85. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
  86. package/dist/types/sdk/bus/telegram-daemon.d.ts +286 -12
  87. package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
  88. package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
  89. package/dist/types/sdk/client/discovery.d.ts +2 -0
  90. package/dist/types/sdk/client/liveness.d.ts +7 -0
  91. package/dist/types/sdk/host/control/operations.d.ts +3 -2
  92. package/dist/types/sdk/host/host.d.ts +3 -1
  93. package/dist/types/sdk/host/query/handlers.d.ts +14 -0
  94. package/dist/types/sdk/index.d.ts +2 -0
  95. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  96. package/dist/types/sdk/prompt-status.d.ts +88 -0
  97. package/dist/types/sdk/session-directory.d.ts +1 -1
  98. package/dist/types/sdk/session.d.ts +7 -0
  99. package/dist/types/sdk/startup-capability.d.ts +9 -0
  100. package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
  101. package/dist/types/sdk/transport/index.d.ts +12 -0
  102. package/dist/types/sdk/transport/relay.d.ts +43 -0
  103. package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
  104. package/dist/types/sdk/transport/socket.d.ts +7 -0
  105. package/dist/types/sdk/transport/stdio.d.ts +3 -0
  106. package/dist/types/session/agent-session.d.ts +83 -24
  107. package/dist/types/session/btw-contract.d.ts +16 -0
  108. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  109. package/dist/types/session/internal/managed-session-scope.d.ts +70 -10
  110. package/dist/types/session/internal/managed-session-storage.d.ts +34 -12
  111. package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
  112. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  113. package/dist/types/session/session-manager.d.ts +89 -18
  114. package/dist/types/session/session-storage.d.ts +31 -0
  115. package/dist/types/session/streaming-output.d.ts +60 -5
  116. package/dist/types/setup/credential-import.d.ts +5 -1
  117. package/dist/types/setup/provider-onboarding.d.ts +2 -0
  118. package/dist/types/skc-runtime/deep-interview-runtime.d.ts +1 -1
  119. package/dist/types/skc-runtime/deep-interview-stage.d.ts +37 -0
  120. package/dist/types/skc-runtime/gc-runtime.d.ts +1 -1
  121. package/dist/types/skc-runtime/linux-proc.d.ts +22 -14
  122. package/dist/types/skc-runtime/memory-guard-owner-claims.d.ts +47 -0
  123. package/dist/types/skc-runtime/ralplan-runtime.d.ts +112 -1
  124. package/dist/types/skc-runtime/repository-binding.d.ts +63 -0
  125. package/dist/types/skc-runtime/state-runtime.d.ts +3 -1
  126. package/dist/types/skc-runtime/state-writer.d.ts +5 -1
  127. package/dist/types/skc-runtime/team-launch.d.ts +1 -1
  128. package/dist/types/skc-runtime/team-runtime.d.ts +9 -2
  129. package/dist/types/skc-runtime/team-store.d.ts +12 -0
  130. package/dist/types/skc-runtime/team-worker-memory-guard.d.ts +92 -0
  131. package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +7 -0
  132. package/dist/types/skc-runtime/ultragoal-guard.d.ts +6 -5
  133. package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +28 -0
  134. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +43 -5
  135. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  136. package/dist/types/ssh/utils.d.ts +1 -0
  137. package/dist/types/task/discovery.d.ts +3 -1
  138. package/dist/types/task/executor.d.ts +6 -0
  139. package/dist/types/task/index.d.ts +3 -1
  140. package/dist/types/task/provider-retry-status.d.ts +13 -0
  141. package/dist/types/task/receipt.d.ts +2 -0
  142. package/dist/types/task/types.d.ts +133 -0
  143. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  144. package/dist/types/tools/browser/launch.d.ts +7 -0
  145. package/dist/types/tools/fetch.d.ts +22 -9
  146. package/dist/types/tools/image-gen.d.ts +63 -1
  147. package/dist/types/tools/index.d.ts +7 -1
  148. package/dist/types/tools/output-meta.d.ts +21 -1
  149. package/dist/types/tools/read-internals.d.ts +12 -0
  150. package/dist/types/tools/read.d.ts +45 -1
  151. package/dist/types/tools/resource-gc.d.ts +23 -0
  152. package/dist/types/tools/skill-discovery.d.ts +7 -0
  153. package/dist/types/tools/sqlite-reader.d.ts +7 -0
  154. package/dist/types/tools/tool-result.d.ts +7 -1
  155. package/dist/types/utils/prompt-suggestion.d.ts +33 -0
  156. package/dist/types/utils/shell-snapshot.d.ts +12 -0
  157. package/dist/types/web/insane/bridge.d.ts +1 -0
  158. package/dist/types/web/insane/url-guard.d.ts +17 -0
  159. package/package.json +8 -12
  160. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  161. package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
  162. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  163. package/scripts/compile-args.ts +0 -1
  164. package/scripts/dogfood-repository-binding.ts +209 -0
  165. package/scripts/generate-docs-index.ts +4 -1
  166. package/scripts/generate-sdk-operation-inventory.ts +9 -0
  167. package/scripts/run-test-manifest.ts +6 -4
  168. package/scripts/verify-skc-sdk-canonicalization.ts +122 -18
  169. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  170. package/src/capability/index.ts +58 -42
  171. package/src/capability/types.ts +3 -0
  172. package/src/cli/args.ts +16 -100
  173. package/src/cli/fast-help.ts +1 -1
  174. package/src/cli/mcp-cli.ts +4 -21
  175. package/src/cli/notify-cli.ts +6 -38
  176. package/src/cli/read-cli.ts +2 -1
  177. package/src/cli/setup-cli.ts +10 -4
  178. package/src/cli.ts +133 -11
  179. package/src/commands/daemon.ts +22 -5
  180. package/src/commands/deep-interview.ts +26 -1
  181. package/src/commands/harness.ts +35 -12
  182. package/src/commands/read.ts +11 -2
  183. package/src/commands/sdk.ts +134 -17
  184. package/src/commands/team.ts +1 -1
  185. package/src/commit/agentic/index.ts +3 -3
  186. package/src/commit/map-reduce/index.ts +2 -2
  187. package/src/config/keybindings.ts +158 -25
  188. package/src/config/model-profile-contract.ts +174 -0
  189. package/src/config/model-profiles.ts +51 -10
  190. package/src/config/model-registry.ts +24 -4
  191. package/src/config/model-resolver.ts +2 -2
  192. package/src/config/provider-auth-health.ts +42 -0
  193. package/src/config/provider-ranking.ts +119 -0
  194. package/src/config/settings-schema.ts +157 -10
  195. package/src/config/settings.ts +254 -108
  196. package/src/coordinator-mcp/model-preset.ts +21 -74
  197. package/src/defaults/skc/skills/deep-interview/SKILL.md +147 -24
  198. package/src/defaults/skc/skills/ralplan/SKILL.md +91 -34
  199. package/src/defaults/skc/skills/team/SKILL.md +1 -1
  200. package/src/defaults/skc/skills/ultragoal/SKILL.md +68 -17
  201. package/src/discovery/ssh.ts +12 -1
  202. package/src/edit/index.ts +3 -6
  203. package/src/edit/modes/patch.ts +38 -1
  204. package/src/edit/modes/replace.ts +42 -0
  205. package/src/edit/path-mutation-lock.ts +112 -0
  206. package/src/eval/js/tool-bridge.ts +1 -1
  207. package/src/eval/py/executor.ts +37 -13
  208. package/src/eval/py/kernel.ts +1 -21
  209. package/src/eval/py/prelude.py +2 -2
  210. package/src/eval/py/runner-artifact.ts +84 -0
  211. package/src/eval/py/tool-bridge.ts +34 -18
  212. package/src/extensibility/extensions/runner.ts +4 -0
  213. package/src/extensibility/extensions/types.ts +42 -4
  214. package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
  215. package/src/extensibility/shared-events.ts +5 -0
  216. package/src/extensibility/skc-plugins/runtime-adapters.ts +2 -1
  217. package/src/hooks/native-skill-hook.ts +17 -4
  218. package/src/internal-urls/docs-index.generated.ts +17 -16
  219. package/src/internal-urls/local-protocol.ts +87 -25
  220. package/src/internal-urls/local-root-gc.ts +127 -0
  221. package/src/internal-urls/mcp-protocol.ts +73 -11
  222. package/src/main.ts +120 -42
  223. package/src/memories/index.ts +10 -0
  224. package/src/modes/DESIGN.md +52 -0
  225. package/src/modes/acp/acp-agent.ts +647 -83
  226. package/src/modes/acp/acp-event-mapper.ts +106 -12
  227. package/src/modes/acp/acp-mode.ts +39 -3
  228. package/src/modes/action-registry.ts +1 -1
  229. package/src/modes/components/assistant-message.ts +123 -22
  230. package/src/modes/components/btw-panel.ts +82 -16
  231. package/src/modes/components/irc-sidebar.ts +176 -62
  232. package/src/modes/components/model-selector.ts +61 -4
  233. package/src/modes/components/notifications-settings-editor.ts +1 -1
  234. package/src/modes/components/oauth-selector.ts +42 -12
  235. package/src/modes/components/plan-preview-overlay.ts +1 -1
  236. package/src/modes/components/queue-pane.ts +55 -9
  237. package/src/modes/components/queued-message-selector.ts +53 -10
  238. package/src/modes/components/read-tool-group.ts +82 -14
  239. package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
  240. package/src/modes/components/sayknow-pet-widget.ts +20 -8
  241. package/src/modes/components/session-observer-overlay.ts +187 -32
  242. package/src/modes/components/settings-selector.ts +60 -18
  243. package/src/modes/components/tool-execution.ts +138 -27
  244. package/src/modes/components/tool-status-header.ts +26 -8
  245. package/src/modes/components/welcome.ts +50 -23
  246. package/src/modes/controllers/btw-controller.ts +119 -36
  247. package/src/modes/controllers/command-controller.ts +101 -54
  248. package/src/modes/controllers/event-controller.ts +119 -27
  249. package/src/modes/controllers/extension-ui-controller.ts +23 -12
  250. package/src/modes/controllers/input-controller.ts +100 -8
  251. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
  252. package/src/modes/controllers/selector-controller.ts +4 -1
  253. package/src/modes/interactive-mode.ts +201 -60
  254. package/src/modes/irc-observation-ledger.ts +71 -15
  255. package/src/modes/prompt-action-autocomplete.ts +32 -13
  256. package/src/modes/prompt-suggestion-controller.ts +96 -0
  257. package/src/modes/runtime-init.ts +20 -2
  258. package/src/modes/shared/agent-wire/command-dispatch.ts +3 -3
  259. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
  260. package/src/modes/types.ts +6 -0
  261. package/src/modes/utils/hotkeys-markdown.ts +64 -36
  262. package/src/modes/utils/ui-helpers.ts +99 -4
  263. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  264. package/src/prompts/agents/architect.md +17 -3
  265. package/src/prompts/agents/critic.md +17 -2
  266. package/src/prompts/agents/executor.md +1 -1
  267. package/src/prompts/agents/planner.md +8 -1
  268. package/src/prompts/system/btw-user.md +3 -8
  269. package/src/prompts/system/prompt-suggestion-system.md +32 -0
  270. package/src/prompts/system/system-prompt.md +6 -3
  271. package/src/prompts/tools/cron.md +3 -1
  272. package/src/prompts/tools/read.md +4 -2
  273. package/src/prompts/tools/skill-discovery.md +1 -0
  274. package/src/runtime/memory-domain.ts +66 -0
  275. package/src/runtime/memory-guard-contract.ts +73 -0
  276. package/src/runtime/memory-guard.ts +292 -0
  277. package/src/runtime/memory-limit.ts +50 -0
  278. package/src/runtime-mcp/client.ts +41 -55
  279. package/src/runtime-mcp/content-limits.ts +71 -0
  280. package/src/runtime-mcp/discoverable-tool-metadata.ts +1 -1
  281. package/src/runtime-mcp/json-rpc.ts +28 -11
  282. package/src/runtime-mcp/manager.ts +77 -59
  283. package/src/runtime-mcp/oauth-flow.ts +0 -72
  284. package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
  285. package/src/runtime-mcp/redaction.ts +30 -0
  286. package/src/runtime-mcp/smithery-auth.ts +17 -2
  287. package/src/runtime-mcp/smithery-connect.ts +11 -1
  288. package/src/runtime-mcp/transports/http.ts +63 -22
  289. package/src/sdk/acp/adapter.ts +8 -1
  290. package/src/sdk/acp/final-text.ts +40 -0
  291. package/src/sdk/acp/mcp.ts +26 -0
  292. package/src/sdk/broker/broker.ts +34 -20
  293. package/src/sdk/broker/identity.ts +22 -12
  294. package/src/sdk/broker/lifecycle.ts +293 -43
  295. package/src/sdk/broker/session-index.ts +11 -1
  296. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  297. package/src/sdk/bus/chat-daemon-control.ts +22 -5
  298. package/src/sdk/bus/config-commands.ts +2 -2
  299. package/src/sdk/bus/config.ts +10 -2
  300. package/src/sdk/bus/control-drain-lease.ts +104 -0
  301. package/src/sdk/bus/conversation-store.ts +40 -6
  302. package/src/sdk/bus/daemon-paths.ts +2 -0
  303. package/src/sdk/bus/index.ts +793 -121
  304. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  305. package/src/sdk/bus/lifecycle-commands.ts +63 -10
  306. package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
  307. package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
  308. package/src/sdk/bus/notification-orchestration.ts +12 -0
  309. package/src/sdk/bus/notification-service.ts +87 -25
  310. package/src/sdk/bus/operator-runtime.ts +31 -5
  311. package/src/sdk/bus/prompt-reconciliation.ts +240 -0
  312. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  313. package/src/sdk/bus/recent-activity.ts +90 -26
  314. package/src/sdk/bus/reconciliation-store.ts +224 -0
  315. package/src/sdk/bus/telegram-cli.ts +6 -13
  316. package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
  317. package/src/sdk/bus/telegram-daemon-contract.ts +34 -9
  318. package/src/sdk/bus/telegram-daemon-control.ts +161 -40
  319. package/src/sdk/bus/telegram-daemon.ts +3649 -593
  320. package/src/sdk/bus/telegram-reference.ts +20 -2
  321. package/src/sdk/bus/topic-registry.ts +451 -12
  322. package/src/sdk/cli/session-cli.ts +15 -2
  323. package/src/sdk/client/discovery.ts +19 -3
  324. package/src/sdk/client/liveness.ts +49 -0
  325. package/src/sdk/host/control/dispatch.ts +8 -2
  326. package/src/sdk/host/control/operations.ts +3 -2
  327. package/src/sdk/host/host.ts +27 -9
  328. package/src/sdk/host/query/handlers.ts +125 -5
  329. package/src/sdk/index.ts +12 -0
  330. package/src/sdk/lifecycle-session.ts +19 -3
  331. package/src/sdk/mcp/server.ts +46 -18
  332. package/src/sdk/prompt-status.ts +97 -0
  333. package/src/sdk/protocol/operation-inventory.generated.json +145 -0
  334. package/src/sdk/protocol/operation-registry.ts +24 -4
  335. package/src/sdk/session-directory.ts +5 -1
  336. package/src/sdk/session.ts +69 -8
  337. package/src/sdk/startup-capability.ts +15 -1
  338. package/src/sdk/transport/auth-preface.ts +82 -0
  339. package/src/sdk/transport/index.ts +18 -0
  340. package/src/sdk/transport/relay.ts +252 -0
  341. package/src/sdk/transport/serve-cli.ts +105 -0
  342. package/src/sdk/transport/socket.ts +197 -0
  343. package/src/sdk/transport/stdio.ts +17 -0
  344. package/src/session/agent-session.ts +1112 -314
  345. package/src/session/blob-store.ts +25 -7
  346. package/src/session/btw-contract.ts +59 -0
  347. package/src/session/fallback-chain-controller.ts +47 -8
  348. package/src/session/internal/managed-session-scope.ts +1426 -259
  349. package/src/session/internal/managed-session-storage.ts +414 -238
  350. package/src/session/internal/native-publish-outcome.ts +271 -0
  351. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  352. package/src/session/session-manager.ts +2079 -469
  353. package/src/session/session-storage.ts +151 -11
  354. package/src/session/streaming-output.ts +262 -43
  355. package/src/setup/credential-import.ts +28 -3
  356. package/src/setup/provider-onboarding.ts +49 -5
  357. package/src/setup/provider-presets.json +2 -1
  358. package/src/skc-runtime/deep-interview-runtime.ts +303 -36
  359. package/src/skc-runtime/deep-interview-stage.ts +1016 -0
  360. package/src/skc-runtime/deep-interview-state.ts +8 -5
  361. package/src/skc-runtime/gc-render.ts +1 -0
  362. package/src/skc-runtime/gc-runtime.ts +11 -1
  363. package/src/skc-runtime/linux-proc.ts +70 -36
  364. package/src/skc-runtime/managed-owner-supervisor.ts +83 -4
  365. package/src/skc-runtime/memory-guard-owner-claims.ts +389 -0
  366. package/src/skc-runtime/ralplan-runtime.ts +1057 -75
  367. package/src/skc-runtime/repository-binding.ts +267 -0
  368. package/src/skc-runtime/session-state-sidecar.ts +25 -9
  369. package/src/skc-runtime/state-runtime.ts +50 -12
  370. package/src/skc-runtime/state-writer.ts +68 -56
  371. package/src/skc-runtime/team-launch.ts +34 -5
  372. package/src/skc-runtime/team-runtime.ts +41 -1
  373. package/src/skc-runtime/team-store.ts +32 -0
  374. package/src/skc-runtime/team-worker-memory-guard.ts +363 -0
  375. package/src/skc-runtime/team-workers.ts +1 -0
  376. package/src/skc-runtime/tmux-common.ts +13 -2
  377. package/src/skc-runtime/tmux-owner-isolation.ts +20 -1
  378. package/src/skc-runtime/tmux-sessions.ts +45 -6
  379. package/src/skc-runtime/ultragoal-guard.ts +93 -15
  380. package/src/skc-runtime/ultragoal-receipt-freshness.ts +123 -0
  381. package/src/skc-runtime/ultragoal-runtime.ts +469 -38
  382. package/src/skc-runtime/workflow-manifest.generated.json +195 -5
  383. package/src/skc-runtime/workflow-manifest.ts +67 -3
  384. package/src/skill-state/workflow-hud.ts +25 -2
  385. package/src/skill-state/workflow-mutation-guard.ts +104 -12
  386. package/src/slash-commands/builtin-registry.ts +1 -1
  387. package/src/ssh/utils.ts +14 -0
  388. package/src/task/discovery.ts +8 -2
  389. package/src/task/executor.ts +165 -47
  390. package/src/task/index.ts +123 -15
  391. package/src/task/provider-retry-status.ts +88 -0
  392. package/src/task/receipt.ts +3 -0
  393. package/src/task/render.ts +45 -26
  394. package/src/task/skc-command.ts +2 -2
  395. package/src/task/types.ts +55 -10
  396. package/src/task/ultragoal-redteam-activation.ts +75 -0
  397. package/src/tools/ask.ts +1 -1
  398. package/src/tools/ast-edit.ts +2 -2
  399. package/src/tools/bash-allowed-prefixes.ts +45 -1
  400. package/src/tools/bash-pty-selection.ts +2 -2
  401. package/src/tools/browser/launch.ts +41 -6
  402. package/src/tools/browser/screenshot-format.ts +6 -1
  403. package/src/tools/cron.ts +1 -1
  404. package/src/tools/fetch.ts +127 -154
  405. package/src/tools/image-gen.ts +476 -23
  406. package/src/tools/index.ts +7 -1
  407. package/src/tools/output-meta.ts +176 -9
  408. package/src/tools/path-utils.ts +5 -1
  409. package/src/tools/read-internals.ts +44 -0
  410. package/src/tools/read.ts +1544 -310
  411. package/src/tools/resource-gc.ts +665 -125
  412. package/src/tools/skill-discovery.ts +39 -3
  413. package/src/tools/sqlite-reader.ts +63 -5
  414. package/src/tools/subagent-render.ts +74 -16
  415. package/src/tools/subagent.ts +2 -0
  416. package/src/tools/tool-result.ts +14 -1
  417. package/src/utils/edit-mode.ts +2 -2
  418. package/src/utils/pasted-image-path.ts +15 -6
  419. package/src/utils/prompt-suggestion.ts +265 -0
  420. package/src/utils/shell-snapshot.ts +173 -63
  421. package/src/utils/sixel.ts +3 -3
  422. package/src/web/insane/bridge.ts +5 -1
  423. package/src/web/insane/url-guard.ts +140 -27
  424. package/src/web/scrapers/docs-rs.ts +30 -5
  425. package/src/web/scrapers/types.ts +19 -49
  426. package/src/web/scrapers/utils.ts +12 -26
  427. package/src/web/search/providers/codex.ts +1 -1
  428. package/src/workflow/workflow-intent-diff.ts +4 -1
@@ -1,5 +1,33 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import type { WindowsJobMemoryProbeResult } from "@sayknow-cli/natives";
5
+
6
+ function safeProbeWindowsJobMemory(): WindowsJobMemoryProbeResult {
7
+ try {
8
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
9
+ const natives = require("@sayknow-cli/natives") as { probeWindowsJobMemory?: () => unknown };
10
+ if (typeof natives.probeWindowsJobMemory === "function") {
11
+ return natives.probeWindowsJobMemory() as WindowsJobMemoryProbeResult;
12
+ }
13
+ } catch {
14
+ // Native addon unbuilt or missing
15
+ }
16
+ return { kind: "unsupported_platform", platform: process.platform };
17
+ }
18
+
1
19
  import { logger } from "@sayknow-cli/utils";
2
20
  import type { Settings } from "../config/settings";
21
+ import { computeMemoryGuardDomain } from "../runtime/memory-domain";
22
+ import {
23
+ chooseMemoryGuardAction,
24
+ MemoryGuardHost,
25
+ resolveMemoryGuardPolicy,
26
+ revalidateMemoryGuardAction,
27
+ } from "../runtime/memory-guard";
28
+ import type { MemoryGuardPolicy, MemoryGuardWorkerSample } from "../runtime/memory-guard-contract";
29
+ import { resolveEffectiveMemoryLimit } from "../runtime/memory-limit";
30
+ import { executeSkcTeamApiOperation, listSkcTeams, readSkcWorkerHeartbeat } from "../skc-runtime/team-runtime";
3
31
  import { listTabsForGc, releaseTabIfGcEligible, type TabGcSnapshot } from "./browser/tab-supervisor";
4
32
  import { cleanupStaleScreenshotFallbackDirs, hasCreatedScreenshotFallbackDir } from "./computer-gc";
5
33
 
@@ -17,7 +45,6 @@ import { cleanupStaleScreenshotFallbackDirs, hasCreatedScreenshotFallbackDir } f
17
45
  * eviction is best-effort and never force-evicts.
18
46
  */
19
47
 
20
- const DEFAULT_SWEEP_INTERVAL_MS = 30_000;
21
48
  const BYTES_PER_MB = 1024 * 1024;
22
49
 
23
50
  export interface BrowserGcPolicy {
@@ -55,58 +82,69 @@ export function resolveSweepIntervalMs(settings: Settings): number {
55
82
  /** Injectable seams so the controller is fully testable without real browsers/filesystem/RSS. */
56
83
  export interface ResourceGcDeps {
57
84
  now: () => number;
85
+ monotonicNow: () => number;
58
86
  rssBytes: () => number;
87
+ memorySnapshot: () => Promise<MemoryPressureSnapshot>;
88
+ runGc: () => void;
59
89
  logWarn: (msg: string, meta?: Record<string, unknown>) => void;
60
90
  listTabs: () => TabGcSnapshot[];
61
91
  releaseTab: (name: string, policy: { now: () => number; idleMs: number }) => Promise<boolean>;
62
92
  cleanupScreenshots: (opts: { now: () => number; staleMs: number }) => Promise<{ scanned: number; removed: number }>;
63
93
  screenshotArmed: () => boolean;
94
+ listTeamWorkers?: (cwd: string, sessionId: string) => Promise<MemoryGuardWorkerSample[]>;
95
+ applyTeamWorkerGuard?: (
96
+ cwd: string,
97
+ sessionId: string,
98
+ workerId: string,
99
+ excessBytes: number,
100
+ incidentId: string,
101
+ ) => Promise<void>;
64
102
  }
65
103
 
66
104
  const defaultDeps: ResourceGcDeps = {
67
105
  now: () => Date.now(),
106
+ monotonicNow: () => performance.now(),
68
107
  rssBytes: () => process.memoryUsage().rss,
108
+ memorySnapshot: () => sampleMemoryPressure(),
109
+ runGc: () => Bun.gc(true),
69
110
  logWarn: (msg, meta) => logger.warn(msg, meta),
70
111
  listTabs: () => listTabsForGc(),
71
112
  releaseTab: (name, policy) => releaseTabIfGcEligible(name, policy),
72
113
  cleanupScreenshots: opts => cleanupStaleScreenshotFallbackDirs(opts),
73
114
  screenshotArmed: () => hasCreatedScreenshotFallbackDir(),
115
+ listTeamWorkers: (cwd, sessionId) => sampleTeamWorkers(cwd, sessionId),
116
+ applyTeamWorkerGuard: (cwd, sessionId, workerId, excessBytes, incidentId) =>
117
+ applySelectedTeamWorker(cwd, sessionId, workerId, excessBytes, incidentId),
74
118
  };
75
119
 
76
120
  // ── Controller state (process-global; tabs/browsers are module-global too) ──────────────────
77
- const activeSessions = new Map<string, Settings>();
78
-
79
- interface ScheduleOwner {
80
- generation: number;
81
- token: number;
82
- }
83
-
84
- interface DeferredSchedule {
85
- generation: number;
86
- deadline: number;
87
- }
88
-
89
- interface WorkOwner {
90
- generation: number;
91
- source: "timer" | "external";
92
- }
93
-
94
- let pendingTimer: NodeJS.Timeout | null = null;
95
- let pendingDeadline: number | null = null;
96
- let pendingOwner: ScheduleOwner | null = null;
97
- let deferredSchedule: DeferredSchedule | null = null;
98
- let inProgressOwner: WorkOwner | null = null;
99
- let stopped = false;
100
- let timerGeneration = 0;
101
- let nextTimerToken = 0;
102
- let schedulerNow = (): number => performance.now();
121
+ const activeSessions = new Map<string, { settings: Settings; cwd: () => string }>();
122
+ const scheduler = new MemoryGuardHost({
123
+ run: async () => {
124
+ await sweepOnce(deps);
125
+ },
126
+ logDebug: (message, meta) => logger.debug(message, meta),
127
+ });
103
128
  let rssWarningActive = false;
104
129
  let lastScreenshotScanAt = 0;
130
+ const memoryGuardGcActive = new Set<string>();
131
+ const memoryGuardLastEvaluatedAt = new Map<string, number>();
132
+ const memoryGuardRestartAboveSince = new Map<string, number>();
133
+ const memoryGuardRestartCooldownUntil = new Map<string, number>();
134
+ const memoryGuardWorkerIncidentIds = new Map<string, string>();
105
135
  let deps: ResourceGcDeps = defaultDeps;
106
136
 
107
137
  export interface ResourceGcRegistration {
108
138
  sessionId: string;
109
139
  settings: Settings;
140
+ cwd?: string | (() => string);
141
+ }
142
+
143
+ function resolveSessionSweepIntervalMs(settings: Settings): number {
144
+ const memoryPolicy = resolveMemoryGuardPolicy(settings);
145
+ return memoryPolicy.enabled
146
+ ? Math.min(resolveSweepIntervalMs(settings), memoryPolicy.checkIntervalMs)
147
+ : resolveSweepIntervalMs(settings);
110
148
  }
111
149
 
112
150
  /**
@@ -115,121 +153,615 @@ export interface ResourceGcRegistration {
115
153
  * session unregisters.
116
154
  */
117
155
  export function registerResourceGcSession(reg: ResourceGcRegistration): () => void {
118
- const isNewSession = !activeSessions.has(reg.sessionId);
119
- activeSessions.set(reg.sessionId, reg.settings);
120
- stopped = false;
121
- if (isNewSession) {
122
- const deadline = schedulerNow() + resolveSweepIntervalMs(reg.settings);
156
+ const registeredCwd = reg.cwd;
157
+ const cwd = typeof registeredCwd === "function" ? registeredCwd : () => registeredCwd ?? process.cwd();
158
+ activeSessions.set(reg.sessionId, { settings: reg.settings, cwd });
159
+ const unregisterSchedule = scheduler.register({
160
+ ownerId: reg.sessionId,
161
+ intervalMs: resolveSessionSweepIntervalMs(reg.settings),
162
+ });
163
+ const unregisterSettings = reg.settings.onChanged(path => {
123
164
  if (
124
- inProgressOwner?.generation === timerGeneration &&
125
- (pendingDeadline === null || pendingDeadline <= deadline)
165
+ path === "memoryGuard.enabled" ||
166
+ path === "memoryGuard.checkIntervalMs" ||
167
+ path === "resourceGc.sweepIntervalMs"
126
168
  ) {
127
- deferSchedule(timerGeneration, deadline);
128
- } else {
129
- requestSchedule(deadline);
169
+ scheduler.updateInterval(reg.sessionId, resolveSessionSweepIntervalMs(reg.settings));
170
+ if (path === "memoryGuard.enabled" && !resolveMemoryGuardPolicy(reg.settings).enabled) {
171
+ memoryGuardGcActive.delete(reg.sessionId);
172
+ memoryGuardRestartAboveSince.delete(reg.sessionId);
173
+ memoryGuardWorkerIncidentIds.delete(reg.sessionId);
174
+ memoryGuardRestartCooldownUntil.delete(reg.sessionId);
175
+ memoryGuardLastEvaluatedAt.delete(reg.sessionId);
176
+ }
130
177
  }
131
- }
178
+ });
132
179
  let unregistered = false;
133
180
  return () => {
134
181
  if (unregistered) return;
135
182
  unregistered = true;
136
183
  activeSessions.delete(reg.sessionId);
137
- if (activeSessions.size === 0) stopTimer();
184
+ memoryGuardLastEvaluatedAt.delete(reg.sessionId);
185
+ memoryGuardGcActive.delete(reg.sessionId);
186
+ memoryGuardRestartAboveSince.delete(reg.sessionId);
187
+ memoryGuardWorkerIncidentIds.delete(reg.sessionId);
188
+ memoryGuardRestartCooldownUntil.delete(reg.sessionId);
189
+ unregisterSchedule();
190
+ unregisterSettings();
138
191
  };
139
192
  }
140
193
 
141
- function currentSweepIntervalMs(): number {
142
- let min = Number.POSITIVE_INFINITY;
143
- for (const settings of activeSessions.values()) min = Math.min(min, resolveSweepIntervalMs(settings));
144
- return Number.isFinite(min) ? min : DEFAULT_SWEEP_INTERVAL_MS;
194
+ export async function sweepOnce(d: ResourceGcDeps = deps): Promise<void> {
195
+ if (activeSessions.size === 0) return;
196
+ try {
197
+ const memorySweep = sweepMemoryPressureGuard(d);
198
+ if (memorySweep) await memorySweep;
199
+ } catch (error) {
200
+ d.logWarn("Memory guard: sweep failed; continuing with browser/screenshot cleanup", { error: String(error) });
201
+ }
202
+ await sweepBrowserTabs(d);
203
+ await sweepScreenshots(d);
145
204
  }
146
205
 
147
- function clearPendingSchedule(): void {
148
- if (pendingTimer) clearTimeout(pendingTimer);
149
- pendingTimer = null;
150
- pendingDeadline = null;
151
- pendingOwner = null;
206
+ export interface MemoryPressureDomain {
207
+ hardCapBytes: number;
208
+ totalUsageBytes: number;
209
+ source: MemoryPressureSnapshot["source"];
210
+ }
211
+
212
+ export interface MemoryPressureSnapshot {
213
+ hardCapBytes: number;
214
+ totalUsageBytes: number;
215
+ parentBytes: number;
216
+ source: "host" | "linux_cgroup_v2" | "linux_cgroup_v1" | "windows_job" | "windows_process_job_limit";
217
+ domains?: MemoryPressureDomain[];
218
+ }
219
+
220
+ function decodeMountInfoPath(value: string): string {
221
+ return value.replace(/\\([0-7]{3})/g, (_match, octal: string) => String.fromCharCode(Number.parseInt(octal, 8)));
222
+ }
223
+
224
+ interface CgroupDirectoryCandidate {
225
+ directory: string;
226
+ mountPoint: string;
227
+ fallback: boolean;
228
+ }
229
+
230
+ function resolveCgroupDirectories(
231
+ mountInfo: string,
232
+ membershipPath: string,
233
+ fsType: "cgroup" | "cgroup2",
234
+ ): CgroupDirectoryCandidate[] {
235
+ const contained: CgroupDirectoryCandidate[] = [];
236
+ const fallbacks: CgroupDirectoryCandidate[] = [];
237
+ const seen = new Set<string>();
238
+ for (const line of mountInfo.split("\n")) {
239
+ const [left, right] = line.split(" - ", 2);
240
+ if (!left || !right) continue;
241
+ const leftFields = left.split(" ");
242
+ const rightFields = right.split(" ");
243
+ if (leftFields.length < 5 || rightFields[0] !== fsType) continue;
244
+ if (fsType === "cgroup" && !rightFields.slice(2).join(",").split(",").includes("memory")) continue;
245
+ const mountRoot = decodeMountInfoPath(leftFields[3]!);
246
+ const mountPoint = decodeMountInfoPath(leftFields[4]!);
247
+ const relative = path.posix.relative(mountRoot, membershipPath);
248
+ const directory =
249
+ !relative.startsWith("..") && !path.posix.isAbsolute(relative)
250
+ ? path.join(mountPoint, relative)
251
+ : path.join(mountPoint, membershipPath.replace(/^\/+/, ""));
252
+ const key = `${directory}\0${mountPoint}`;
253
+ if (seen.has(key)) continue;
254
+ seen.add(key);
255
+ const fallback = relative.startsWith("..") || path.posix.isAbsolute(relative);
256
+ const candidate = { directory, mountPoint, fallback };
257
+ if (!relative.startsWith("..") && !path.posix.isAbsolute(relative)) contained.push(candidate);
258
+ else fallbacks.push(candidate);
259
+ }
260
+ return [...contained, ...fallbacks];
152
261
  }
153
262
 
154
- function requestSchedule(deadline: number): void {
155
- if (stopped || activeSessions.size === 0) return;
156
- if (pendingDeadline !== null && pendingDeadline <= deadline) return;
157
- clearPendingSchedule();
158
- const owner = { generation: timerGeneration, token: ++nextTimerToken };
159
- pendingDeadline = deadline;
160
- pendingOwner = owner;
161
- pendingTimer = setTimeout(
162
- () => {
163
- void handleTimerCallback(owner, deadline);
164
- },
165
- Math.max(0, deadline - schedulerNow()),
263
+ async function readMemoryCounter(file: string): Promise<number | null> {
264
+ try {
265
+ const value = (await fs.readFile(file, "utf8")).trim();
266
+ if (value === "max" || !/^\d+$/.test(value)) return null;
267
+ const parsed = Number(value);
268
+ return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : null;
269
+ } catch {
270
+ return null;
271
+ }
272
+ }
273
+
274
+ type MemoryLimitCounter = { kind: "finite"; bytes: number } | { kind: "unlimited" };
275
+
276
+ async function readMemoryLimit(file: string, fsType: "cgroup" | "cgroup2"): Promise<MemoryLimitCounter | null> {
277
+ try {
278
+ const value = (await fs.readFile(file, "utf8")).trim();
279
+ if (value === "max") return { kind: "unlimited" };
280
+ if (!/^\d+$/.test(value)) return null;
281
+ const bytes = BigInt(value);
282
+ if (fsType === "cgroup" && bytes > BigInt(Number.MAX_SAFE_INTEGER)) return { kind: "unlimited" };
283
+ const numericBytes = Number(bytes);
284
+ return Number.isSafeInteger(numericBytes) ? { kind: "finite", bytes: numericBytes } : null;
285
+ } catch {
286
+ return null;
287
+ }
288
+ }
289
+
290
+ function parseCgroupEntry(line: string): [string, string, string] | null {
291
+ const first = line.indexOf(":");
292
+ const second = first < 0 ? -1 : line.indexOf(":", first + 1);
293
+ if (first < 0 || second < 0) return null;
294
+ return [line.slice(0, first), line.slice(first + 1, second), line.slice(second + 1)];
295
+ }
296
+
297
+ async function sampleLinuxCgroupDirectory(
298
+ candidate: CgroupDirectoryCandidate,
299
+ fsType: "cgroup" | "cgroup2",
300
+ hostBytes: number,
301
+ ): Promise<MemoryPressureDomain[]> {
302
+ const limitName = fsType === "cgroup2" ? "memory.max" : "memory.limit_in_bytes";
303
+ const usageName = fsType === "cgroup2" ? "memory.current" : "memory.usage_in_bytes";
304
+ const source = fsType === "cgroup2" ? "linux_cgroup_v2" : "linux_cgroup_v1";
305
+ const domains: MemoryPressureDomain[] = [];
306
+ let current = candidate.directory;
307
+ while (true) {
308
+ const [limit, usage] = await Promise.all([
309
+ readMemoryLimit(path.join(current, limitName), fsType),
310
+ readMemoryCounter(path.join(current, usageName)),
311
+ ]);
312
+ if (limit !== null && usage !== null && (limit.kind === "unlimited" || limit.bytes > 0)) {
313
+ domains.push({
314
+ hardCapBytes: limit.kind === "unlimited" ? hostBytes : Math.min(hostBytes, limit.bytes),
315
+ totalUsageBytes: usage,
316
+ source,
317
+ });
318
+ }
319
+ if (current === candidate.mountPoint) break;
320
+ const parent = path.dirname(current);
321
+ if (
322
+ parent === current ||
323
+ (parent !== candidate.mountPoint && !parent.startsWith(`${candidate.mountPoint}${path.sep}`))
324
+ ) {
325
+ break;
326
+ }
327
+ current = parent;
328
+ }
329
+ return domains;
330
+ }
331
+
332
+ export async function __sampleLinuxCgroupHierarchyForTest(
333
+ mountInfo: string,
334
+ membership: string,
335
+ fsType: "cgroup" | "cgroup2",
336
+ hostBytes: number,
337
+ parentBytes: number,
338
+ ): Promise<MemoryPressureSnapshot | null> {
339
+ const containedDomains: MemoryPressureDomain[] = [];
340
+ const fallbackDomains: MemoryPressureDomain[] = [];
341
+ for (const candidate of resolveCgroupDirectories(mountInfo, membership, fsType)) {
342
+ const domains = await sampleLinuxCgroupDirectory(candidate, fsType, hostBytes);
343
+ if (candidate.fallback) fallbackDomains.push(...domains);
344
+ else containedDomains.push(...domains);
345
+ }
346
+ const domains = containedDomains.length > 0 ? containedDomains : fallbackDomains;
347
+ if (domains.length === 0) return null;
348
+ const selected = domains.reduce((current, domain) =>
349
+ domain.totalUsageBytes / Math.min(hostBytes, domain.hardCapBytes) >
350
+ current.totalUsageBytes / Math.min(hostBytes, current.hardCapBytes)
351
+ ? domain
352
+ : current,
166
353
  );
167
- pendingTimer.unref?.();
354
+ return {
355
+ ...selected,
356
+ totalUsageBytes: Math.max(parentBytes, selected.totalUsageBytes),
357
+ parentBytes,
358
+ domains,
359
+ };
168
360
  }
169
361
 
170
- function deferSchedule(generation: number, deadline: number): void {
171
- if (generation !== timerGeneration) return;
172
- if (deferredSchedule?.generation === generation) {
173
- deferredSchedule.deadline = Math.min(deferredSchedule.deadline, deadline);
174
- return;
362
+ async function sampleLinuxCgroupMemory(hostBytes: number, parentBytes: number): Promise<MemoryPressureSnapshot | null> {
363
+ let cgroup: string;
364
+ let mountInfo: string;
365
+ try {
366
+ [cgroup, mountInfo] = await Promise.all([
367
+ fs.readFile("/proc/self/cgroup", "utf8"),
368
+ fs.readFile("/proc/self/mountinfo", "utf8"),
369
+ ]);
370
+ } catch {
371
+ return null;
372
+ }
373
+
374
+ const entries = cgroup
375
+ .split("\n")
376
+ .map(parseCgroupEntry)
377
+ .filter((entry): entry is [string, string, string] => entry !== null);
378
+ const v2Membership = entries.find(parts => parts[0] === "0" && parts[1] === "")?.[2];
379
+ const v1Membership = entries.find(parts => parts[1].split(",").includes("memory"))?.[2];
380
+ if (v2Membership) {
381
+ const snapshot = await __sampleLinuxCgroupHierarchyForTest(
382
+ mountInfo,
383
+ v2Membership,
384
+ "cgroup2",
385
+ hostBytes,
386
+ parentBytes,
387
+ );
388
+ if (snapshot) return snapshot;
389
+ }
390
+ if (v1Membership) {
391
+ return __sampleLinuxCgroupHierarchyForTest(mountInfo, v1Membership, "cgroup", hostBytes, parentBytes);
175
392
  }
176
- deferredSchedule = { generation, deadline };
393
+ return null;
394
+ }
395
+
396
+ export function __sampleWindowsJobMemoryForTest(
397
+ hostBytes: number,
398
+ parentBytes: number,
399
+ probeResult?: WindowsJobMemoryProbeResult,
400
+ ): MemoryPressureSnapshot | null {
401
+ return sampleWindowsJobMemory(hostBytes, parentBytes, probeResult);
402
+ }
403
+
404
+ function sampleWindowsJobMemory(
405
+ hostBytes: number,
406
+ parentBytes: number,
407
+ result = safeProbeWindowsJobMemory(),
408
+ ): MemoryPressureSnapshot | null {
409
+ if (result.kind !== "job_snapshot") return null;
410
+ const domains: MemoryPressureDomain[] = [];
411
+ const jobUsage = Number(result.jobMemoryUsedBytes);
412
+ const jobLimitRaw = result.jobMemoryLimitBytes;
413
+ const hasJobLimit = jobLimitRaw !== undefined && jobLimitRaw !== null;
414
+ const jobLimit = hasJobLimit ? Number(jobLimitRaw) : NaN;
415
+ if (Number.isSafeInteger(jobUsage) && jobUsage >= 0) {
416
+ if (hasJobLimit && Number.isSafeInteger(jobLimit) && jobLimit > 0) {
417
+ domains.push({
418
+ hardCapBytes: jobLimit,
419
+ totalUsageBytes: jobUsage,
420
+ source: "windows_job",
421
+ });
422
+ } else {
423
+ // Uncapped Job Object: usage participates against policy limit (no physical RAM clamp)
424
+ domains.push({
425
+ hardCapBytes: Number.MAX_SAFE_INTEGER,
426
+ totalUsageBytes: jobUsage,
427
+ source: "windows_job",
428
+ });
429
+ }
430
+ }
431
+ const processUsage = Number(result.processPrivateUsageBytes);
432
+ const processLimitRaw = result.processMemoryLimitBytes;
433
+ const hasProcessLimit = processLimitRaw !== undefined && processLimitRaw !== null;
434
+ const processLimit = hasProcessLimit ? Number(processLimitRaw) : NaN;
435
+ if (Number.isSafeInteger(processUsage) && processUsage >= 0) {
436
+ if (hasProcessLimit && Number.isSafeInteger(processLimit) && processLimit > 0) {
437
+ domains.push({
438
+ hardCapBytes: processLimit,
439
+ totalUsageBytes: processUsage,
440
+ source: "windows_process_job_limit",
441
+ });
442
+ } else {
443
+ domains.push({
444
+ hardCapBytes: Number.MAX_SAFE_INTEGER,
445
+ totalUsageBytes: processUsage,
446
+ source: "windows_process_job_limit",
447
+ });
448
+ }
449
+ }
450
+ const workingSetUsage = Number(result.processWorkingSetBytes);
451
+ if (Number.isSafeInteger(workingSetUsage) && workingSetUsage >= 0) {
452
+ domains.push({
453
+ hardCapBytes: hostBytes,
454
+ totalUsageBytes: Math.max(parentBytes, workingSetUsage),
455
+ source: "windows_process_job_limit",
456
+ });
457
+ }
458
+ if (domains.length === 0) return null;
459
+ const selected = domains.reduce((current, candidate) =>
460
+ candidate.totalUsageBytes / candidate.hardCapBytes > current.totalUsageBytes / current.hardCapBytes
461
+ ? candidate
462
+ : current,
463
+ );
464
+ return {
465
+ ...selected,
466
+ parentBytes,
467
+ domains,
468
+ };
177
469
  }
178
470
 
179
- async function handleTimerCallback(owner: ScheduleOwner, deadline: number): Promise<void> {
180
- if (pendingOwner?.generation !== owner.generation || pendingOwner.token !== owner.token) return;
181
- pendingTimer = null;
182
- pendingDeadline = null;
183
- pendingOwner = null;
184
- if (inProgressOwner) {
185
- deferSchedule(owner.generation, deadline);
186
- return;
471
+ async function sampleMemoryPressure(): Promise<MemoryPressureSnapshot> {
472
+ const parentBytes = process.memoryUsage().rss;
473
+ const hostBytes = os.totalmem();
474
+ if (process.platform === "linux") {
475
+ const cgroup = await sampleLinuxCgroupMemory(hostBytes, parentBytes);
476
+ if (cgroup) return cgroup;
477
+ }
478
+ if (process.platform === "win32") {
479
+ const job = sampleWindowsJobMemory(hostBytes, parentBytes);
480
+ if (job) return job;
187
481
  }
188
- await runTick(owner.generation, "timer");
482
+ return { hardCapBytes: hostBytes, totalUsageBytes: parentBytes, parentBytes, source: "host" };
483
+ }
484
+
485
+ export function __selectMemoryPressureDomainForTest(
486
+ snapshot: MemoryPressureSnapshot,
487
+ policyLimitBytes: number | null,
488
+ ): MemoryPressureSnapshot {
489
+ const domains = snapshot.domains;
490
+ if (!domains || domains.length === 0) return snapshot;
491
+ const selected = domains.reduce((current, domain) => {
492
+ const currentLimit = Math.min(current.hardCapBytes, policyLimitBytes ?? current.hardCapBytes);
493
+ const domainLimit = Math.min(domain.hardCapBytes, policyLimitBytes ?? domain.hardCapBytes);
494
+ return domain.totalUsageBytes / domainLimit > current.totalUsageBytes / currentLimit ? domain : current;
495
+ });
496
+ return {
497
+ ...snapshot,
498
+ ...selected,
499
+ totalUsageBytes: Math.max(snapshot.parentBytes, selected.totalUsageBytes),
500
+ };
189
501
  }
190
502
 
191
- function reconcileCurrentSchedule(): void {
192
- if (stopped || activeSessions.size === 0 || inProgressOwner) return;
193
- const normalDeadline = schedulerNow() + currentSweepIntervalMs();
194
- const deferredDeadline = deferredSchedule?.generation === timerGeneration ? deferredSchedule.deadline : null;
195
- if (deferredDeadline !== null) deferredSchedule = null;
196
- requestSchedule(deferredDeadline === null ? normalDeadline : Math.min(deferredDeadline, normalDeadline));
503
+ function sweepMemoryPressureGuard(d: ResourceGcDeps): Promise<void> | undefined {
504
+ let enabled = false;
505
+ for (const [sessionId, { settings }] of activeSessions) {
506
+ if (resolveMemoryGuardPolicy(settings).enabled) {
507
+ enabled = true;
508
+ continue;
509
+ }
510
+ memoryGuardGcActive.delete(sessionId);
511
+ memoryGuardRestartAboveSince.delete(sessionId);
512
+ memoryGuardWorkerIncidentIds.delete(sessionId);
513
+ memoryGuardRestartCooldownUntil.delete(sessionId);
514
+ memoryGuardLastEvaluatedAt.delete(sessionId);
515
+ }
516
+ if (!enabled) return undefined;
517
+ return sweepEnabledMemoryPressureGuard(d);
197
518
  }
198
519
 
199
- function stopTimer(): void {
200
- stopped = true;
201
- timerGeneration++;
202
- clearPendingSchedule();
203
- deferredSchedule = null;
520
+ async function readLinuxWorkerRssBytes(pid: number): Promise<number | null> {
521
+ if (process.platform !== "linux" || !Number.isSafeInteger(pid) || pid <= 0) return null;
522
+ try {
523
+ const status = await Bun.file(`/proc/${pid}/status`).text();
524
+ const match = /^VmRSS:\s+(\d+)\s+kB$/m.exec(status);
525
+ if (!match) return null;
526
+ const kibibytes = Number(match[1]);
527
+ return Number.isSafeInteger(kibibytes) ? kibibytes * 1024 : null;
528
+ } catch {
529
+ return null;
530
+ }
204
531
  }
205
532
 
206
- async function runTick(generation = timerGeneration, source: WorkOwner["source"] = "external"): Promise<void> {
207
- if (inProgressOwner || activeSessions.size === 0) return;
208
- const owner: WorkOwner = { generation, source };
209
- inProgressOwner = owner;
533
+ async function readLinuxProcessStartTime(pid: number): Promise<string | null> {
210
534
  try {
211
- await sweepOnce(deps);
212
- } catch (err) {
213
- logger.debug("resource GC sweep failed", { error: (err as Error).message });
214
- } finally {
215
- if (inProgressOwner === owner) inProgressOwner = null;
216
- // A stale completion only releases its own work lock. Once that lock is released, current
217
- // generation demand (which may have deferred behind it) can be reconciled independently.
218
- reconcileCurrentSchedule();
535
+ const stat = await Bun.file(`/proc/${pid}/stat`).text();
536
+ const commandEnd = stat.lastIndexOf(")");
537
+ if (commandEnd < 0) return null;
538
+ const startTime = stat
539
+ .slice(commandEnd + 2)
540
+ .trim()
541
+ .split(/\s+/)[19];
542
+ return startTime && /^\d+$/.test(startTime) ? startTime : null;
543
+ } catch {
544
+ return null;
219
545
  }
220
546
  }
221
547
 
222
- export async function sweepOnce(d: ResourceGcDeps = deps): Promise<void> {
223
- if (activeSessions.size === 0) return;
224
- await sweepBrowserTabs(d);
225
- await sweepScreenshots(d);
548
+ async function sampleTeamWorkers(cwd: string, sessionId: string): Promise<MemoryGuardWorkerSample[]> {
549
+ if (process.platform !== "linux") return [];
550
+ const samples: MemoryGuardWorkerSample[] = [];
551
+ for (const team of await listSkcTeams(cwd, { ...process.env, SKC_SESSION_ID: sessionId })) {
552
+ if (team.phase === "complete" || team.phase === "cancelled") continue;
553
+ for (const worker of team.workers) {
554
+ try {
555
+ const heartbeat = await readSkcWorkerHeartbeat(team.team_name, worker.id, cwd, {
556
+ ...process.env,
557
+ SKC_SESSION_ID: sessionId,
558
+ });
559
+ const heartbeatAt = Date.parse(heartbeat?.last_turn_at ?? "");
560
+ if (
561
+ !heartbeat?.alive ||
562
+ !heartbeat.process_start_time ||
563
+ !Number.isFinite(heartbeatAt) ||
564
+ Date.now() - heartbeatAt >= 120_000 ||
565
+ (await readLinuxProcessStartTime(heartbeat.pid)) !== heartbeat.process_start_time
566
+ )
567
+ continue;
568
+ const guard = (await executeSkcTeamApiOperation(
569
+ "read-worker-memory-guard",
570
+ { team_name: team.team_name, worker: worker.id, platform: process.platform },
571
+ cwd,
572
+ { ...process.env, SKC_SESSION_ID: sessionId },
573
+ )) as { automatic_action_allowed?: boolean; state?: string };
574
+ if (!guard.automatic_action_allowed || guard.state === "blocked") continue;
575
+ const bytes = await readLinuxWorkerRssBytes(heartbeat.pid);
576
+ if (bytes === null) continue;
577
+ samples.push({ workerId: `${team.team_name}/${worker.id}`, bytes, accepted: true });
578
+ } catch {
579
+ // Missing or malformed worker authority is not eligible for automatic mutation.
580
+ }
581
+ }
582
+ }
583
+ return samples;
584
+ }
585
+
586
+ async function applySelectedTeamWorker(
587
+ cwd: string,
588
+ sessionId: string,
589
+ workerId: string,
590
+ excessBytes: number,
591
+ incidentId: string,
592
+ ): Promise<void> {
593
+ const separator = workerId.lastIndexOf("/");
594
+ if (separator <= 0 || separator === workerId.length - 1) return;
595
+ const teamName = workerId.slice(0, separator);
596
+ const worker = workerId.slice(separator + 1);
597
+ await executeSkcTeamApiOperation(
598
+ "apply-worker-memory-guard",
599
+ {
600
+ team_name: teamName,
601
+ worker,
602
+ platform: process.platform,
603
+ reason: "production_memory_pressure_sweep",
604
+ incident_id: incidentId,
605
+ candidates: [{ worker_id: worker, platform: process.platform, excess_bytes: excessBytes }],
606
+ },
607
+ cwd,
608
+ { ...process.env, SKC_SESSION_ID: sessionId },
609
+ );
610
+ }
611
+ async function sweepEnabledMemoryPressureGuard(d: ResourceGcDeps): Promise<void> {
612
+ const now = d.monotonicNow();
613
+ const dueSessions: Array<{ sessionId: string; policy: MemoryGuardPolicy; cwd: string }> = [];
614
+ for (const [sessionId, { settings, cwd: resolveCwd }] of activeSessions) {
615
+ const policy = resolveMemoryGuardPolicy(settings);
616
+ const cwd = resolveCwd();
617
+ if (!policy.enabled) {
618
+ memoryGuardGcActive.delete(sessionId);
619
+ memoryGuardRestartAboveSince.delete(sessionId);
620
+ memoryGuardWorkerIncidentIds.delete(sessionId);
621
+ memoryGuardRestartCooldownUntil.delete(sessionId);
622
+ memoryGuardLastEvaluatedAt.delete(sessionId);
623
+ continue;
624
+ }
625
+ const lastEvaluated = memoryGuardLastEvaluatedAt.get(sessionId);
626
+ if (lastEvaluated !== undefined && now - lastEvaluated < policy.checkIntervalMs) continue;
627
+ memoryGuardLastEvaluatedAt.set(sessionId, now);
628
+ dueSessions.push({ sessionId, policy, cwd });
629
+ }
630
+ if (dueSessions.length === 0) return;
631
+
632
+ const snapshot = await d.memorySnapshot();
633
+ let gcRequested = false;
634
+ const gcTelemetry: Array<{ sessionId: string } & Record<string, unknown>> = [];
635
+ let workerActionTaken = false;
636
+ for (const { sessionId, policy, cwd } of dueSessions) {
637
+ const pressure = __selectMemoryPressureDomainForTest(snapshot, policy.policyLimitBytes);
638
+ const limit = resolveEffectiveMemoryLimit({
639
+ hardCapBytes: pressure.hardCapBytes,
640
+ policyLimitBytes: policy.policyLimitBytes,
641
+ });
642
+ if (limit.effectiveBytes === null) {
643
+ memoryGuardGcActive.delete(sessionId);
644
+ memoryGuardRestartAboveSince.delete(sessionId);
645
+ memoryGuardWorkerIncidentIds.delete(sessionId);
646
+ memoryGuardRestartCooldownUntil.delete(sessionId);
647
+ continue;
648
+ }
649
+ const workerSamples = await (d.listTeamWorkers ?? (async () => []))(cwd, sessionId);
650
+ const domain = computeMemoryGuardDomain({
651
+ effectiveLimitBytes: limit.effectiveBytes,
652
+ totalUsageBytes: pressure.totalUsageBytes,
653
+ parentBytes: pressure.parentBytes,
654
+ parentReserveBytes: policy.parentReserveBytes,
655
+ workers: workerSamples,
656
+ });
657
+ const decision = chooseMemoryGuardAction({
658
+ domain,
659
+ hostSupported: false,
660
+ workerSupported: workerId =>
661
+ workerSamples.some(worker => worker.workerId === workerId && worker.accepted !== false),
662
+ });
663
+ const usageRatio = pressure.totalUsageBytes / limit.effectiveBytes;
664
+ if (usageRatio >= policy.gcThresholdRatio) {
665
+ if (!memoryGuardGcActive.has(sessionId)) {
666
+ gcRequested = true;
667
+ gcTelemetry.push({
668
+ sessionId,
669
+ parentBytes: pressure.parentBytes,
670
+ totalUsageBytes: pressure.totalUsageBytes,
671
+ effectiveLimitBytes: limit.effectiveBytes,
672
+ domainSource: pressure.source,
673
+ limitSource: limit.source,
674
+ usageRatio,
675
+ decision: decision.kind,
676
+ });
677
+ }
678
+ } else {
679
+ memoryGuardGcActive.delete(sessionId);
680
+ }
681
+
682
+ if (usageRatio < policy.restartThresholdRatio) {
683
+ memoryGuardRestartAboveSince.delete(sessionId);
684
+ memoryGuardWorkerIncidentIds.delete(sessionId);
685
+ continue;
686
+ }
687
+ const aboveSince = memoryGuardRestartAboveSince.get(sessionId);
688
+ if (aboveSince === undefined) {
689
+ memoryGuardRestartAboveSince.set(sessionId, now);
690
+ memoryGuardWorkerIncidentIds.set(sessionId, `worker-pressure:${sessionId}:${Math.trunc(now)}`);
691
+ continue;
692
+ }
693
+ const cooldownUntil = memoryGuardRestartCooldownUntil.get(sessionId) ?? 0;
694
+ if (now - aboveSince < policy.restartThresholdWindowMs || now < cooldownUntil) continue;
695
+ const workerIncidentId =
696
+ memoryGuardWorkerIncidentIds.get(sessionId) ?? `worker-pressure:${sessionId}:${Math.trunc(aboveSince)}`;
697
+ let workerActionAttemptedForSession = false;
698
+ if (decision.kind === "execute" && decision.target.kind === "worker" && !workerActionTaken) {
699
+ const refreshedWorkers = await (d.listTeamWorkers ?? (async () => []))(cwd, sessionId);
700
+ const refreshedDomain = computeMemoryGuardDomain({
701
+ effectiveLimitBytes: limit.effectiveBytes,
702
+ totalUsageBytes: pressure.totalUsageBytes,
703
+ parentBytes: pressure.parentBytes,
704
+ parentReserveBytes: policy.parentReserveBytes,
705
+ workers: refreshedWorkers,
706
+ });
707
+ const refreshedDecision = chooseMemoryGuardAction({
708
+ domain: refreshedDomain,
709
+ hostSupported: false,
710
+ workerSupported: workerId =>
711
+ refreshedWorkers.some(worker => worker.workerId === workerId && worker.accepted !== false),
712
+ });
713
+ const revalidated = revalidateMemoryGuardAction(decision, refreshedDecision);
714
+ if (revalidated.kind !== "execute" || revalidated.target.kind !== "worker") continue;
715
+ workerActionTaken = true;
716
+ workerActionAttemptedForSession = true;
717
+ try {
718
+ await (d.applyTeamWorkerGuard ?? (async () => undefined))(
719
+ cwd,
720
+ sessionId,
721
+ revalidated.target.workerId,
722
+ revalidated.target.excessBytes,
723
+ workerIncidentId,
724
+ );
725
+ } catch (error) {
726
+ d.logWarn("Memory guard: team worker action failed; continuing sweep", {
727
+ sessionId,
728
+ workerId: revalidated.target.workerId,
729
+ error: String(error),
730
+ });
731
+ }
732
+ }
733
+ if (workerActionAttemptedForSession) memoryGuardRestartCooldownUntil.set(sessionId, now + policy.cooldownMs);
734
+ d.logWarn("Memory guard: restart threshold sustained", {
735
+ sessionId,
736
+ parentBytes: pressure.parentBytes,
737
+ totalUsageBytes: pressure.totalUsageBytes,
738
+ effectiveLimitBytes: limit.effectiveBytes,
739
+ domainSource: pressure.source,
740
+ limitSource: limit.source,
741
+ usageRatio,
742
+ windowMs: policy.restartThresholdWindowMs,
743
+ cooldownMs: policy.cooldownMs,
744
+ decision: decision.kind,
745
+ });
746
+ }
747
+ if (gcRequested) {
748
+ try {
749
+ d.runGc();
750
+ for (const { sessionId, ...telemetry } of gcTelemetry) {
751
+ memoryGuardGcActive.add(sessionId);
752
+ d.logWarn("Memory guard: GC threshold reached", { sessionId, ...telemetry });
753
+ }
754
+ } catch (error) {
755
+ d.logWarn("Memory guard: GC invocation failed; latch not set", { error: String(error) });
756
+ }
757
+ }
226
758
  }
227
759
 
228
760
  function ownerBrowserPolicy(snapshot: TabGcSnapshot): BrowserGcPolicy | null {
229
761
  if (!snapshot.ownerId) return null;
230
- const settings = activeSessions.get(snapshot.ownerId);
231
- if (!settings) return null;
232
- return resolveBrowserGcPolicy(settings);
762
+ const registration = activeSessions.get(snapshot.ownerId);
763
+ if (!registration) return null;
764
+ return resolveBrowserGcPolicy(registration.settings);
233
765
  }
234
766
 
235
767
  /** Coarse, ordering-only eligibility; the live recheck in releaseTabIfGcEligible is authoritative. */
@@ -268,7 +800,7 @@ async function sweepBrowserTabs(d: ResourceGcDeps): Promise<void> {
268
800
  function pressuredOwnerIds(d: ResourceGcDeps): Set<string> {
269
801
  const rss = d.rssBytes();
270
802
  const owners = new Set<string>();
271
- for (const [sessionId, settings] of activeSessions) {
803
+ for (const [sessionId, { settings }] of activeSessions) {
272
804
  const policy = resolveBrowserGcPolicy(settings);
273
805
  if (policy.enabled && rss > policy.rssLimitBytes) owners.add(sessionId);
274
806
  }
@@ -307,7 +839,7 @@ async function sweepScreenshots(d: ResourceGcDeps): Promise<void> {
307
839
 
308
840
  let staleMs: number | null = null;
309
841
  let scanIntervalMs = Number.POSITIVE_INFINITY;
310
- for (const settings of activeSessions.values()) {
842
+ for (const { settings } of activeSessions.values()) {
311
843
  const policy = resolveComputerGcPolicy(settings);
312
844
  if (!policy.enabled) continue;
313
845
  staleMs = staleMs === null ? policy.staleMs : Math.min(staleMs, policy.staleMs);
@@ -323,22 +855,28 @@ async function sweepScreenshots(d: ResourceGcDeps): Promise<void> {
323
855
 
324
856
  // ── Test-only seams ─────────────────────────────────────────────────────────────────────────
325
857
  export function __setResourceGcDepsForTest(overrides: Partial<ResourceGcDeps>): void {
326
- deps = { ...defaultDeps, ...overrides };
858
+ deps = {
859
+ ...defaultDeps,
860
+ ...overrides,
861
+ monotonicNow: overrides.monotonicNow ?? overrides.now ?? defaultDeps.monotonicNow,
862
+ listTeamWorkers: overrides.listTeamWorkers ?? (async () => []),
863
+ applyTeamWorkerGuard: overrides.applyTeamWorkerGuard ?? (async () => undefined),
864
+ };
327
865
  }
328
866
 
329
867
  export function __setResourceGcSchedulerNowForTest(now: () => number): void {
330
- schedulerNow = now;
868
+ scheduler.setSchedulerNowForTest(now);
331
869
  }
332
870
 
333
871
  export async function __runResourceGcTickForTest(): Promise<void> {
334
- await runTick();
872
+ await scheduler.runTick();
335
873
  }
336
874
 
337
875
  export async function __runResourceGcTimerCallbackForTest(
338
876
  owner: { generation: number; token: number },
339
877
  deadline: number,
340
878
  ): Promise<void> {
341
- await handleTimerCallback(owner, deadline);
879
+ await scheduler.runTimerCallbackForTest(owner, deadline);
342
880
  }
343
881
 
344
882
  export function __getResourceGcStateForTest(): {
@@ -353,28 +891,30 @@ export function __getResourceGcStateForTest(): {
353
891
  deferredGeneration: number | null;
354
892
  activeGeneration: number | null;
355
893
  } {
894
+ const state = scheduler.getStateForTest();
356
895
  return {
357
- timerActive: pendingTimer !== null,
896
+ timerActive: state.timerActive,
358
897
  sessionCount: activeSessions.size,
359
898
  rssWarningActive,
360
- inProgress: inProgressOwner !== null,
361
- generation: timerGeneration,
362
- pendingDeadline,
363
- pendingOwner: pendingOwner ? { ...pendingOwner } : null,
364
- deferredDeadline: deferredSchedule?.deadline ?? null,
365
- deferredGeneration: deferredSchedule?.generation ?? null,
366
- activeGeneration: inProgressOwner?.generation ?? null,
899
+ inProgress: state.inProgress,
900
+ generation: state.generation,
901
+ pendingDeadline: state.pendingDeadline,
902
+ pendingOwner: state.pendingOwner,
903
+ deferredDeadline: state.deferredDeadline,
904
+ deferredGeneration: state.deferredGeneration,
905
+ activeGeneration: state.activeGeneration,
367
906
  };
368
907
  }
369
908
 
370
909
  export function __resetResourceGcForTest(): void {
371
- stopTimer();
910
+ scheduler.resetForTest();
372
911
  activeSessions.clear();
373
- inProgressOwner = null;
374
- deferredSchedule = null;
375
912
  rssWarningActive = false;
913
+ memoryGuardGcActive.clear();
914
+ memoryGuardRestartAboveSince.clear();
915
+ memoryGuardWorkerIncidentIds.clear();
916
+ memoryGuardRestartCooldownUntil.clear();
917
+ memoryGuardLastEvaluatedAt.clear();
376
918
  lastScreenshotScanAt = 0;
377
- nextTimerToken = 0;
378
- schedulerNow = (): number => performance.now();
379
919
  deps = defaultDeps;
380
920
  }