@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.
- package/CHANGELOG.md +122 -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 +20 -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
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kind-aware invocation reconciliation (prompt | skill) with optional durable store.
|
|
3
|
+
* Preserves Q26 admit/first-terminal/capacity/TTL semantics; indexes and caps are per-kind.
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
PROMPT_RECONCILIATION_ACTIVE_CAPACITY,
|
|
7
|
+
PROMPT_RECONCILIATION_TERMINAL_CAPACITY,
|
|
8
|
+
PROMPT_RECONCILIATION_TERMINAL_TTL_MS,
|
|
9
|
+
type PromptCorrelation,
|
|
10
|
+
type PromptReconciliationStatus,
|
|
11
|
+
sanitizePromptFailure,
|
|
12
|
+
type TurnPromptReconciliation,
|
|
13
|
+
} from "./prompt-reconciliation";
|
|
14
|
+
import type { DurableReconciliationRecord, ReconciliationKind, ReconciliationStore } from "./reconciliation-store";
|
|
15
|
+
|
|
16
|
+
export type { ReconciliationKind };
|
|
17
|
+
|
|
18
|
+
export interface KindCorrelation extends PromptCorrelation {
|
|
19
|
+
kind: ReconciliationKind;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface KindAwareReconciliation {
|
|
23
|
+
admit(kind: ReconciliationKind, clientRef?: string): void;
|
|
24
|
+
releaseAdmission(kind: ReconciliationKind, clientRef?: string): void;
|
|
25
|
+
noteAccepted(
|
|
26
|
+
kind: ReconciliationKind,
|
|
27
|
+
correlation: PromptCorrelation,
|
|
28
|
+
clientRef?: string,
|
|
29
|
+
extra?: { skillName?: string },
|
|
30
|
+
): Promise<void>;
|
|
31
|
+
noteTransition(
|
|
32
|
+
kind: ReconciliationKind,
|
|
33
|
+
correlation: PromptCorrelation | undefined,
|
|
34
|
+
frame: { type: "agent_start" | "agent_end" } | { type: "agent_failed"; error: unknown },
|
|
35
|
+
): Promise<void>;
|
|
36
|
+
lookup(
|
|
37
|
+
kind: ReconciliationKind,
|
|
38
|
+
selector: { commandId?: string; turnId?: string; clientRef?: string },
|
|
39
|
+
): TurnPromptReconciliation;
|
|
40
|
+
cleanup(): void;
|
|
41
|
+
activeCount(kind: ReconciliationKind): number;
|
|
42
|
+
/** Hydrate from durable store (call once at session host start). */
|
|
43
|
+
hydrateFromStore(): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createKindAwareReconciliation(
|
|
47
|
+
options: { now?: () => number; store?: ReconciliationStore | null } = {},
|
|
48
|
+
): KindAwareReconciliation {
|
|
49
|
+
const now = options.now ?? Date.now;
|
|
50
|
+
const store = options.store ?? null;
|
|
51
|
+
const records = new Map<string, DurableReconciliationRecord>();
|
|
52
|
+
const clientRefIndex = new Map<string, string>(); // `${kind}\0${clientRef}` -> key
|
|
53
|
+
const reservedClientRefs = new Map<ReconciliationKind, Set<string>>();
|
|
54
|
+
const reservations: Array<{ kind: ReconciliationKind; clientRef?: string }> = [];
|
|
55
|
+
|
|
56
|
+
const keyOf = (kind: ReconciliationKind, correlation: PromptCorrelation) =>
|
|
57
|
+
`${kind}:${correlation.commandId}:${correlation.turnId}`;
|
|
58
|
+
const refKey = (kind: ReconciliationKind, clientRef: string) => `${kind}\0${clientRef}`;
|
|
59
|
+
|
|
60
|
+
const reservedFor = (kind: ReconciliationKind) => {
|
|
61
|
+
let set = reservedClientRefs.get(kind);
|
|
62
|
+
if (!set) {
|
|
63
|
+
set = new Set();
|
|
64
|
+
reservedClientRefs.set(kind, set);
|
|
65
|
+
}
|
|
66
|
+
return set;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const remove = (key: string) => {
|
|
70
|
+
const record = records.get(key);
|
|
71
|
+
if (!record) return;
|
|
72
|
+
records.delete(key);
|
|
73
|
+
if (record.clientRef !== undefined) {
|
|
74
|
+
const rk = refKey(record.kind, record.clientRef);
|
|
75
|
+
if (clientRefIndex.get(rk) === key) clientRefIndex.delete(rk);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const cleanup = () => {
|
|
80
|
+
const at = now();
|
|
81
|
+
for (const [key, record] of records)
|
|
82
|
+
if (record.terminalAt !== undefined && record.terminalAt + PROMPT_RECONCILIATION_TERMINAL_TTL_MS <= at)
|
|
83
|
+
remove(key);
|
|
84
|
+
for (const kind of ["prompt", "skill"] as const) {
|
|
85
|
+
const terminalEntries = [...records.entries()].filter(
|
|
86
|
+
([, record]) => record.kind === kind && record.terminalAt !== undefined,
|
|
87
|
+
);
|
|
88
|
+
if (terminalEntries.length <= PROMPT_RECONCILIATION_TERMINAL_CAPACITY) continue;
|
|
89
|
+
terminalEntries.sort((a, b) => (a[1].terminalAt as number) - (b[1].terminalAt as number));
|
|
90
|
+
for (const [key] of terminalEntries.slice(0, terminalEntries.length - PROMPT_RECONCILIATION_TERMINAL_CAPACITY))
|
|
91
|
+
remove(key);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const activeCount = (kind: ReconciliationKind) => {
|
|
96
|
+
let count = 0;
|
|
97
|
+
for (const record of records.values()) if (record.kind === kind && record.terminalAt === undefined) count++;
|
|
98
|
+
return count;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const reservationCount = (kind: ReconciliationKind) => reservations.filter(r => r.kind === kind).length;
|
|
102
|
+
|
|
103
|
+
const consumeReservation = (kind: ReconciliationKind, clientRef?: string) => {
|
|
104
|
+
const index = reservations.findIndex(r => r.kind === kind && r.clientRef === clientRef);
|
|
105
|
+
if (index === -1) return;
|
|
106
|
+
reservations.splice(index, 1);
|
|
107
|
+
if (clientRef !== undefined && !reservations.some(r => r.kind === kind && r.clientRef === clientRef))
|
|
108
|
+
reservedFor(kind).delete(clientRef);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const persist = async () => {
|
|
112
|
+
if (!store) return;
|
|
113
|
+
await store.transact(() => [...records.values()].map(r => ({ ...r })));
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const admit = (kind: ReconciliationKind, clientRef?: string) => {
|
|
117
|
+
cleanup();
|
|
118
|
+
const reserved = reservedFor(kind);
|
|
119
|
+
if (clientRef !== undefined && (clientRefIndex.has(refKey(kind, clientRef)) || reserved.has(clientRef)))
|
|
120
|
+
throw Object.assign(
|
|
121
|
+
new Error("A submission with this clientRef is already retained; never reuse a clientRef for retry."),
|
|
122
|
+
{ code: "client_ref_conflict" },
|
|
123
|
+
);
|
|
124
|
+
if (activeCount(kind) + reservationCount(kind) >= PROMPT_RECONCILIATION_ACTIVE_CAPACITY)
|
|
125
|
+
throw Object.assign(new Error("Too many active submissions; reconcile or await terminal state."), {
|
|
126
|
+
code: "reconciliation_capacity",
|
|
127
|
+
});
|
|
128
|
+
reservations.push({ kind, clientRef });
|
|
129
|
+
if (clientRef !== undefined) reserved.add(clientRef);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const releaseAdmission = (kind: ReconciliationKind, clientRef?: string) => {
|
|
133
|
+
consumeReservation(kind, clientRef);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const noteAccepted = async (
|
|
137
|
+
kind: ReconciliationKind,
|
|
138
|
+
correlation: PromptCorrelation,
|
|
139
|
+
clientRef?: string,
|
|
140
|
+
extra?: { skillName?: string },
|
|
141
|
+
) => {
|
|
142
|
+
cleanup();
|
|
143
|
+
consumeReservation(kind, clientRef);
|
|
144
|
+
const at = now();
|
|
145
|
+
const key = keyOf(kind, correlation);
|
|
146
|
+
const record: DurableReconciliationRecord = {
|
|
147
|
+
kind,
|
|
148
|
+
commandId: correlation.commandId,
|
|
149
|
+
turnId: correlation.turnId,
|
|
150
|
+
...(clientRef !== undefined ? { clientRef } : {}),
|
|
151
|
+
status: "accepted",
|
|
152
|
+
acceptedAt: at,
|
|
153
|
+
...(extra?.skillName ? { skillName: extra.skillName } : {}),
|
|
154
|
+
};
|
|
155
|
+
records.set(key, record);
|
|
156
|
+
if (clientRef !== undefined) clientRefIndex.set(refKey(kind, clientRef), key);
|
|
157
|
+
await persist();
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const noteTransition = async (
|
|
161
|
+
kind: ReconciliationKind,
|
|
162
|
+
correlation: PromptCorrelation | undefined,
|
|
163
|
+
frame: { type: "agent_start" | "agent_end" } | { type: "agent_failed"; error: unknown },
|
|
164
|
+
) => {
|
|
165
|
+
if (!correlation) return;
|
|
166
|
+
const record = records.get(keyOf(kind, correlation));
|
|
167
|
+
if (!record || record.terminalAt !== undefined) return;
|
|
168
|
+
if (frame.type === "agent_start") {
|
|
169
|
+
if (record.status === "accepted") {
|
|
170
|
+
record.status = "in_flight";
|
|
171
|
+
record.startedAt = now();
|
|
172
|
+
await persist();
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
record.terminalAt = now();
|
|
177
|
+
if (frame.type === "agent_failed") {
|
|
178
|
+
record.status = "failed";
|
|
179
|
+
record.error = sanitizePromptFailure(frame.error);
|
|
180
|
+
} else {
|
|
181
|
+
record.status = "terminal_ok";
|
|
182
|
+
}
|
|
183
|
+
cleanup();
|
|
184
|
+
await persist();
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const lookup = (
|
|
188
|
+
kind: ReconciliationKind,
|
|
189
|
+
selector: { commandId?: string; turnId?: string; clientRef?: string },
|
|
190
|
+
): TurnPromptReconciliation => {
|
|
191
|
+
cleanup();
|
|
192
|
+
const key =
|
|
193
|
+
selector.clientRef !== undefined
|
|
194
|
+
? clientRefIndex.get(refKey(kind, selector.clientRef))
|
|
195
|
+
: selector.commandId !== undefined && selector.turnId !== undefined
|
|
196
|
+
? keyOf(kind, { commandId: selector.commandId, turnId: selector.turnId })
|
|
197
|
+
: undefined;
|
|
198
|
+
const record = key === undefined ? undefined : records.get(key);
|
|
199
|
+
if (!record) return { status: "unknown" };
|
|
200
|
+
const identity = {
|
|
201
|
+
commandId: record.commandId,
|
|
202
|
+
turnId: record.turnId,
|
|
203
|
+
...(record.clientRef !== undefined ? { clientRef: record.clientRef } : {}),
|
|
204
|
+
acceptedAt: record.acceptedAt,
|
|
205
|
+
};
|
|
206
|
+
if (record.status === "accepted") return { status: "accepted", ...identity };
|
|
207
|
+
if (record.status === "in_flight")
|
|
208
|
+
return { status: "in_flight", ...identity, startedAt: record.startedAt as number };
|
|
209
|
+
const terminal = {
|
|
210
|
+
...identity,
|
|
211
|
+
...(record.startedAt !== undefined ? { startedAt: record.startedAt } : {}),
|
|
212
|
+
terminalAt: record.terminalAt as number,
|
|
213
|
+
};
|
|
214
|
+
if (record.status === "terminal_ok") return { status: "terminal_ok", ...terminal };
|
|
215
|
+
return { status: "failed", ...terminal, error: record.error ?? sanitizePromptFailure(undefined) };
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const hydrateFromStore = async () => {
|
|
219
|
+
if (!store) return;
|
|
220
|
+
const loaded = await store.load();
|
|
221
|
+
records.clear();
|
|
222
|
+
clientRefIndex.clear();
|
|
223
|
+
for (const record of loaded) {
|
|
224
|
+
const key = keyOf(record.kind, record);
|
|
225
|
+
records.set(key, record);
|
|
226
|
+
if (record.clientRef !== undefined) clientRefIndex.set(refKey(record.kind, record.clientRef), key);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
admit,
|
|
232
|
+
releaseAdmission,
|
|
233
|
+
noteAccepted,
|
|
234
|
+
noteTransition,
|
|
235
|
+
lookup,
|
|
236
|
+
cleanup,
|
|
237
|
+
activeCount,
|
|
238
|
+
hydrateFromStore,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export type { PromptReconciliationStatus };
|
|
@@ -49,6 +49,49 @@ const USAGE = [
|
|
|
49
49
|
"/session_recent [create|resume]",
|
|
50
50
|
].join("\n");
|
|
51
51
|
|
|
52
|
+
function tokenizeLifecycleCommand(value: string): string[] | undefined {
|
|
53
|
+
const tokens: string[] = [];
|
|
54
|
+
let token = "";
|
|
55
|
+
let quote: '"' | "'" | undefined;
|
|
56
|
+
let started = false;
|
|
57
|
+
for (let index = 0; index < value.length; index++) {
|
|
58
|
+
const character = value[index]!;
|
|
59
|
+
if (character === "\\") {
|
|
60
|
+
const next = value[index + 1];
|
|
61
|
+
if (next !== undefined && (next === "\\" || next === '"' || next === "'" || /\s/u.test(next))) {
|
|
62
|
+
token += next;
|
|
63
|
+
index++;
|
|
64
|
+
} else token += character;
|
|
65
|
+
started = true;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (quote) {
|
|
69
|
+
if (character === quote) quote = undefined;
|
|
70
|
+
else token += character;
|
|
71
|
+
started = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (character === '"' || character === "'") {
|
|
75
|
+
quote = character;
|
|
76
|
+
started = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (/\s/u.test(character)) {
|
|
80
|
+
if (started) {
|
|
81
|
+
tokens.push(token);
|
|
82
|
+
token = "";
|
|
83
|
+
started = false;
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
token += character;
|
|
88
|
+
started = true;
|
|
89
|
+
}
|
|
90
|
+
if (quote) return undefined;
|
|
91
|
+
if (started) tokens.push(token);
|
|
92
|
+
return tokens;
|
|
93
|
+
}
|
|
94
|
+
|
|
52
95
|
/** True when the text begins with any /session_* token, regardless of addressability. */
|
|
53
96
|
export function isLifecycleCommandLikeText(text: string | undefined): boolean {
|
|
54
97
|
if (!text) return false;
|
|
@@ -98,8 +141,10 @@ export function parseLifecycleCommand(
|
|
|
98
141
|
text: string | undefined,
|
|
99
142
|
ctx: { chatType?: string; botUsername?: string } = {},
|
|
100
143
|
): ParsedLifecycleCommand {
|
|
144
|
+
const tokens = tokenizeLifecycleCommand((text ?? "").trim());
|
|
145
|
+
if (!tokens) return { kind: "usage", message: USAGE };
|
|
146
|
+
const [rawCommand, ...args] = tokens;
|
|
101
147
|
const raw = (text ?? "").trim();
|
|
102
|
-
const [rawCommand, ...args] = raw.split(/\s+/);
|
|
103
148
|
const command = normalizeLifecycleCommandToken(rawCommand ?? "", ctx);
|
|
104
149
|
if (command === undefined) return { kind: "none" };
|
|
105
150
|
|
|
@@ -140,9 +185,8 @@ export function parseLifecycleCommand(
|
|
|
140
185
|
|
|
141
186
|
// /session_create <kind> ... [--mpreset <profile>]
|
|
142
187
|
const { positional, modelPreset } = extractModelPreset(args);
|
|
143
|
-
if (modelPreset !== undefined &&
|
|
188
|
+
if (modelPreset !== undefined && modelPreset.length === 0)
|
|
144
189
|
return { kind: "reject", reason: "invalid_target", message: `Invalid model preset name.\n\n${USAGE}` };
|
|
145
|
-
}
|
|
146
190
|
const kind = positional[0];
|
|
147
191
|
if (kind === "path") {
|
|
148
192
|
if (positional.length !== 2) return { kind: "usage", message: USAGE };
|
|
@@ -238,11 +282,12 @@ function isSafeBranch(value: string): boolean {
|
|
|
238
282
|
/**
|
|
239
283
|
* Map a lifecycle response/error to a user-facing Telegram message (G010).
|
|
240
284
|
*
|
|
241
|
-
* Only derives text from sessionId, mode, reason, a safe message,
|
|
242
|
-
* {sessionId,path} — never a token or prompt. Each
|
|
243
|
-
* actionable copy; an "in progress" pending response
|
|
285
|
+
* Only derives text from sessionId, mode, reason, a safe message, the originating
|
|
286
|
+
* lifecycle verb, and candidate {sessionId,path} — never a token or prompt. Each
|
|
287
|
+
* error reason gets tailored, actionable copy; an "in progress" pending response
|
|
288
|
+
* is surfaced distinctly.
|
|
244
289
|
*/
|
|
245
|
-
export function formatLifecycleOutcome(r: SessionLifecycleResponse): string {
|
|
290
|
+
export function formatLifecycleOutcome(r: SessionLifecycleResponse, verb?: LifecycleCommandVerb): string {
|
|
246
291
|
switch (r.type) {
|
|
247
292
|
case "session_create_response":
|
|
248
293
|
return `\u{1f680} Launching session ${r.sessionId} in tmux. It will appear once ready \u2014 check /session_recent.`;
|
|
@@ -282,9 +327,17 @@ export function formatLifecycleOutcome(r: SessionLifecycleResponse): string {
|
|
|
282
327
|
case "unsupported_platform":
|
|
283
328
|
return "Remote session lifecycle is unavailable on this psmux host because SKC cannot prove immutable session identity. No lifecycle action was performed. Use a local SKC terminal with a supported tmux provider.";
|
|
284
329
|
case "terminal_uncertain":
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
330
|
+
if (/in progress/i.test(r.message)) return "\u23f3 That request is already in progress \u2014 hold on.";
|
|
331
|
+
switch (verb) {
|
|
332
|
+
case "session_create":
|
|
333
|
+
return "\u26a0\ufe0f Create outcome uncertain. The session may already be starting \u2014 check /session_recent before retrying to avoid starting it twice.";
|
|
334
|
+
case "session_close":
|
|
335
|
+
return "\u26a0\ufe0f Close outcome uncertain. The session may already be closed \u2014 check /session_recent before retrying.";
|
|
336
|
+
case "session_resume":
|
|
337
|
+
return "\u26a0\ufe0f Resume outcome uncertain. The session may already be reattached or restarting \u2014 check /session_recent before retrying.";
|
|
338
|
+
default:
|
|
339
|
+
return "\u26a0\ufe0f Outcome uncertain. Check /session_recent before retrying.";
|
|
340
|
+
}
|
|
288
341
|
default:
|
|
289
342
|
return `\u26a0\ufe0f ${r.reason}: ${r.message}`;
|
|
290
343
|
}
|
|
@@ -1178,16 +1178,25 @@ export function outcomeToResponse(frame: SessionLifecycleRequest, outcome: Lifec
|
|
|
1178
1178
|
if (frame.type === "session_close") {
|
|
1179
1179
|
if (typeof e.sessionId !== "string" || typeof e.processGone !== "boolean")
|
|
1180
1180
|
throw new Error("invalid close success ledger entry");
|
|
1181
|
+
if (!e.processGone) {
|
|
1182
|
+
return {
|
|
1183
|
+
type: "session_lifecycle_error",
|
|
1184
|
+
requestId: frame.requestId,
|
|
1185
|
+
status: "error",
|
|
1186
|
+
reason: "terminal_uncertain",
|
|
1187
|
+
message: "close did not confirm process termination",
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1181
1190
|
return {
|
|
1182
1191
|
type: "session_close_response",
|
|
1183
1192
|
requestId: frame.requestId,
|
|
1184
1193
|
status: "ok",
|
|
1185
1194
|
sessionId: e.sessionId,
|
|
1186
|
-
processGone:
|
|
1195
|
+
processGone: true,
|
|
1187
1196
|
historyPreserved: true,
|
|
1188
1197
|
// The killed session's per-session endpoint record is reaped by the
|
|
1189
1198
|
// daemon's dead-PID scan (scanRoots), so it is effectively stale.
|
|
1190
|
-
endpointStale:
|
|
1199
|
+
endpointStale: true,
|
|
1191
1200
|
};
|
|
1192
1201
|
}
|
|
1193
1202
|
if (
|
|
@@ -309,6 +309,21 @@ export async function handleLifecycleRequest(
|
|
|
309
309
|
|
|
310
310
|
// 4. Durable idempotency.
|
|
311
311
|
const doc = await deps.store.read();
|
|
312
|
+
const existing = doc.entries[key];
|
|
313
|
+
if (
|
|
314
|
+
frame.type === "session_close" &&
|
|
315
|
+
existing?.verb === "session_close" &&
|
|
316
|
+
existing.requestHash === hash &&
|
|
317
|
+
existing.state === "success" &&
|
|
318
|
+
typeof existing.sessionId === "string" &&
|
|
319
|
+
existing.processGone === false
|
|
320
|
+
) {
|
|
321
|
+
existing.state = "terminal_uncertain";
|
|
322
|
+
existing.reason = "terminal_uncertain";
|
|
323
|
+
existing.updatedAt = deps.now();
|
|
324
|
+
delete existing.processGone;
|
|
325
|
+
await deps.store.write(doc);
|
|
326
|
+
}
|
|
312
327
|
const dup = classifyDuplicate(doc.entries[key], hash);
|
|
313
328
|
switch (dup.kind) {
|
|
314
329
|
case "conflict":
|
|
@@ -394,12 +409,28 @@ export async function handleLifecycleRequest(
|
|
|
394
409
|
|
|
395
410
|
if (frame.type === "session_close") {
|
|
396
411
|
const closed = await deps.closeSession(frame.target);
|
|
412
|
+
if (!closed.processGone) {
|
|
413
|
+
Object.assign(entry, {
|
|
414
|
+
state: "terminal_uncertain",
|
|
415
|
+
updatedAt: deps.now(),
|
|
416
|
+
reason: "terminal_uncertain",
|
|
417
|
+
sessionId: frame.target.sessionId,
|
|
418
|
+
tmuxSession: frame.target.tmuxSession,
|
|
419
|
+
});
|
|
420
|
+
await deps.store.write(doc);
|
|
421
|
+
await deps.audit({ ...baseAudit, event: "terminal_uncertain", reason: "terminal_uncertain" });
|
|
422
|
+
return {
|
|
423
|
+
status: "error",
|
|
424
|
+
reason: "terminal_uncertain",
|
|
425
|
+
message: "session_close could not confirm process disappearance; manual check",
|
|
426
|
+
};
|
|
427
|
+
}
|
|
397
428
|
Object.assign(entry, {
|
|
398
429
|
state: "success",
|
|
399
430
|
updatedAt: deps.now(),
|
|
400
431
|
sessionId: frame.target.sessionId,
|
|
401
432
|
tmuxSession: frame.target.tmuxSession,
|
|
402
|
-
processGone:
|
|
433
|
+
processGone: true,
|
|
403
434
|
});
|
|
404
435
|
await deps.store.write(doc);
|
|
405
436
|
await deps.audit({ ...baseAudit, event: "success" });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CasRestoreResult } from "../../config/atomic-yaml-patch";
|
|
2
2
|
import type { RawSettings, SettingsAtomicPatch, SettingsAtomicReceipt } from "../../config/settings";
|
|
3
|
+
import { isProcessIncarnation, processIncarnation } from "../broker/process-incarnation";
|
|
3
4
|
import {
|
|
4
5
|
getNotificationConfig,
|
|
5
6
|
hasNonBlankValue,
|
|
@@ -53,6 +54,7 @@ export interface ProposedTelegramIdentityPreflightInput {
|
|
|
53
54
|
deps?: {
|
|
54
55
|
fs?: TelegramDaemonFs;
|
|
55
56
|
pidAlive?: (pid: number) => boolean;
|
|
57
|
+
pidIncarnation?: (pid: number) => string | undefined;
|
|
56
58
|
};
|
|
57
59
|
}
|
|
58
60
|
|
|
@@ -109,6 +111,16 @@ export async function proposedTelegramIdentity(
|
|
|
109
111
|
|
|
110
112
|
const pidAlive = input.deps?.pidAlive ?? defaultPidAlive;
|
|
111
113
|
if (!pidAlive(metadata.pid)) return { status: "absent" };
|
|
114
|
+
const pidIncarnation = input.deps?.pidIncarnation ?? processIncarnation;
|
|
115
|
+
const persistedIncarnation = state.incarnation;
|
|
116
|
+
const currentIncarnation = pidIncarnation(metadata.pid);
|
|
117
|
+
if (
|
|
118
|
+
!isProcessIncarnation(persistedIncarnation) ||
|
|
119
|
+
!isProcessIncarnation(currentIncarnation) ||
|
|
120
|
+
persistedIncarnation !== currentIncarnation
|
|
121
|
+
) {
|
|
122
|
+
return { status: "absent" };
|
|
123
|
+
}
|
|
112
124
|
|
|
113
125
|
if (state.tokenFingerprint === tokenFingerprint(input.botToken) && state.chatId === input.chatId) {
|
|
114
126
|
return {
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
tokenFingerprint,
|
|
29
29
|
} from "./config";
|
|
30
30
|
import { type DaemonPaths, daemonPaths, HEARTBEAT_TTL_MS } from "./daemon-paths";
|
|
31
|
+
import { type OwnerFreshnessSnapshot, readOwnerFreshnessSnapshot, type TelegramDaemonFs } from "./telegram-daemon";
|
|
31
32
|
import { DAEMON_GENERATION } from "./telegram-daemon-contract";
|
|
32
33
|
|
|
33
34
|
const DEFAULT_API_BASE = "https://api.telegram.org";
|
|
@@ -251,19 +252,29 @@ export function formatNotificationStatusReport(report: NotificationStatusReport)
|
|
|
251
252
|
|
|
252
253
|
// --- endpoint / daemon file readers -------------------------------------
|
|
253
254
|
|
|
254
|
-
interface
|
|
255
|
+
export interface NotificationEndpointView {
|
|
255
256
|
sessionId: string;
|
|
256
257
|
pid: number | undefined;
|
|
257
258
|
stale: boolean;
|
|
258
259
|
}
|
|
259
260
|
|
|
260
|
-
type
|
|
261
|
-
|
|
261
|
+
export type NotificationEndpointLiveness = "live" | "dead" | "unknown";
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Classification used by recovery and startup takeover. A file is an endpoint
|
|
265
|
+
* only when it has endpoint authority fields; lifecycle/audit records are never
|
|
266
|
+
* candidates for endpoint cleanup.
|
|
267
|
+
*/
|
|
268
|
+
export type NotificationEndpointClassification =
|
|
269
|
+
| {
|
|
270
|
+
kind: "endpoint";
|
|
271
|
+
view: NotificationEndpointView;
|
|
272
|
+
liveness: NotificationEndpointLiveness;
|
|
273
|
+
identity: NotificationEndpointFileIdentity;
|
|
274
|
+
}
|
|
262
275
|
| { kind: "non-endpoint" }
|
|
263
276
|
| { kind: "unreadable" };
|
|
264
277
|
|
|
265
|
-
type EndpointLiveness = "live" | "dead" | "unknown";
|
|
266
|
-
|
|
267
278
|
/**
|
|
268
279
|
* Classify an endpoint using owner-proof semantics. An endpoint is only `dead`
|
|
269
280
|
* with positive proof: an explicit `stale` tombstone, or a recorded pid that is
|
|
@@ -271,7 +282,10 @@ type EndpointLiveness = "live" | "dead" | "unknown";
|
|
|
271
282
|
* must never be treated as dead — removing it could delete a live session's
|
|
272
283
|
* discovery file that simply omitted a pid.
|
|
273
284
|
*/
|
|
274
|
-
function
|
|
285
|
+
export function notificationEndpointLiveness(
|
|
286
|
+
view: NotificationEndpointView,
|
|
287
|
+
pidAlive: (pid: number) => boolean,
|
|
288
|
+
): NotificationEndpointLiveness {
|
|
275
289
|
if (view.stale) return "dead";
|
|
276
290
|
if (view.pid === undefined) return "unknown";
|
|
277
291
|
return pidAlive(view.pid) ? "live" : "dead";
|
|
@@ -314,11 +328,19 @@ function isCanonicalLifecycleArtifactName(name: string): boolean {
|
|
|
314
328
|
);
|
|
315
329
|
}
|
|
316
330
|
|
|
317
|
-
function unreadableEndpointResult(file: string):
|
|
331
|
+
function unreadableEndpointResult(file: string): NotificationEndpointClassification {
|
|
318
332
|
return isCanonicalLifecycleArtifactName(path.basename(file)) ? { kind: "non-endpoint" } : { kind: "unreadable" };
|
|
319
333
|
}
|
|
320
334
|
|
|
321
|
-
|
|
335
|
+
/**
|
|
336
|
+
* Read and classify one endpoint candidate. The returned identity belongs to
|
|
337
|
+
* exactly the bytes inspected and is required for any later deletion.
|
|
338
|
+
*/
|
|
339
|
+
export async function classifyNotificationEndpoint(
|
|
340
|
+
fs: Pick<NotificationServiceFs, "readEndpointFile">,
|
|
341
|
+
file: string,
|
|
342
|
+
pidAlive: (pid: number) => boolean,
|
|
343
|
+
): Promise<NotificationEndpointClassification> {
|
|
322
344
|
let endpoint: NotificationEndpointFile;
|
|
323
345
|
let raw: string;
|
|
324
346
|
try {
|
|
@@ -334,16 +356,19 @@ async function readEndpointView(fs: NotificationServiceFs, file: string): Promis
|
|
|
334
356
|
return unreadableEndpointResult(file);
|
|
335
357
|
}
|
|
336
358
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { kind: "non-endpoint" };
|
|
359
|
+
if (path.basename(file) === "broker.json") return { kind: "non-endpoint" };
|
|
337
360
|
const rec = parsed as Record<string, unknown>;
|
|
338
361
|
if (isLifecycleArtifact(rec) || typeof rec.url !== "string" || typeof rec.token !== "string")
|
|
339
362
|
return { kind: "non-endpoint" };
|
|
363
|
+
const view: NotificationEndpointView = {
|
|
364
|
+
sessionId: typeof rec.sessionId === "string" ? rec.sessionId : path.basename(file, ".json"),
|
|
365
|
+
pid: safePositiveInteger(rec.pid),
|
|
366
|
+
stale: rec.stale === true,
|
|
367
|
+
};
|
|
340
368
|
return {
|
|
341
369
|
kind: "endpoint",
|
|
342
|
-
view
|
|
343
|
-
|
|
344
|
-
pid: safePositiveInteger(rec.pid),
|
|
345
|
-
stale: rec.stale === true,
|
|
346
|
-
},
|
|
370
|
+
view,
|
|
371
|
+
liveness: notificationEndpointLiveness(view, pidAlive),
|
|
347
372
|
identity: endpoint.identity,
|
|
348
373
|
};
|
|
349
374
|
}
|
|
@@ -554,10 +579,21 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
|
|
|
554
579
|
checks.push({ name: "config", level: "ok", detail: "enabled with at least one configured adapter" });
|
|
555
580
|
}
|
|
556
581
|
|
|
557
|
-
// Daemon ownership state (offline;
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
582
|
+
// Daemon ownership state (offline; corrupt or unreadable state degrades to a
|
|
583
|
+
// warning instead of making the health command itself fail).
|
|
584
|
+
let daemonStateUnreadable = false;
|
|
585
|
+
let snapshot: OwnerFreshnessSnapshot;
|
|
586
|
+
try {
|
|
587
|
+
snapshot = await readOwnerFreshnessSnapshot({
|
|
588
|
+
settings: opts.settings,
|
|
589
|
+
fs: fs as unknown as TelegramDaemonFs,
|
|
590
|
+
});
|
|
591
|
+
} catch {
|
|
592
|
+
daemonStateUnreadable = true;
|
|
593
|
+
snapshot = { ownerTag: null, effectiveHeartbeatAt: undefined, legacyEmbedded: false, state: undefined };
|
|
594
|
+
}
|
|
595
|
+
const state = snapshot.state ? parseDaemonState(JSON.stringify(snapshot.state)) : undefined;
|
|
596
|
+
const heartbeatAt = finiteNonNegativeNumber(snapshot.effectiveHeartbeatAt);
|
|
561
597
|
const daemon: DaemonHealth = {
|
|
562
598
|
present: Boolean(state),
|
|
563
599
|
ownerId: state?.ownerId,
|
|
@@ -576,7 +612,9 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
|
|
|
576
612
|
currentGeneration: DAEMON_GENERATION,
|
|
577
613
|
generationRelation: daemonGenerationRelation(state),
|
|
578
614
|
};
|
|
579
|
-
if (
|
|
615
|
+
if (daemonStateUnreadable) {
|
|
616
|
+
checks.push({ name: "daemon", level: "warn", detail: "daemon ownership record is corrupt or unreadable" });
|
|
617
|
+
} else if (!state) {
|
|
580
618
|
checks.push({ name: "daemon", level: "ok", detail: "no daemon ownership record (none running)" });
|
|
581
619
|
} else if (!daemon.alive) {
|
|
582
620
|
checks.push({
|
|
@@ -604,14 +642,13 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
|
|
|
604
642
|
let unknownEndpoints = 0;
|
|
605
643
|
let unreadable = 0;
|
|
606
644
|
for (const name of files) {
|
|
607
|
-
const record = await
|
|
645
|
+
const record = await classifyNotificationEndpoint(fs, path.join(dir, name), pidAlive);
|
|
608
646
|
if (record.kind === "non-endpoint") continue;
|
|
609
647
|
if (record.kind === "unreadable") {
|
|
610
648
|
unreadable += 1;
|
|
611
649
|
continue;
|
|
612
650
|
}
|
|
613
|
-
|
|
614
|
-
switch (endpointLiveness(view, pidAlive)) {
|
|
651
|
+
switch (record.liveness) {
|
|
615
652
|
case "live":
|
|
616
653
|
live += 1;
|
|
617
654
|
break;
|
|
@@ -877,7 +914,16 @@ async function detachTransitionMarker(
|
|
|
877
914
|
): Promise<boolean> {
|
|
878
915
|
if (!snapshot.identity || !fs.exactUnlink) return false;
|
|
879
916
|
try {
|
|
880
|
-
|
|
917
|
+
const removed = await fs.exactUnlink(path, snapshot.identity);
|
|
918
|
+
if (removed.ok) return true;
|
|
919
|
+
// Accept only typed retained authority: a concrete detached quarantine plus
|
|
920
|
+
// a proven-absent canonical marker pathname. Anything else stays fail-closed.
|
|
921
|
+
return (
|
|
922
|
+
removed.code === "cleanup_pending" &&
|
|
923
|
+
typeof removed.detachedPath === "string" &&
|
|
924
|
+
removed.detachedPath.length > 0 &&
|
|
925
|
+
(await fs.readFile(path, "utf8").catch(() => undefined)) === undefined
|
|
926
|
+
);
|
|
881
927
|
} catch {
|
|
882
928
|
return false;
|
|
883
929
|
}
|
|
@@ -1040,7 +1086,7 @@ export async function recoverNotifications(opts: RecoveryOptions): Promise<Notif
|
|
|
1040
1086
|
let unreadable = 0;
|
|
1041
1087
|
for (const name of files) {
|
|
1042
1088
|
const file = path.join(dir, name);
|
|
1043
|
-
const record = await
|
|
1089
|
+
const record = await classifyNotificationEndpoint(fs, file, pidAlive);
|
|
1044
1090
|
if (record.kind === "non-endpoint") continue;
|
|
1045
1091
|
if (record.kind === "unreadable") {
|
|
1046
1092
|
// Leave unparseable files untouched: they may be mid-write by a live server.
|
|
@@ -1048,7 +1094,7 @@ export async function recoverNotifications(opts: RecoveryOptions): Promise<Notif
|
|
|
1048
1094
|
continue;
|
|
1049
1095
|
}
|
|
1050
1096
|
const view = record.view;
|
|
1051
|
-
if (
|
|
1097
|
+
if (record.liveness !== "dead") {
|
|
1052
1098
|
// Keep live AND unknown (PID-less) endpoints: only positive proof of
|
|
1053
1099
|
// death (a stale tombstone or a dead pid) authorizes removal.
|
|
1054
1100
|
kept += 1;
|
|
@@ -1057,11 +1103,27 @@ export async function recoverNotifications(opts: RecoveryOptions): Promise<Notif
|
|
|
1057
1103
|
try {
|
|
1058
1104
|
const result = await fs.exactUnlink(file, record.identity);
|
|
1059
1105
|
if (!result.ok) {
|
|
1060
|
-
recoveryFailures += 1;
|
|
1061
1106
|
if (result.detachedPath) detached.push(result.detachedPath);
|
|
1062
1107
|
if (result.retainedSuccessorPath) retainedSuccessors.push(result.retainedSuccessorPath);
|
|
1063
1108
|
if (result.retainedPlaceholderPath) retainedPlaceholders.push(result.retainedPlaceholderPath);
|
|
1064
1109
|
if (result.retainedUnknownPath) retainedUnknown.push(result.retainedUnknownPath);
|
|
1110
|
+
// Typed retained authority with a concrete quarantine and a proven-absent
|
|
1111
|
+
// canonical endpoint counts as removed; the detached path above is the
|
|
1112
|
+
// durable evidence. Anything else is a genuine recovery failure.
|
|
1113
|
+
const retainedRemoved =
|
|
1114
|
+
result.code === "cleanup_pending" &&
|
|
1115
|
+
typeof result.detachedPath === "string" &&
|
|
1116
|
+
result.detachedPath.length > 0 &&
|
|
1117
|
+
(await fs.readFile(file, "utf8").catch(() => undefined)) === undefined;
|
|
1118
|
+
if (retainedRemoved) {
|
|
1119
|
+
removed.push({
|
|
1120
|
+
sessionId: view.sessionId,
|
|
1121
|
+
pid: view.pid,
|
|
1122
|
+
reason: view.stale ? "stale-flag" : "dead-pid",
|
|
1123
|
+
});
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
recoveryFailures += 1;
|
|
1065
1127
|
if (
|
|
1066
1128
|
!result.detachedPath &&
|
|
1067
1129
|
!result.retainedSuccessorPath &&
|