@sayknow-cli/coding-agent 0.4.6 → 0.5.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.
- package/CHANGELOG.md +112 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/cli.d.ts +9 -1
- package/dist/types/commands/daemon.d.ts +2 -2
- package/dist/types/commands/deep-interview.d.ts +9 -0
- package/dist/types/commands/harness.d.ts +22 -0
- package/dist/types/commands/read.d.ts +6 -0
- package/dist/types/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +34 -4
- package/dist/types/config/model-profile-contract.d.ts +40 -0
- package/dist/types/config/model-profiles.d.ts +2 -3
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/provider-auth-health.d.ts +14 -0
- package/dist/types/config/provider-ranking.d.ts +59 -0
- package/dist/types/config/settings-schema.d.ts +156 -10
- package/dist/types/config/settings.d.ts +3 -0
- package/dist/types/edit/modes/patch.d.ts +13 -0
- package/dist/types/edit/path-mutation-lock.d.ts +15 -0
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/eval/py/runner-artifact.d.ts +8 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -2
- package/dist/types/extensibility/extensions/types.d.ts +43 -2
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +33 -2
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +22 -9
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/queue-pane.d.ts +8 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/tool-status-header.d.ts +4 -2
- package/dist/types/modes/components/welcome.d.ts +5 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +13 -2
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
- package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- package/dist/types/runtime/memory-domain.d.ts +9 -0
- package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
- package/dist/types/runtime/memory-guard.d.ts +39 -0
- package/dist/types/runtime/memory-limit.d.ts +11 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +1 -1
- package/dist/types/runtime-mcp/manager.d.ts +9 -1
- package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
- package/dist/types/runtime-mcp/redaction.d.ts +2 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/acp/final-text.d.ts +19 -0
- package/dist/types/sdk/acp/mcp.d.ts +23 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +13 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +9 -1
- package/dist/types/sdk/bus/config-commands.d.ts +2 -2
- package/dist/types/sdk/bus/config.d.ts +2 -0
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +52 -4
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +106 -0
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +33 -9
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +286 -12
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -0
- package/dist/types/sdk/host/control/operations.d.ts +3 -2
- package/dist/types/sdk/host/host.d.ts +3 -1
- package/dist/types/sdk/host/query/handlers.d.ts +14 -0
- package/dist/types/sdk/index.d.ts +2 -0
- package/dist/types/sdk/lifecycle-session.d.ts +10 -1
- package/dist/types/sdk/prompt-status.d.ts +88 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/sdk/session.d.ts +7 -0
- package/dist/types/sdk/startup-capability.d.ts +9 -0
- package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +83 -24
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/fallback-chain-controller.d.ts +18 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +70 -10
- package/dist/types/session/internal/managed-session-storage.d.ts +34 -12
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +89 -18
- package/dist/types/session/session-storage.d.ts +31 -0
- package/dist/types/session/streaming-output.d.ts +60 -5
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/skc-runtime/deep-interview-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/skc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/linux-proc.d.ts +22 -14
- package/dist/types/skc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/skc-runtime/ralplan-runtime.d.ts +112 -1
- package/dist/types/skc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/skc-runtime/state-runtime.d.ts +3 -1
- package/dist/types/skc-runtime/state-writer.d.ts +5 -1
- package/dist/types/skc-runtime/team-launch.d.ts +1 -1
- package/dist/types/skc-runtime/team-runtime.d.ts +9 -2
- package/dist/types/skc-runtime/team-store.d.ts +12 -0
- package/dist/types/skc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +7 -0
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +6 -5
- package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +28 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +43 -5
- package/dist/types/skill-state/workflow-hud.d.ts +3 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/provider-retry-status.d.ts +13 -0
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +133 -0
- package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +22 -9
- package/dist/types/tools/image-gen.d.ts +63 -1
- package/dist/types/tools/index.d.ts +7 -1
- package/dist/types/tools/output-meta.d.ts +21 -1
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -1
- package/dist/types/tools/resource-gc.d.ts +23 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/dist/types/utils/prompt-suggestion.d.ts +33 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -0
- package/package.json +8 -12
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/compile-args.ts +0 -1
- package/scripts/dogfood-repository-binding.ts +209 -0
- package/scripts/generate-docs-index.ts +4 -1
- package/scripts/generate-sdk-operation-inventory.ts +9 -0
- package/scripts/run-test-manifest.ts +6 -4
- package/scripts/verify-skc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/capability/index.ts +58 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/args.ts +16 -100
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/mcp-cli.ts +4 -21
- package/src/cli/notify-cli.ts +6 -38
- package/src/cli/read-cli.ts +2 -1
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +133 -11
- package/src/commands/daemon.ts +22 -5
- package/src/commands/deep-interview.ts +26 -1
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/sdk.ts +134 -17
- package/src/commands/team.ts +1 -1
- package/src/commit/agentic/index.ts +3 -3
- package/src/commit/map-reduce/index.ts +2 -2
- package/src/config/keybindings.ts +158 -25
- package/src/config/model-profile-contract.ts +174 -0
- package/src/config/model-profiles.ts +51 -10
- package/src/config/model-registry.ts +24 -4
- package/src/config/model-resolver.ts +2 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +157 -10
- package/src/config/settings.ts +254 -108
- package/src/coordinator-mcp/model-preset.ts +21 -74
- package/src/defaults/skc/skills/deep-interview/SKILL.md +147 -24
- package/src/defaults/skc/skills/ralplan/SKILL.md +91 -34
- package/src/defaults/skc/skills/team/SKILL.md +1 -1
- package/src/defaults/skc/skills/ultragoal/SKILL.md +68 -17
- package/src/discovery/ssh.ts +12 -1
- package/src/edit/index.ts +3 -6
- package/src/edit/modes/patch.ts +38 -1
- package/src/edit/modes/replace.ts +42 -0
- package/src/edit/path-mutation-lock.ts +112 -0
- package/src/eval/js/tool-bridge.ts +1 -1
- package/src/eval/py/executor.ts +37 -13
- package/src/eval/py/kernel.ts +1 -21
- package/src/eval/py/prelude.py +2 -2
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +42 -4
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- package/src/extensibility/shared-events.ts +5 -0
- package/src/extensibility/skc-plugins/runtime-adapters.ts +2 -1
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/internal-urls/docs-index.generated.ts +17 -16
- package/src/internal-urls/local-protocol.ts +87 -25
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +120 -42
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +52 -0
- package/src/modes/acp/acp-agent.ts +647 -83
- package/src/modes/acp/acp-event-mapper.ts +106 -12
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/btw-panel.ts +82 -16
- package/src/modes/components/irc-sidebar.ts +176 -62
- package/src/modes/components/model-selector.ts +61 -4
- package/src/modes/components/notifications-settings-editor.ts +1 -1
- package/src/modes/components/oauth-selector.ts +42 -12
- package/src/modes/components/plan-preview-overlay.ts +1 -1
- package/src/modes/components/queue-pane.ts +55 -9
- package/src/modes/components/queued-message-selector.ts +53 -10
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
- package/src/modes/components/sayknow-pet-widget.ts +27 -8
- package/src/modes/components/session-observer-overlay.ts +187 -32
- package/src/modes/components/settings-selector.ts +60 -18
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/components/tool-status-header.ts +26 -8
- package/src/modes/components/welcome.ts +50 -23
- package/src/modes/controllers/btw-controller.ts +119 -36
- package/src/modes/controllers/command-controller.ts +101 -54
- package/src/modes/controllers/event-controller.ts +119 -27
- package/src/modes/controllers/extension-ui-controller.ts +23 -12
- package/src/modes/controllers/input-controller.ts +100 -8
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +4 -1
- package/src/modes/interactive-mode.ts +201 -60
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/prompt-action-autocomplete.ts +32 -13
- package/src/modes/prompt-suggestion-controller.ts +96 -0
- package/src/modes/runtime-init.ts +20 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +3 -3
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
- package/src/modes/types.ts +6 -0
- package/src/modes/utils/hotkeys-markdown.ts +64 -36
- package/src/modes/utils/ui-helpers.ts +99 -4
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/architect.md +17 -3
- package/src/prompts/agents/critic.md +17 -2
- package/src/prompts/agents/executor.md +1 -1
- package/src/prompts/agents/planner.md +8 -1
- package/src/prompts/system/btw-user.md +3 -8
- package/src/prompts/system/prompt-suggestion-system.md +32 -0
- package/src/prompts/system/system-prompt.md +6 -3
- package/src/prompts/tools/cron.md +3 -1
- package/src/prompts/tools/read.md +4 -2
- package/src/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime/memory-domain.ts +66 -0
- package/src/runtime/memory-guard-contract.ts +73 -0
- package/src/runtime/memory-guard.ts +292 -0
- package/src/runtime/memory-limit.ts +50 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/discoverable-tool-metadata.ts +1 -1
- package/src/runtime-mcp/json-rpc.ts +28 -11
- package/src/runtime-mcp/manager.ts +77 -59
- package/src/runtime-mcp/oauth-flow.ts +0 -72
- package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
- package/src/runtime-mcp/redaction.ts +30 -0
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/runtime-mcp/transports/http.ts +63 -22
- package/src/sdk/acp/adapter.ts +8 -1
- package/src/sdk/acp/final-text.ts +40 -0
- package/src/sdk/acp/mcp.ts +26 -0
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/identity.ts +22 -12
- package/src/sdk/broker/lifecycle.ts +293 -43
- package/src/sdk/broker/session-index.ts +11 -1
- package/src/sdk/bus/chat-daemon-cli.ts +2 -2
- package/src/sdk/bus/chat-daemon-control.ts +22 -5
- package/src/sdk/bus/config-commands.ts +2 -2
- package/src/sdk/bus/config.ts +10 -2
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/conversation-store.ts +40 -6
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/index.ts +793 -121
- package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
- package/src/sdk/bus/lifecycle-commands.ts +63 -10
- package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
- package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
- package/src/sdk/bus/notification-orchestration.ts +12 -0
- package/src/sdk/bus/notification-service.ts +87 -25
- package/src/sdk/bus/operator-runtime.ts +31 -5
- package/src/sdk/bus/prompt-reconciliation.ts +240 -0
- package/src/sdk/bus/rate-limit-pool.ts +8 -0
- package/src/sdk/bus/recent-activity.ts +90 -26
- package/src/sdk/bus/reconciliation-store.ts +224 -0
- package/src/sdk/bus/telegram-cli.ts +6 -13
- package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +34 -9
- package/src/sdk/bus/telegram-daemon-control.ts +161 -40
- package/src/sdk/bus/telegram-daemon.ts +3649 -593
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/cli/session-cli.ts +15 -2
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- package/src/sdk/host/control/dispatch.ts +8 -2
- package/src/sdk/host/control/operations.ts +3 -2
- package/src/sdk/host/host.ts +27 -9
- package/src/sdk/host/query/handlers.ts +125 -5
- package/src/sdk/index.ts +12 -0
- package/src/sdk/lifecycle-session.ts +19 -3
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/prompt-status.ts +97 -0
- package/src/sdk/protocol/operation-inventory.generated.json +145 -0
- package/src/sdk/protocol/operation-registry.ts +24 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +69 -8
- package/src/sdk/startup-capability.ts +15 -1
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +1112 -314
- package/src/session/blob-store.ts +25 -7
- package/src/session/btw-contract.ts +59 -0
- package/src/session/fallback-chain-controller.ts +47 -8
- package/src/session/internal/managed-session-scope.ts +1426 -259
- package/src/session/internal/managed-session-storage.ts +414 -238
- package/src/session/internal/native-publish-outcome.ts +271 -0
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +2079 -469
- package/src/session/session-storage.ts +151 -11
- package/src/session/streaming-output.ts +262 -43
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +49 -5
- package/src/setup/provider-presets.json +2 -1
- package/src/skc-runtime/deep-interview-runtime.ts +303 -36
- package/src/skc-runtime/deep-interview-stage.ts +1016 -0
- package/src/skc-runtime/deep-interview-state.ts +8 -5
- package/src/skc-runtime/gc-render.ts +1 -0
- package/src/skc-runtime/gc-runtime.ts +11 -1
- package/src/skc-runtime/linux-proc.ts +70 -36
- package/src/skc-runtime/managed-owner-supervisor.ts +83 -4
- package/src/skc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/skc-runtime/ralplan-runtime.ts +1057 -75
- package/src/skc-runtime/repository-binding.ts +267 -0
- package/src/skc-runtime/session-state-sidecar.ts +25 -9
- package/src/skc-runtime/state-runtime.ts +50 -12
- package/src/skc-runtime/state-writer.ts +68 -56
- package/src/skc-runtime/team-launch.ts +34 -5
- package/src/skc-runtime/team-runtime.ts +41 -1
- package/src/skc-runtime/team-store.ts +32 -0
- package/src/skc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/skc-runtime/team-workers.ts +1 -0
- package/src/skc-runtime/tmux-common.ts +13 -2
- package/src/skc-runtime/tmux-owner-isolation.ts +20 -1
- package/src/skc-runtime/tmux-sessions.ts +45 -6
- package/src/skc-runtime/ultragoal-guard.ts +93 -15
- package/src/skc-runtime/ultragoal-receipt-freshness.ts +123 -0
- package/src/skc-runtime/ultragoal-runtime.ts +469 -38
- package/src/skc-runtime/workflow-manifest.generated.json +195 -5
- package/src/skc-runtime/workflow-manifest.ts +67 -3
- package/src/skill-state/workflow-hud.ts +25 -2
- package/src/skill-state/workflow-mutation-guard.ts +104 -12
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/task/executor.ts +165 -47
- package/src/task/index.ts +123 -15
- package/src/task/provider-retry-status.ts +88 -0
- package/src/task/receipt.ts +3 -0
- package/src/task/render.ts +45 -26
- package/src/task/skc-command.ts +2 -2
- package/src/task/types.ts +55 -10
- package/src/task/ultragoal-redteam-activation.ts +75 -0
- package/src/tools/ask.ts +1 -1
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/bash-allowed-prefixes.ts +45 -1
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/browser/screenshot-format.ts +6 -1
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +127 -154
- package/src/tools/image-gen.ts +476 -23
- package/src/tools/index.ts +7 -1
- package/src/tools/output-meta.ts +176 -9
- package/src/tools/path-utils.ts +5 -1
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1544 -310
- package/src/tools/resource-gc.ts +665 -125
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/tools/subagent-render.ts +74 -16
- package/src/tools/subagent.ts +2 -0
- package/src/tools/tool-result.ts +14 -1
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +140 -27
- package/src/web/scrapers/docs-rs.ts +30 -5
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
- package/src/web/search/providers/codex.ts +1 -1
- package/src/workflow/workflow-intent-diff.ts +4 -1
package/src/tools/resource-gc.ts
CHANGED
|
@@ -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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
125
|
-
|
|
165
|
+
path === "memoryGuard.enabled" ||
|
|
166
|
+
path === "memoryGuard.checkIntervalMs" ||
|
|
167
|
+
path === "resourceGc.sweepIntervalMs"
|
|
126
168
|
) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
354
|
+
return {
|
|
355
|
+
...selected,
|
|
356
|
+
totalUsageBytes: Math.max(parentBytes, selected.totalUsageBytes),
|
|
357
|
+
parentBytes,
|
|
358
|
+
domains,
|
|
359
|
+
};
|
|
168
360
|
}
|
|
169
361
|
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
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
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
await
|
|
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
|
|
231
|
-
if (!
|
|
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 = {
|
|
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
|
-
|
|
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
|
|
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:
|
|
896
|
+
timerActive: state.timerActive,
|
|
358
897
|
sessionCount: activeSessions.size,
|
|
359
898
|
rssWarningActive,
|
|
360
|
-
inProgress:
|
|
361
|
-
generation:
|
|
362
|
-
pendingDeadline,
|
|
363
|
-
pendingOwner: pendingOwner
|
|
364
|
-
deferredDeadline:
|
|
365
|
-
deferredGeneration:
|
|
366
|
-
activeGeneration:
|
|
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
|
-
|
|
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
|
}
|