@gajae-code/coding-agent 0.10.2 → 0.11.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 +50 -2
- package/dist/types/append-only-mode.d.ts +1 -1
- package/dist/types/cli/args.d.ts +4 -5
- package/dist/types/cli/daemon-cli.d.ts +12 -1
- package/dist/types/cli/notify-cli.d.ts +21 -1
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/commands/daemon.d.ts +39 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/commands/notify.d.ts +28 -1
- package/dist/types/commands/sdk.d.ts +33 -0
- package/dist/types/config/atomic-yaml-patch.d.ts +85 -0
- package/dist/types/config/model-profile-activation.d.ts +35 -15
- package/dist/types/config/model-profiles.d.ts +11 -5
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/model-resolver.d.ts +39 -6
- package/dist/types/config/model-selector-value.d.ts +17 -0
- package/dist/types/config/models-config-schema.d.ts +10 -6
- package/dist/types/config/settings-schema.d.ts +133 -6
- package/dist/types/config/settings.d.ts +63 -35
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -6
- package/dist/types/coordinator-mcp/server.d.ts +6 -49
- package/dist/types/daemon/builtin.d.ts +6 -5
- package/dist/types/daemon/control-types.d.ts +1 -1
- package/dist/types/discovery/mcp-json.d.ts +12 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
- package/dist/types/extensibility/extensions/compact-handler.d.ts +4 -2
- package/dist/types/extensibility/extensions/get-commands-handler.d.ts +2 -2
- package/dist/types/extensibility/extensions/loader.d.ts +10 -1
- package/dist/types/extensibility/extensions/runner.d.ts +3 -3
- package/dist/types/extensibility/extensions/types.d.ts +141 -16
- package/dist/types/extensibility/hooks/types.d.ts +1 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -2
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +3 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +3 -4
- package/dist/types/gjc-runtime/tmux-owner-isolation-cli.d.ts +13 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +4 -1
- package/dist/types/gjc-runtime/windows-powershell-command.d.ts +9 -0
- package/dist/types/harness-control-plane/operate.d.ts +5 -5
- package/dist/types/harness-control-plane/owner.d.ts +18 -4
- package/dist/types/harness-control-plane/receipts.d.ts +0 -1
- package/dist/types/harness-control-plane/sdk-transport.d.ts +43 -0
- package/dist/types/harness-control-plane/session-transport.d.ts +48 -0
- package/dist/types/harness-control-plane/types.d.ts +3 -4
- package/dist/types/lsp/index.d.ts +1 -1
- package/dist/types/main.d.ts +32 -38
- package/dist/types/modes/acp/acp-agent.d.ts +224 -53
- package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -0
- package/dist/types/modes/acp/acp-mode.d.ts +6 -15
- package/dist/types/modes/acp/permission-mode.d.ts +4 -0
- package/dist/types/modes/acp/startup-options.d.ts +6 -0
- package/dist/types/modes/components/agent-dashboard.d.ts +3 -0
- package/dist/types/modes/components/model-selector.d.ts +1 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +135 -0
- package/dist/types/modes/components/settings-selector.d.ts +6 -4
- package/dist/types/modes/components/welcome.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +35 -1
- package/dist/types/modes/index.d.ts +0 -4
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/runtime-init.d.ts +2 -2
- package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +6 -9
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -6
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +6 -6
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +4 -9
- package/dist/types/modes/shared/agent-wire/event-observation.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +169 -30
- package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +7 -7
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +105 -0
- package/dist/types/modes/types.d.ts +5 -0
- package/dist/types/runtime-mcp/client.d.ts +2 -0
- package/dist/types/runtime-mcp/config.d.ts +5 -1
- package/dist/types/runtime-mcp/manager.d.ts +8 -1
- package/dist/types/runtime-mcp/types.d.ts +4 -0
- package/dist/types/sdk/acp/adapter.d.ts +67 -0
- package/dist/types/sdk/acp/index.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +53 -0
- package/dist/types/sdk/broker/discovery.d.ts +30 -0
- package/dist/types/sdk/broker/ensure.d.ts +33 -0
- package/dist/types/sdk/broker/identity.d.ts +3 -0
- package/dist/types/sdk/broker/index.d.ts +8 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +95 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +85 -0
- package/dist/types/sdk/broker/process-incarnation.d.ts +14 -0
- package/dist/types/sdk/broker/runtime.d.ts +33 -0
- package/dist/types/sdk/broker/session-index.d.ts +57 -0
- package/dist/types/sdk/broker/state-version.d.ts +8 -0
- package/dist/types/sdk/broker/transport.d.ts +9 -0
- package/dist/types/sdk/bus/chat-command-policy.d.ts +49 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +21 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +111 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +77 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +84 -0
- package/dist/types/{notifications → sdk/bus}/config-commands.d.ts +7 -2
- package/dist/types/sdk/bus/config.d.ts +274 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +72 -0
- package/dist/types/sdk/bus/discord-conversation.d.ts +58 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +45 -0
- package/dist/types/sdk/bus/discord-live-provider.d.ts +70 -0
- package/dist/types/sdk/bus/discord-provider.d.ts +85 -0
- package/dist/types/{notifications → sdk/bus}/html-format.d.ts +2 -2
- package/dist/types/{notifications → sdk/bus}/index.d.ts +113 -13
- package/dist/types/{notifications → sdk/bus}/lifecycle-control-runtime.d.ts +4 -2
- package/dist/types/{notifications → sdk/bus}/lifecycle-orchestrator.d.ts +18 -21
- package/dist/types/{notifications → sdk/bus}/managed-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/native-runtime-compatibility.d.ts +13 -0
- package/dist/types/sdk/bus/notification-orchestration.d.ts +170 -0
- package/dist/types/{notifications → sdk/bus}/notification-service.d.ts +7 -1
- package/dist/types/sdk/bus/session-control.d.ts +83 -0
- package/dist/types/sdk/bus/slack-conversation.d.ts +56 -0
- package/dist/types/sdk/bus/slack-daemon.d.ts +60 -0
- package/dist/types/sdk/bus/slack-live-provider.d.ts +59 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +55 -0
- package/dist/types/{notifications → sdk/bus}/telegram-cli.d.ts +4 -4
- package/dist/types/{notifications → sdk/bus}/telegram-daemon-cli.d.ts +14 -5
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -0
- package/dist/types/{notifications → sdk/bus}/telegram-daemon-control.d.ts +2 -2
- package/dist/types/{notifications → sdk/bus}/telegram-daemon.d.ts +55 -17
- package/dist/types/{notifications → sdk/bus}/telegram-reference.d.ts +2 -2
- package/dist/types/sdk/bus/telegram-setup.d.ts +128 -0
- package/dist/types/{notifications → sdk/bus}/topic-registry.d.ts +11 -0
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/session-cli.d.ts +19 -0
- package/dist/types/sdk/client/client.d.ts +1 -0
- package/dist/types/sdk/client/discovery.d.ts +27 -0
- package/dist/types/sdk/client/index.d.ts +2 -0
- package/dist/types/sdk/host/control/dispatch.d.ts +34 -0
- package/dist/types/sdk/host/control/index.d.ts +2 -0
- package/dist/types/sdk/host/control/operations.d.ts +65 -0
- package/dist/types/sdk/host/events.d.ts +35 -0
- package/dist/types/sdk/host/host.d.ts +35 -0
- package/dist/types/sdk/host/index.d.ts +4 -0
- package/dist/types/sdk/host/query/cursor.d.ts +62 -0
- package/dist/types/sdk/host/query/handlers.d.ts +70 -0
- package/dist/types/sdk/host/query/index.d.ts +3 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +60 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +45 -0
- package/dist/types/sdk/host/types.d.ts +32 -0
- package/dist/types/sdk/index.d.ts +6 -0
- package/dist/types/sdk/lifecycle-session.d.ts +14 -0
- package/dist/types/sdk/mcp/index.d.ts +1 -0
- package/dist/types/sdk/mcp/server.d.ts +37 -0
- package/dist/types/sdk/models.d.ts +44 -0
- package/dist/types/sdk/protocol/adapter-validation.d.ts +14 -0
- package/dist/types/sdk/protocol/operation-registry.d.ts +24 -0
- package/dist/types/{sdk.d.ts → sdk/session.d.ts} +44 -38
- package/dist/types/sdk/startup-capability.d.ts +50 -0
- package/dist/types/session/agent-session.d.ts +114 -21
- package/dist/types/session/default-model-selection.d.ts +25 -0
- package/dist/types/session/fallback-chain-controller.d.ts +52 -0
- package/dist/types/session/session-manager.d.ts +58 -1
- package/dist/types/setup/credential-auto-import.d.ts +36 -6
- package/dist/types/tools/ask-answer-registry.d.ts +1 -1
- package/dist/types/tools/browser/benchmark/detector-report.d.ts +63 -0
- package/dist/types/tools/browser/benchmark/run-suite.d.ts +18 -0
- package/dist/types/tools/browser/launch.d.ts +22 -0
- package/dist/types/tools/browser/profile-discovery.d.ts +31 -0
- package/dist/types/tools/browser/profile-posture.d.ts +37 -0
- package/dist/types/tools/browser/profile-reuse.d.ts +39 -0
- package/dist/types/tools/browser/profile-warmup.d.ts +39 -0
- package/dist/types/tools/browser/registry.d.ts +19 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
- package/dist/types/tools/index.d.ts +8 -8
- package/package.json +34 -13
- package/scripts/build-sdk-package-smoke.ts +94 -0
- package/scripts/capture-notifications-settings-showcase.ts +435 -0
- package/scripts/compile-args.ts +4 -2
- package/scripts/g005-daemon-smoke.ts +4 -5
- package/scripts/g011-daemon-path-smoke.ts +5 -5
- package/scripts/generate-sdk-adapter-parity-manifest.ts +163 -0
- package/scripts/generate-sdk-operation-inventory.ts +867 -0
- package/scripts/generate-telegram-baseline-manifest.ts +128 -0
- package/scripts/manifest-schema.ts +124 -0
- package/scripts/run-test-manifest.ts +153 -0
- package/scripts/run-threat-model-check.ts +157 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +3467 -0
- package/scripts/verify-gjc-sdk-rename.ts +90 -0
- package/src/append-only-mode.ts +1 -1
- package/src/cli/args.ts +43 -12
- package/src/cli/auth-gateway-cli.ts +2 -2
- package/src/cli/config-cli.ts +2 -1
- package/src/cli/daemon-cli.ts +63 -14
- package/src/cli/list-models.ts +1 -1
- package/src/cli/notify-cli.ts +288 -286
- package/src/cli.ts +21 -2
- package/src/commands/daemon.ts +50 -6
- package/src/commands/harness.ts +93 -36
- package/src/commands/launch.ts +5 -2
- package/src/commands/mcp-serve.ts +19 -12
- package/src/commands/notify.ts +36 -3
- package/src/commands/sdk.ts +426 -0
- package/src/commit/agentic/agent.ts +1 -0
- package/src/config/atomic-yaml-patch.ts +423 -0
- package/src/config/model-profile-activation.ts +240 -89
- package/src/config/model-profiles.ts +18 -15
- package/src/config/model-registry.ts +51 -26
- package/src/config/model-resolver.ts +138 -51
- package/src/config/model-selector-value.ts +50 -0
- package/src/config/models-config-schema.ts +14 -21
- package/src/config/settings-schema.ts +128 -10
- package/src/config/settings.ts +1120 -158
- package/src/coordinator-mcp/model-preset.ts +74 -20
- package/src/coordinator-mcp/server.ts +1597 -3052
- package/src/daemon/builtin.ts +8 -5
- package/src/daemon/control-types.ts +1 -1
- package/src/defaults/gjc/skills/team/SKILL.md +4 -4
- package/src/discovery/mcp-json.ts +327 -20
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/compact-handler.ts +3 -3
- package/src/extensibility/extensions/get-commands-handler.ts +2 -2
- package/src/extensibility/extensions/loader.ts +69 -5
- package/src/extensibility/extensions/runner.ts +112 -4
- package/src/extensibility/extensions/types.ts +176 -17
- package/src/extensibility/hooks/types.ts +1 -1
- package/src/gjc-runtime/launch-tmux.ts +170 -86
- package/src/gjc-runtime/psmux-detect.ts +82 -4
- package/src/gjc-runtime/session-state-sidecar.ts +41 -31
- package/src/gjc-runtime/team-runtime.ts +54 -35
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +53 -17
- package/src/gjc-runtime/tmux-owner-isolation.ts +1 -1
- package/src/gjc-runtime/tmux-sessions.ts +19 -13
- package/src/gjc-runtime/ultragoal-guard.ts +53 -6
- package/src/gjc-runtime/ultragoal-runtime.ts +155 -3
- package/src/gjc-runtime/windows-powershell-command.ts +58 -0
- package/src/harness-control-plane/operate.ts +9 -9
- package/src/harness-control-plane/owner.ts +283 -39
- package/src/harness-control-plane/receipts.ts +0 -1
- package/src/harness-control-plane/sdk-transport.ts +395 -0
- package/src/harness-control-plane/session-transport.ts +94 -0
- package/src/harness-control-plane/types.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +34 -35
- package/src/lsp/client.ts +42 -6
- package/src/lsp/index.ts +2 -2
- package/src/main.ts +299 -204
- package/src/modes/DESIGN.md +258 -0
- package/src/modes/acp/acp-agent.ts +1228 -2612
- package/src/modes/acp/acp-client-bridge.ts +2 -20
- package/src/modes/acp/acp-event-mapper.ts +18 -2
- package/src/modes/acp/acp-mode.ts +8 -36
- package/src/modes/acp/permission-mode.ts +25 -0
- package/src/modes/acp/startup-options.ts +6 -0
- package/src/modes/components/agent-dashboard.ts +37 -17
- package/src/modes/components/model-selector.ts +93 -35
- package/src/modes/components/notifications-settings-editor.ts +1379 -0
- package/src/modes/components/settings-selector.ts +60 -11
- package/src/modes/components/welcome.ts +4 -4
- package/src/modes/controllers/command-controller.ts +1 -2
- package/src/modes/controllers/event-controller.ts +11 -16
- package/src/modes/controllers/extension-ui-controller.ts +348 -4
- package/src/modes/controllers/selector-controller.ts +674 -36
- package/src/modes/index.ts +0 -22
- package/src/modes/interactive-mode.ts +59 -20
- package/src/modes/runtime-init.ts +298 -6
- package/src/modes/shared/agent-wire/approval-gate.ts +9 -11
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +10 -11
- package/src/modes/shared/agent-wire/event-contract.ts +7 -8
- package/src/modes/shared/agent-wire/event-envelope.ts +9 -17
- package/src/modes/shared/agent-wire/event-observation.ts +16 -7
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +1254 -87
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +15 -15
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +113 -0
- package/src/modes/types.ts +5 -0
- package/src/runtime-mcp/client.ts +94 -19
- package/src/runtime-mcp/config.ts +27 -15
- package/src/runtime-mcp/manager.ts +343 -105
- package/src/runtime-mcp/oauth-flow.ts +42 -12
- package/src/runtime-mcp/transports/http.ts +49 -19
- package/src/runtime-mcp/transports/stdio.ts +47 -28
- package/src/runtime-mcp/types.ts +10 -0
- package/src/sdk/acp/adapter.ts +418 -0
- package/src/sdk/acp/index.ts +2 -0
- package/src/sdk/broker/broker.ts +642 -0
- package/src/sdk/broker/discovery.ts +94 -0
- package/src/sdk/broker/ensure.ts +249 -0
- package/src/sdk/broker/identity.ts +42 -0
- package/src/sdk/broker/index.ts +8 -0
- package/src/sdk/broker/internal-source-marker-2178.txt +1 -0
- package/src/sdk/broker/internal-source.bunfig.toml +1 -0
- package/src/sdk/broker/lifecycle-ledger.ts +286 -0
- package/src/sdk/broker/lifecycle.ts +2157 -0
- package/src/sdk/broker/process-incarnation.ts +159 -0
- package/src/sdk/broker/runtime.ts +164 -0
- package/src/sdk/broker/session-index.ts +318 -0
- package/src/sdk/broker/state-version.ts +23 -0
- package/src/sdk/broker/transport.ts +125 -0
- package/src/sdk/bus/chat-command-policy.ts +107 -0
- package/src/sdk/bus/chat-daemon-cli.ts +214 -0
- package/src/sdk/bus/chat-daemon-control.ts +713 -0
- package/src/sdk/bus/chat-daemon-runtime.ts +526 -0
- package/src/sdk/bus/chat-effect-journal.ts +353 -0
- package/src/{notifications → sdk/bus}/config-commands.ts +39 -10
- package/src/sdk/bus/config.ts +433 -0
- package/src/sdk/bus/conversation-store.ts +391 -0
- package/src/sdk/bus/discord-conversation.ts +85 -0
- package/src/sdk/bus/discord-daemon.ts +2275 -0
- package/src/sdk/bus/discord-live-provider.ts +521 -0
- package/src/sdk/bus/discord-provider.ts +56 -0
- package/src/{notifications → sdk/bus}/html-format.ts +2 -2
- package/src/sdk/bus/index.ts +3943 -0
- package/src/{notifications → sdk/bus}/lifecycle-commands.ts +2 -0
- package/src/{notifications → sdk/bus}/lifecycle-control-runtime.ts +225 -196
- package/src/{notifications → sdk/bus}/lifecycle-orchestrator.ts +161 -74
- package/src/{notifications → sdk/bus}/managed-daemon.ts +2 -2
- package/src/sdk/bus/native-runtime-compatibility.ts +42 -0
- package/src/sdk/bus/natives-augment.d.ts +38 -0
- package/src/sdk/bus/notification-orchestration.ts +535 -0
- package/src/{notifications → sdk/bus}/notification-service.ts +106 -12
- package/src/sdk/bus/session-control.ts +333 -0
- package/src/sdk/bus/slack-conversation.ts +74 -0
- package/src/sdk/bus/slack-daemon.ts +2144 -0
- package/src/sdk/bus/slack-live-provider.ts +380 -0
- package/src/sdk/bus/slack-provider.ts +76 -0
- package/src/{notifications → sdk/bus}/telegram-cli.ts +8 -8
- package/src/sdk/bus/telegram-daemon-cli.ts +330 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +14 -0
- package/src/{notifications → sdk/bus}/telegram-daemon-control.ts +4 -4
- package/src/{notifications → sdk/bus}/telegram-daemon.ts +502 -54
- package/src/{notifications → sdk/bus}/telegram-reference.ts +2 -2
- package/src/sdk/bus/telegram-setup.ts +663 -0
- package/src/{notifications → sdk/bus}/topic-registry.ts +32 -0
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/session-cli.ts +275 -0
- package/src/sdk/client/client.ts +1 -0
- package/src/sdk/client/discovery.ts +108 -0
- package/src/sdk/client/index.ts +2 -0
- package/src/sdk/host/control/dispatch.ts +367 -0
- package/src/sdk/host/control/index.ts +10 -0
- package/src/sdk/host/control/operations.ts +63 -0
- package/src/sdk/host/events.ts +91 -0
- package/src/sdk/host/host.ts +296 -0
- package/src/sdk/host/index.ts +4 -0
- package/src/sdk/host/query/cursor.ts +189 -0
- package/src/sdk/host/query/handlers.ts +533 -0
- package/src/sdk/host/query/index.ts +3 -0
- package/src/sdk/host/query/revision-store.ts +1126 -0
- package/src/sdk/host/reverse-leases.ts +325 -0
- package/src/sdk/host/types.ts +29 -0
- package/src/sdk/index.ts +13 -0
- package/src/sdk/lifecycle-session.ts +39 -0
- package/src/sdk/mcp/index.ts +1 -0
- package/src/sdk/mcp/server.ts +332 -0
- package/src/sdk/models.ts +168 -0
- package/src/sdk/protocol/adapter-validation.ts +48 -0
- package/src/sdk/protocol/operation-inventory.generated.json +5654 -0
- package/src/sdk/protocol/operation-registry.ts +312 -0
- package/src/{sdk.ts → sdk/session.ts} +339 -212
- package/src/sdk/startup-capability.ts +189 -0
- package/src/session/agent-session.ts +2453 -894
- package/src/session/default-model-selection.ts +93 -0
- package/src/session/fallback-chain-controller.ts +136 -0
- package/src/session/session-manager.ts +438 -11
- package/src/session/session-storage.ts +8 -1
- package/src/setup/credential-auto-import.ts +290 -35
- package/src/setup/hermes/templates/operator-instructions.v1.md +3 -5
- package/src/slash-commands/builtin-registry.ts +2 -1
- package/src/task/executor.ts +62 -13
- package/src/tools/ask-answer-registry.ts +1 -1
- package/src/tools/ask.ts +13 -8
- package/src/tools/browser/STEALTH-THIRD-PARTY-NOTICES.md +26 -0
- package/src/tools/browser/benchmark/detector-report.ts +157 -0
- package/src/tools/browser/benchmark/run-suite.ts +39 -0
- package/src/tools/browser/launch.ts +124 -19
- package/src/tools/browser/profile-discovery.ts +63 -0
- package/src/tools/browser/profile-posture.ts +59 -0
- package/src/tools/browser/profile-reuse.ts +102 -0
- package/src/tools/browser/profile-warmup.ts +109 -0
- package/src/tools/browser/registry.ts +87 -11
- package/src/tools/browser/tab-protocol.ts +1 -0
- package/src/tools/browser/tab-supervisor.ts +1 -0
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/browser.ts +6 -0
- package/src/tools/index.ts +8 -8
- package/src/tools/puppeteer/14_stealth_permissions.txt +57 -0
- package/src/tools/puppeteer/15_stealth_webrtc.txt +60 -0
- package/src/tools/telegram-send.ts +2 -2
- package/vendor/insane-search/MANIFEST.json +2 -1
- package/vendor/insane-search/engine/bias_check.py +4 -1
- package/vendor/insane-search/engine/phase0.py +26 -1
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +0 -69
- package/dist/types/modes/bridge/auth.d.ts +0 -12
- package/dist/types/modes/bridge/bridge-client-bridge.d.ts +0 -9
- package/dist/types/modes/bridge/bridge-mode.d.ts +0 -47
- package/dist/types/modes/bridge/bridge-ui-context.d.ts +0 -88
- package/dist/types/modes/bridge/event-stream.d.ts +0 -8
- package/dist/types/modes/rpc/host-tools.d.ts +0 -1
- package/dist/types/modes/rpc/host-uris.d.ts +0 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +0 -299
- package/dist/types/modes/rpc/rpc-mode.d.ts +0 -86
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +0 -10
- package/dist/types/modes/rpc/rpc-types.d.ts +0 -810
- package/dist/types/modes/shared/agent-wire/command-contract.d.ts +0 -18
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +0 -37
- package/dist/types/modes/shared/agent-wire/command-validation.d.ts +0 -2
- package/dist/types/modes/shared/agent-wire/handshake.d.ts +0 -56
- package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +0 -16
- package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +0 -17
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +0 -25
- package/dist/types/modes/shared/agent-wire/responses.d.ts +0 -4
- package/dist/types/modes/shared/agent-wire/scopes.d.ts +0 -18
- package/dist/types/modes/shared/agent-wire/session-registry.d.ts +0 -25
- package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +0 -42
- package/dist/types/modes/shared/agent-wire/ui-result.d.ts +0 -27
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +0 -29
- package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +0 -68
- package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +0 -166
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +0 -127
- package/dist/types/notifications/config.d.ts +0 -132
- package/src/harness-control-plane/rpc-adapter.ts +0 -290
- package/src/modes/bridge/auth.ts +0 -41
- package/src/modes/bridge/bridge-client-bridge.ts +0 -47
- package/src/modes/bridge/bridge-mode.ts +0 -675
- package/src/modes/bridge/bridge-ui-context.ts +0 -200
- package/src/modes/bridge/event-stream.ts +0 -70
- package/src/modes/rpc/host-tools.ts +0 -1
- package/src/modes/rpc/host-uris.ts +0 -1
- package/src/modes/rpc/rpc-client.ts +0 -1166
- package/src/modes/rpc/rpc-mode.ts +0 -1029
- package/src/modes/rpc/rpc-socket-security.ts +0 -115
- package/src/modes/rpc/rpc-types.ts +0 -637
- package/src/modes/shared/agent-wire/command-contract.ts +0 -18
- package/src/modes/shared/agent-wire/command-dispatch.ts +0 -498
- package/src/modes/shared/agent-wire/command-validation.ts +0 -181
- package/src/modes/shared/agent-wire/handshake.ts +0 -157
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +0 -197
- package/src/modes/shared/agent-wire/host-uri-bridge.ts +0 -236
- package/src/modes/shared/agent-wire/protocol.ts +0 -46
- package/src/modes/shared/agent-wire/responses.ts +0 -17
- package/src/modes/shared/agent-wire/scopes.ts +0 -94
- package/src/modes/shared/agent-wire/session-registry.ts +0 -109
- package/src/modes/shared/agent-wire/ui-request-broker.ts +0 -150
- package/src/modes/shared/agent-wire/ui-result.ts +0 -48
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +0 -365
- package/src/modes/shared/agent-wire/unattended-audit.ts +0 -176
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +0 -426
- package/src/modes/shared/agent-wire/unattended-session.ts +0 -528
- package/src/notifications/config.ts +0 -219
- package/src/notifications/index.ts +0 -1835
- package/src/notifications/telegram-daemon-cli.ts +0 -243
- package/src/tools/puppeteer/09_stealth_locale.txt +0 -46
- /package/dist/types/{notifications → sdk/bus}/attachment-registry.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/chat-adapters.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/daemon-paths.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/engine.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/helpers.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/lifecycle-commands.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/operator-runtime.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/rate-limit-pool.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/recent-activity.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/reply-sent-store.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/rich-draft.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/rich-render.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/threaded-inbound.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/threaded-render.d.ts +0 -0
- /package/src/{notifications → sdk/bus}/attachment-registry.ts +0 -0
- /package/src/{notifications → sdk/bus}/chat-adapters.ts +0 -0
- /package/src/{notifications → sdk/bus}/daemon-paths.ts +0 -0
- /package/src/{notifications → sdk/bus}/engine.ts +0 -0
- /package/src/{notifications → sdk/bus}/helpers.ts +0 -0
- /package/src/{notifications → sdk/bus}/operator-runtime.ts +0 -0
- /package/src/{notifications → sdk/bus}/rate-limit-pool.ts +0 -0
- /package/src/{notifications → sdk/bus}/recent-activity.ts +0 -0
- /package/src/{notifications → sdk/bus}/reply-sent-store.ts +0 -0
- /package/src/{notifications → sdk/bus}/rich-draft.ts +0 -0
- /package/src/{notifications → sdk/bus}/rich-render.ts +0 -0
- /package/src/{notifications → sdk/bus}/threaded-inbound.ts +0 -0
- /package/src/{notifications → sdk/bus}/threaded-render.ts +0 -0
|
@@ -4,14 +4,21 @@ import * as fs from "node:fs";
|
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { logger } from "@gajae-code/utils";
|
|
7
|
-
import { withFileLock } from "
|
|
8
|
-
import type { Settings } from "
|
|
9
|
-
import type { DaemonRuntimeInfo } from "
|
|
10
|
-
import { resolveGjcRuntimeSpawnInfo } from "
|
|
7
|
+
import { withFileLock } from "../../config/file-lock";
|
|
8
|
+
import type { Settings } from "../../config/settings";
|
|
9
|
+
import type { DaemonRuntimeInfo } from "../../daemon/control-types";
|
|
10
|
+
import { resolveGjcRuntimeSpawnInfo } from "../../daemon/runtime";
|
|
11
11
|
import { getNotificationConfig, isTelegramConfigured, tokenFingerprint } from "./config";
|
|
12
12
|
import { parseInThreadConfigCommand, parseRichToggleCommand, parseTelegramControlCommand } from "./config-commands";
|
|
13
13
|
import { daemonPaths, HEARTBEAT_TTL_MS } from "./daemon-paths";
|
|
14
|
+
import { sanitizeDiagnostic } from "./notification-service";
|
|
15
|
+
import { DAEMON_GENERATION, NOTIFICATION_PROTOCOL_VERSION } from "./telegram-daemon-contract";
|
|
16
|
+
import { withTelegramSetupLease } from "./telegram-setup";
|
|
17
|
+
|
|
18
|
+
export { DAEMON_GENERATION, NOTIFICATION_PROTOCOL_VERSION } from "./telegram-daemon-contract";
|
|
19
|
+
|
|
14
20
|
import {
|
|
21
|
+
buildButtonGrid,
|
|
15
22
|
buildCompactChoiceGrid,
|
|
16
23
|
code,
|
|
17
24
|
splitTelegramHtml,
|
|
@@ -41,6 +48,7 @@ import {
|
|
|
41
48
|
type LifecycleControlServer,
|
|
42
49
|
type LifecycleControlServerFactory,
|
|
43
50
|
} from "./lifecycle-control-runtime";
|
|
51
|
+
import type { OrchestratorDeps } from "./lifecycle-orchestrator";
|
|
44
52
|
import { NotificationOperatorRuntime, OperatorBackoffPolicy, OperatorEventRouter } from "./operator-runtime";
|
|
45
53
|
import { RateLimitPool } from "./rate-limit-pool";
|
|
46
54
|
import { listRecentSessions } from "./recent-activity";
|
|
@@ -61,6 +69,8 @@ import { renderThreadedFrame, type ThreadedSend } from "./threaded-render";
|
|
|
61
69
|
import { TopicRegistry, type TopicRegistryState } from "./topic-registry";
|
|
62
70
|
|
|
63
71
|
export type EnsureDaemonResult = "owner_spawned" | "attached" | "disabled" | "blocked";
|
|
72
|
+
/** Detailed result for orchestration that must distinguish a #2028 handoff from a fresh spawn. */
|
|
73
|
+
export type EnsureTelegramDaemonDetailedResult = "spawned" | "reloaded" | "attached" | "disabled" | "blocked_identity";
|
|
64
74
|
|
|
65
75
|
export interface DaemonState {
|
|
66
76
|
pid: number;
|
|
@@ -123,21 +133,9 @@ export { HEARTBEAT_TTL_MS };
|
|
|
123
133
|
export const DAEMON_VERSION = 1;
|
|
124
134
|
/** Capability token advertised when the server supports app-level ping/pong. */
|
|
125
135
|
export const CLIENT_PING_PONG_CAPABILITY = "client_ping_pong";
|
|
126
|
-
/** Protocol version the daemon advertises in its ClientHello. */
|
|
127
|
-
export const NOTIFICATION_PROTOCOL_VERSION = 3;
|
|
128
136
|
/** Capability required for typed controls and semantic Selected acknowledgement frames. */
|
|
129
137
|
export const ASK_SELECTED_ACK_CAPABILITY = "ask_selected_ack_v1";
|
|
130
138
|
export const ASK_CONTROLS_CAPABILITY = "ask_controls_v1";
|
|
131
|
-
/**
|
|
132
|
-
* Operational generation the current daemon build speaks, persisted into
|
|
133
|
-
* {@link DaemonState.generation} on ownership acquisition. It is tied to the
|
|
134
|
-
* wire {@link NOTIFICATION_PROTOCOL_VERSION} so any protocol bump (which is what
|
|
135
|
-
* gates capabilities like {@link ASK_SELECTED_ACK_CAPABILITY}) also bumps the
|
|
136
|
-
* generation, letting a freshly-upgraded host recognise an older, still-live
|
|
137
|
-
* daemon and reload it instead of silently attaching to it. Distinct from the
|
|
138
|
-
* persisted schema {@link DAEMON_VERSION}, which did not change across #1999.
|
|
139
|
-
*/
|
|
140
|
-
export const DAEMON_GENERATION = NOTIFICATION_PROTOCOL_VERSION;
|
|
141
139
|
|
|
142
140
|
const nodeFs: TelegramDaemonFs = fs.promises as unknown as TelegramDaemonFs;
|
|
143
141
|
|
|
@@ -177,6 +175,36 @@ const PENDING_TOPIC_FRAME_LIMIT = 20;
|
|
|
177
175
|
const SEEN_UPDATE_ID_LIMIT = 1_000;
|
|
178
176
|
const ORPHAN_TOPIC_GRACE_MS = 60_000;
|
|
179
177
|
const CONSUMED_REACTION = "✅";
|
|
178
|
+
const MODEL_CALLBACK_PREFIX = "m:";
|
|
179
|
+
const MODEL_CHOICE_TTL_MS = 10 * 60 * 1_000;
|
|
180
|
+
|
|
181
|
+
const MODEL_BUTTON_LABEL_MAX_BYTES = 48;
|
|
182
|
+
const SENSITIVE_MODEL_LABEL =
|
|
183
|
+
/(?:\b(?:https?|wss?):\/\/|\b[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}\b|\b(?:api[-_ ]?key|access[-_ ]?token|bearer|secret|password|account(?:\s*id)?|email|exception|stack trace)\b|\b(?:sk|pk|rk)-[A-Za-z0-9_-]{12,}\b)/i;
|
|
184
|
+
|
|
185
|
+
/** Inline-keyboard labels are plain text, bounded, and stripped of visual control characters. */
|
|
186
|
+
function safeModelButtonLabel(value: unknown): string | undefined {
|
|
187
|
+
if (typeof value !== "string") return undefined;
|
|
188
|
+
const normalized = value
|
|
189
|
+
.replace(/[\u0000-\u001F\u007F-\u009F\u202A-\u202E\u2066-\u2069]/g, " ")
|
|
190
|
+
.replace(/\s+/g, " ")
|
|
191
|
+
.trim();
|
|
192
|
+
if (!normalized) return undefined;
|
|
193
|
+
if (SENSITIVE_MODEL_LABEL.test(normalized)) return undefined;
|
|
194
|
+
if (Buffer.byteLength(normalized, "utf8") <= MODEL_BUTTON_LABEL_MAX_BYTES) return normalized;
|
|
195
|
+
|
|
196
|
+
const marker = "…";
|
|
197
|
+
const markerBytes = Buffer.byteLength(marker, "utf8");
|
|
198
|
+
let label = "";
|
|
199
|
+
let labelBytes = 0;
|
|
200
|
+
for (const char of normalized) {
|
|
201
|
+
const charBytes = Buffer.byteLength(char, "utf8");
|
|
202
|
+
if (labelBytes + charBytes + markerBytes > MODEL_BUTTON_LABEL_MAX_BYTES) break;
|
|
203
|
+
label += char;
|
|
204
|
+
labelBytes += charBytes;
|
|
205
|
+
}
|
|
206
|
+
return `${label}${marker}`;
|
|
207
|
+
}
|
|
180
208
|
|
|
181
209
|
function splitTelegramPlainText(text: string, max = TELEGRAM_MESSAGE_LIMIT): string[] {
|
|
182
210
|
if (text.length <= max) return [text];
|
|
@@ -254,6 +282,9 @@ async function fetchWithRetry(
|
|
|
254
282
|
}
|
|
255
283
|
|
|
256
284
|
export { type DaemonPaths, daemonPaths } from "./daemon-paths";
|
|
285
|
+
export function deriveLifecycleAuditRedactionKey(botToken: string): Uint8Array {
|
|
286
|
+
return crypto.createHmac("sha256", botToken).update("gjc.lifecycle.audit.v2.key", "utf8").digest();
|
|
287
|
+
}
|
|
257
288
|
|
|
258
289
|
/**
|
|
259
290
|
* Attach session-lifecycle control (create/close/resume) to the running daemon.
|
|
@@ -265,19 +296,23 @@ export { type DaemonPaths, daemonPaths } from "./daemon-paths";
|
|
|
265
296
|
* (NotificationControlServer) is owned/started by the daemon process; this
|
|
266
297
|
* function only connects it to policy. Returns the orchestrator deps for tests.
|
|
267
298
|
*/
|
|
299
|
+
|
|
268
300
|
export function startDaemonLifecycleControl(input: {
|
|
269
301
|
controlServer: ControlServerLike;
|
|
270
302
|
pairedChatId: string;
|
|
271
303
|
agentDir: string;
|
|
304
|
+
auditRedactionKey: Uint8Array;
|
|
272
305
|
env?: NodeJS.ProcessEnv;
|
|
273
|
-
}):
|
|
306
|
+
}): OrchestratorDeps {
|
|
274
307
|
const deps = buildOrchestratorDeps({
|
|
275
308
|
pairedChatId: input.pairedChatId,
|
|
276
309
|
agentNotificationsDir: daemonPaths(input.agentDir).dir,
|
|
277
310
|
sessionsRoot: path.join(input.agentDir, "sessions"),
|
|
311
|
+
auditRedactionKey: input.auditRedactionKey,
|
|
278
312
|
env: input.env,
|
|
279
313
|
});
|
|
280
314
|
attachLifecycleControl(input.controlServer, deps);
|
|
315
|
+
return deps;
|
|
281
316
|
}
|
|
282
317
|
|
|
283
318
|
async function ensureDir(fsImpl: TelegramDaemonFs, dir: string): Promise<void> {
|
|
@@ -340,6 +375,44 @@ export async function registerNotificationRoot(input: {
|
|
|
340
375
|
return root;
|
|
341
376
|
}
|
|
342
377
|
|
|
378
|
+
export interface UnregisterNotificationRootResult {
|
|
379
|
+
root: string;
|
|
380
|
+
remainingRoots: number;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Remove one session's Telegram scan root without disturbing other live session roots. */
|
|
384
|
+
export async function unregisterNotificationRoot(input: {
|
|
385
|
+
settings: Settings;
|
|
386
|
+
cwd: string;
|
|
387
|
+
sessionId: string;
|
|
388
|
+
fs?: TelegramDaemonFs;
|
|
389
|
+
}): Promise<UnregisterNotificationRootResult> {
|
|
390
|
+
const fsImpl = input.fs ?? nodeFs;
|
|
391
|
+
const paths = daemonPaths(input.settings.getAgentDir());
|
|
392
|
+
const root = notificationRootForCwd(input.cwd);
|
|
393
|
+
await ensureDir(fsImpl, paths.dir);
|
|
394
|
+
let remainingRoots = 0;
|
|
395
|
+
await withFileLock(
|
|
396
|
+
paths.roots,
|
|
397
|
+
async () => {
|
|
398
|
+
const current = await readJson<{ roots?: string[]; sessions?: Record<string, string> }>(fsImpl, paths.roots);
|
|
399
|
+
if (!current) return;
|
|
400
|
+
const sessions = { ...(current.sessions ?? {}) };
|
|
401
|
+
delete sessions[input.sessionId];
|
|
402
|
+
const rootStillReferenced = Object.values(sessions).includes(root);
|
|
403
|
+
const roots = (current.roots ?? []).filter(candidate => candidate !== root || rootStillReferenced);
|
|
404
|
+
remainingRoots = roots.length;
|
|
405
|
+
await writeJsonAtomic(fsImpl, paths.roots, {
|
|
406
|
+
version: 1,
|
|
407
|
+
roots: Array.from(new Set(roots)).sort(),
|
|
408
|
+
sessions,
|
|
409
|
+
});
|
|
410
|
+
},
|
|
411
|
+
{ staleMs: 10_000 },
|
|
412
|
+
);
|
|
413
|
+
return { root, remainingRoots };
|
|
414
|
+
}
|
|
415
|
+
|
|
343
416
|
function notificationRootForCwd(cwd: string): string {
|
|
344
417
|
return path.join(cwd, ".gjc", "state");
|
|
345
418
|
}
|
|
@@ -541,14 +614,17 @@ export async function releaseDaemonOwnership(input: {
|
|
|
541
614
|
|
|
542
615
|
/** Read the persisted daemon ownership state (or undefined when absent). */
|
|
543
616
|
export async function readDaemonState(
|
|
544
|
-
settings: Settings,
|
|
617
|
+
settings: Pick<Settings, "getAgentDir">,
|
|
545
618
|
fs: TelegramDaemonFs = nodeFs,
|
|
546
619
|
): Promise<DaemonState | undefined> {
|
|
547
620
|
return readJson<DaemonState>(fs, daemonPaths(settings.getAgentDir()).state);
|
|
548
621
|
}
|
|
549
622
|
|
|
550
623
|
/** Read the persisted notification roots list. */
|
|
551
|
-
export async function readDaemonRoots(
|
|
624
|
+
export async function readDaemonRoots(
|
|
625
|
+
settings: Pick<Settings, "getAgentDir">,
|
|
626
|
+
fs: TelegramDaemonFs = nodeFs,
|
|
627
|
+
): Promise<string[]> {
|
|
552
628
|
const roots = await readJson<{ roots?: string[] }>(fs, daemonPaths(settings.getAgentDir()).roots);
|
|
553
629
|
return roots?.roots ?? [];
|
|
554
630
|
}
|
|
@@ -688,35 +764,57 @@ export async function spawnTelegramDaemonOwner(
|
|
|
688
764
|
return { result: "owner_spawned", ownerId: ownership.ownerId, runtime, warnings: [] };
|
|
689
765
|
}
|
|
690
766
|
|
|
691
|
-
|
|
767
|
+
/**
|
|
768
|
+
* Ensure a configured daemon owns this session root, preserving ownership safety
|
|
769
|
+
* while exposing whether a #2028 generation handoff was required.
|
|
770
|
+
*/
|
|
771
|
+
export async function ensureTelegramDaemonRunningDetailed(
|
|
692
772
|
input: { settings: Settings; cwd: string; sessionId: string },
|
|
693
773
|
deps: TelegramDaemonDeps = {},
|
|
694
|
-
): Promise<
|
|
774
|
+
): Promise<EnsureTelegramDaemonDetailedResult> {
|
|
695
775
|
const cfg = getNotificationConfig(input.settings);
|
|
696
776
|
if (!isTelegramConfigured(cfg)) return "disabled";
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
// up a fresh current-generation daemon.
|
|
777
|
+
return await withTelegramSetupLease(cfg.botToken, async () => {
|
|
778
|
+
const root = notificationRootForCwd(input.cwd);
|
|
779
|
+
const fp = tokenFingerprint(cfg.botToken);
|
|
780
|
+
const spawned = await spawnTelegramDaemonOwner(
|
|
781
|
+
{ settings: input.settings, roots: [root], tokenFingerprint: fp, chatId: cfg.chatId },
|
|
782
|
+
deps,
|
|
783
|
+
);
|
|
784
|
+
if (spawned.result === "blocked") {
|
|
785
|
+
logger.warn(`notifications: failed to ensure Telegram daemon: ${spawned.warnings.join("; ")}`);
|
|
786
|
+
return "blocked_identity";
|
|
787
|
+
}
|
|
788
|
+
if (spawned.reloadRequired) {
|
|
789
|
+
await registerNotificationRoot({ ...input, fs: deps.fs });
|
|
790
|
+
await reloadStaleGenerationOwner(input.settings, deps);
|
|
791
|
+
return "reloaded";
|
|
792
|
+
}
|
|
714
793
|
await registerNotificationRoot({ ...input, fs: deps.fs });
|
|
715
|
-
|
|
716
|
-
|
|
794
|
+
return spawned.result === "owner_spawned" ? "spawned" : "attached";
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Legacy compatibility mapping for callers that only distinguish ownership from
|
|
800
|
+
* attachment. New orchestration should use {@link ensureTelegramDaemonRunningDetailed}.
|
|
801
|
+
*/
|
|
802
|
+
export async function ensureTelegramDaemonRunning(
|
|
803
|
+
input: { settings: Settings; cwd: string; sessionId: string },
|
|
804
|
+
deps: TelegramDaemonDeps = {},
|
|
805
|
+
): Promise<EnsureDaemonResult> {
|
|
806
|
+
const result = await ensureTelegramDaemonRunningDetailed(input, deps);
|
|
807
|
+
switch (result) {
|
|
808
|
+
case "spawned":
|
|
809
|
+
case "reloaded":
|
|
810
|
+
return "owner_spawned";
|
|
811
|
+
case "attached":
|
|
812
|
+
return "attached";
|
|
813
|
+
case "disabled":
|
|
814
|
+
return "disabled";
|
|
815
|
+
case "blocked_identity":
|
|
816
|
+
return "blocked";
|
|
717
817
|
}
|
|
718
|
-
await registerNotificationRoot({ ...input, fs: deps.fs });
|
|
719
|
-
return spawned.result;
|
|
720
818
|
}
|
|
721
819
|
|
|
722
820
|
/**
|
|
@@ -876,7 +974,7 @@ export class TelegramUpdatePoller {
|
|
|
876
974
|
// A cooperative stop aborts the in-flight long poll; treat as a clean wake.
|
|
877
975
|
if (isAbortError(err)) return 0;
|
|
878
976
|
// A transient Telegram API failure must never crash the daemon.
|
|
879
|
-
logger.error("notifications daemon: getUpdates failed", { error: String(err) });
|
|
977
|
+
logger.error("notifications daemon: getUpdates failed", { error: sanitizeDiagnostic(String(err)) });
|
|
880
978
|
await this.#opts.runtime.sleep(POLL_BACKOFF_MS, signal);
|
|
881
979
|
return 0;
|
|
882
980
|
}
|
|
@@ -955,6 +1053,16 @@ export interface TelegramDaemonOptions {
|
|
|
955
1053
|
* default applies (e.g. lifecycle control disabled), no control server starts.
|
|
956
1054
|
*/
|
|
957
1055
|
createLifecycleControlServer?: LifecycleControlServerFactory | null;
|
|
1056
|
+
/**
|
|
1057
|
+
* Test seam for observing the exact production lifecycle dependency input.
|
|
1058
|
+
* Production defaults to {@link buildOrchestratorDeps}.
|
|
1059
|
+
*/
|
|
1060
|
+
createLifecycleOrchestratorDeps?: (input: {
|
|
1061
|
+
pairedChatId: string;
|
|
1062
|
+
agentNotificationsDir: string;
|
|
1063
|
+
sessionsRoot: string;
|
|
1064
|
+
auditRedactionKey: Uint8Array;
|
|
1065
|
+
}) => OrchestratorDeps;
|
|
958
1066
|
/** Rich text promotion (enabled by default; see rich-render.ts). */
|
|
959
1067
|
rich?: { enabled: boolean };
|
|
960
1068
|
/** Opt-in rich-draft streaming of live turn previews (off by default; see rich-draft.ts). */
|
|
@@ -968,7 +1076,10 @@ export interface TelegramDaemonOptions {
|
|
|
968
1076
|
}
|
|
969
1077
|
|
|
970
1078
|
interface SessionSocket {
|
|
1079
|
+
/** Immutable key of the transport endpoint that owns this socket. */
|
|
971
1080
|
sessionId: string;
|
|
1081
|
+
/** Current logical session carried by valid threaded frames on this transport. */
|
|
1082
|
+
logicalSessionId: string;
|
|
972
1083
|
token: string;
|
|
973
1084
|
endpointKey: string;
|
|
974
1085
|
ws: WebSocket;
|
|
@@ -981,6 +1092,25 @@ interface SessionSocket {
|
|
|
981
1092
|
awaitingNonce: string | undefined;
|
|
982
1093
|
/** Per-session liveness interval handle (only set for capable sessions). */
|
|
983
1094
|
pingTimer: ReturnType<typeof setInterval> | undefined;
|
|
1095
|
+
/** Correlation id for the startup replay barrier. */
|
|
1096
|
+
replayId: string;
|
|
1097
|
+
/** Queues live frames until startup replay is applied. */
|
|
1098
|
+
replayPending: boolean;
|
|
1099
|
+
replayQueue: Record<string, unknown>[];
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
interface ModelChoiceRoute {
|
|
1103
|
+
/** Exact transport socket that rendered this choice. */
|
|
1104
|
+
session: SessionSocket;
|
|
1105
|
+
/** Logical session current when this choice was rendered. */
|
|
1106
|
+
sessionId: string;
|
|
1107
|
+
selector: string;
|
|
1108
|
+
expiresAt: number;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
interface RenderedModelChoice {
|
|
1112
|
+
selector: string;
|
|
1113
|
+
label: string;
|
|
984
1114
|
}
|
|
985
1115
|
|
|
986
1116
|
interface PendingThreadedFrame {
|
|
@@ -1017,6 +1147,9 @@ export class TelegramNotificationDaemon {
|
|
|
1017
1147
|
/** Telegram message id backing each streamed `${sessionId}:${coalesceKey}`, for in-place edits. */
|
|
1018
1148
|
private readonly liveMessages = new Map<string, number>();
|
|
1019
1149
|
readonly sessions = new Map<string, SessionSocket>();
|
|
1150
|
+
/** Ephemeral aliases for model choices; deliberately never serialized across daemon restarts. */
|
|
1151
|
+
#modelChoiceAliases = new Map<string, ModelChoiceRoute>();
|
|
1152
|
+
|
|
1020
1153
|
private readonly runtime: NotificationOperatorRuntime;
|
|
1021
1154
|
private readonly sessionRouter: OperatorEventRouter<SessionSocket>;
|
|
1022
1155
|
private readonly pollConflictBackoff = new OperatorBackoffPolicy({ initialMs: 500, maxMs: 5_000 });
|
|
@@ -1140,10 +1273,11 @@ export class TelegramNotificationDaemon {
|
|
|
1140
1273
|
const token = crypto.randomBytes(32).toString("base64url");
|
|
1141
1274
|
const agentDir = this.opts.settings.getAgentDir();
|
|
1142
1275
|
server = factory({ token, ownerId: this.opts.ownerId, agentDir });
|
|
1143
|
-
const deps = buildOrchestratorDeps({
|
|
1276
|
+
const deps = (this.opts.createLifecycleOrchestratorDeps ?? buildOrchestratorDeps)({
|
|
1144
1277
|
pairedChatId: this.opts.chatId,
|
|
1145
1278
|
agentNotificationsDir: daemonPaths(agentDir).dir,
|
|
1146
1279
|
sessionsRoot: path.join(agentDir, "sessions"),
|
|
1280
|
+
auditRedactionKey: deriveLifecycleAuditRedactionKey(this.opts.botToken),
|
|
1147
1281
|
});
|
|
1148
1282
|
// Register the lifecycle-request handler BEFORE start(): the native
|
|
1149
1283
|
// control server captures the callback at start time, so wiring must
|
|
@@ -1697,6 +1831,26 @@ export class TelegramNotificationDaemon {
|
|
|
1697
1831
|
logger.warn(`notifications: failed to persist Telegram update id ${updateId}: ${String(err)}`);
|
|
1698
1832
|
}
|
|
1699
1833
|
}
|
|
1834
|
+
private async rememberSeenUpdateIdForUnavailableNotice(updateId: number): Promise<boolean> {
|
|
1835
|
+
if (!Number.isSafeInteger(updateId) || updateId < 0) return false;
|
|
1836
|
+
const candidate = new Set(this.dispatchState.seenUpdateIds);
|
|
1837
|
+
candidate.add(updateId);
|
|
1838
|
+
while (candidate.size > SEEN_UPDATE_ID_LIMIT) candidate.delete(candidate.values().next().value!);
|
|
1839
|
+
try {
|
|
1840
|
+
const paths = daemonPaths(this.opts.settings.getAgentDir());
|
|
1841
|
+
await ensureDir(this.fsImpl, paths.dir);
|
|
1842
|
+
await writeJsonAtomic(this.fsImpl, paths.seenUpdates, {
|
|
1843
|
+
version: 1,
|
|
1844
|
+
updateIds: [...candidate],
|
|
1845
|
+
});
|
|
1846
|
+
} catch {
|
|
1847
|
+
logger.warn("notifications: unavailable-control notice state publication failed");
|
|
1848
|
+
return false;
|
|
1849
|
+
}
|
|
1850
|
+
this.dispatchState.seenUpdateIds.clear();
|
|
1851
|
+
for (const seenId of candidate) this.dispatchState.seenUpdateIds.add(seenId);
|
|
1852
|
+
return true;
|
|
1853
|
+
}
|
|
1700
1854
|
|
|
1701
1855
|
async scanRoots(): Promise<void> {
|
|
1702
1856
|
const paths = daemonPaths(this.opts.settings.getAgentDir());
|
|
@@ -1704,7 +1858,7 @@ export class TelegramNotificationDaemon {
|
|
|
1704
1858
|
const endpointSessionIds = new Set<string>();
|
|
1705
1859
|
let allRootsReadable = true;
|
|
1706
1860
|
for (const root of rootState?.roots ?? []) {
|
|
1707
|
-
const dir = path.join(root, "
|
|
1861
|
+
const dir = path.join(root, "sdk");
|
|
1708
1862
|
let files: string[];
|
|
1709
1863
|
try {
|
|
1710
1864
|
files = await this.fsImpl.readdir(dir);
|
|
@@ -1748,8 +1902,14 @@ export class TelegramNotificationDaemon {
|
|
|
1748
1902
|
const ws = new WS(`${url}/?token=${encodeURIComponent(token)}`);
|
|
1749
1903
|
const endpointKey = endpointGenerationKey(url, token);
|
|
1750
1904
|
this.closedEndpointKeys.delete(sessionId);
|
|
1905
|
+
const existing = this.sessions.get(sessionId);
|
|
1906
|
+
if (existing) this.#clearModelChoiceAliasesForSocket(existing);
|
|
1907
|
+
this.#clearModelChoiceAliases(sessionId);
|
|
1908
|
+
|
|
1751
1909
|
const session: SessionSocket = {
|
|
1752
1910
|
sessionId,
|
|
1911
|
+
logicalSessionId: sessionId,
|
|
1912
|
+
|
|
1753
1913
|
token,
|
|
1754
1914
|
endpointKey,
|
|
1755
1915
|
ws,
|
|
@@ -1758,12 +1918,18 @@ export class TelegramNotificationDaemon {
|
|
|
1758
1918
|
lastPongAt: 0,
|
|
1759
1919
|
awaitingNonce: undefined,
|
|
1760
1920
|
pingTimer: undefined,
|
|
1921
|
+
replayId: `telegram-startup-replay:${sessionId}`,
|
|
1922
|
+
replayPending: false,
|
|
1923
|
+
replayQueue: [],
|
|
1761
1924
|
};
|
|
1762
1925
|
this.sessions.set(sessionId, session);
|
|
1763
1926
|
// Bidirectional capability advertisement: announce client_ping_pong once the
|
|
1764
1927
|
// socket is open. Sent on "open" only — a real WHATWG WebSocket cannot send
|
|
1765
1928
|
// while CONNECTING — and liveness starts only after a capable ServerHello.
|
|
1766
1929
|
ws.addEventListener("open", () => {
|
|
1930
|
+
session.replayPending = true;
|
|
1931
|
+
session.replayQueue = [];
|
|
1932
|
+
const replayCursor = this.topics.replayCursor(sessionId);
|
|
1767
1933
|
if (session.ws.readyState === WebSocket.OPEN) {
|
|
1768
1934
|
try {
|
|
1769
1935
|
session.ws.send(
|
|
@@ -1774,6 +1940,16 @@ export class TelegramNotificationDaemon {
|
|
|
1774
1940
|
}),
|
|
1775
1941
|
);
|
|
1776
1942
|
} catch {}
|
|
1943
|
+
try {
|
|
1944
|
+
session.ws.send(
|
|
1945
|
+
JSON.stringify({
|
|
1946
|
+
type: "event_replay",
|
|
1947
|
+
id: session.replayId,
|
|
1948
|
+
sinceGeneration: replayCursor?.generation ?? 1,
|
|
1949
|
+
sinceSeq: replayCursor?.seq ?? 0,
|
|
1950
|
+
}),
|
|
1951
|
+
);
|
|
1952
|
+
} catch {}
|
|
1777
1953
|
}
|
|
1778
1954
|
// Eagerly create the session's Telegram topic as soon as it connects, so
|
|
1779
1955
|
// a thread exists the moment a notifications-enabled session is live —
|
|
@@ -1843,8 +2019,10 @@ export class TelegramNotificationDaemon {
|
|
|
1843
2019
|
this.deleteMessageRoutes(session.sessionId);
|
|
1844
2020
|
}
|
|
1845
2021
|
if (isCurrentSession) {
|
|
2022
|
+
this.#clearModelChoiceAliasesForSocket(session);
|
|
1846
2023
|
this.sessions.delete(session.sessionId);
|
|
1847
2024
|
}
|
|
2025
|
+
|
|
1848
2026
|
for (const item of new Set(this.selectedAckPending.values())) {
|
|
1849
2027
|
if (item.session !== session) continue;
|
|
1850
2028
|
if (item.state === "queued") this.pool.removeById(item.itemId);
|
|
@@ -1866,6 +2044,39 @@ export class TelegramNotificationDaemon {
|
|
|
1866
2044
|
}
|
|
1867
2045
|
}
|
|
1868
2046
|
|
|
2047
|
+
#logicalSessionId(session: SessionSocket): string {
|
|
2048
|
+
return session.logicalSessionId ?? session.sessionId;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
#clearModelChoiceAliases(sessionId: string): void {
|
|
2052
|
+
for (const [alias, route] of this.#modelChoiceAliases) {
|
|
2053
|
+
if (route.sessionId === sessionId) this.#modelChoiceAliases.delete(alias);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
#clearModelChoiceAliasesForSocket(session: SessionSocket): void {
|
|
2058
|
+
for (const [alias, route] of this.#modelChoiceAliases) {
|
|
2059
|
+
if (route.session === session) this.#modelChoiceAliases.delete(alias);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
#sweepExpiredModelChoiceAliases(): void {
|
|
2064
|
+
const now = this.runtime.now();
|
|
2065
|
+
for (const [alias, route] of this.#modelChoiceAliases) {
|
|
2066
|
+
if (route.expiresAt <= now) this.#modelChoiceAliases.delete(alias);
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
#putModelChoiceAlias(route: Omit<ModelChoiceRoute, "expiresAt">): string {
|
|
2071
|
+
this.#sweepExpiredModelChoiceAliases();
|
|
2072
|
+
let alias: string;
|
|
2073
|
+
do {
|
|
2074
|
+
alias = `${MODEL_CALLBACK_PREFIX}${crypto.randomBytes(16).toString("base64url")}`;
|
|
2075
|
+
} while (this.#modelChoiceAliases.has(alias));
|
|
2076
|
+
this.#modelChoiceAliases.set(alias, { ...route, expiresAt: this.runtime.now() + MODEL_CHOICE_TTL_MS });
|
|
2077
|
+
return alias;
|
|
2078
|
+
}
|
|
2079
|
+
|
|
1869
2080
|
private static readonly THREADED_FRAMES = new Set([
|
|
1870
2081
|
"identity_header",
|
|
1871
2082
|
"context_update",
|
|
@@ -1876,6 +2087,22 @@ export class TelegramNotificationDaemon {
|
|
|
1876
2087
|
"control_command_result",
|
|
1877
2088
|
]);
|
|
1878
2089
|
|
|
2090
|
+
/** Rekey model controls before a valid threaded frame can render or a silent config update can route output. */
|
|
2091
|
+
#updateLogicalSessionForThreadedFrame(session: SessionSocket, msg: Record<string, unknown>): void {
|
|
2092
|
+
if (
|
|
2093
|
+
typeof msg.type !== "string" ||
|
|
2094
|
+
!TelegramNotificationDaemon.THREADED_FRAMES.has(msg.type) ||
|
|
2095
|
+
typeof msg.sessionId !== "string" ||
|
|
2096
|
+
!msg.sessionId.trim() ||
|
|
2097
|
+
msg.sessionId === this.#logicalSessionId(session) ||
|
|
2098
|
+
(msg.type !== "config_update" && !renderThreadedFrame(msg))
|
|
2099
|
+
)
|
|
2100
|
+
return;
|
|
2101
|
+
this.#clearModelChoiceAliasesForSocket(session);
|
|
2102
|
+
this.#clearModelChoiceAliases(msg.sessionId);
|
|
2103
|
+
session.logicalSessionId = msg.sessionId;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
1879
2106
|
private topicNameFor(sessionId: string, msg: { title?: unknown; repo?: unknown; branch?: unknown }): string {
|
|
1880
2107
|
const repo = typeof msg?.repo === "string" && msg.repo ? msg.repo : undefined;
|
|
1881
2108
|
const branch = typeof msg?.branch === "string" && msg.branch ? msg.branch : undefined;
|
|
@@ -2134,7 +2361,7 @@ export class TelegramNotificationDaemon {
|
|
|
2134
2361
|
if (!res.ok) return undefined;
|
|
2135
2362
|
return Buffer.from(await res.arrayBuffer());
|
|
2136
2363
|
} catch (e) {
|
|
2137
|
-
logger.warn(`notifications: file download failed: ${String(e)}`);
|
|
2364
|
+
logger.warn(`notifications: file download failed: ${sanitizeDiagnostic(String(e))}`);
|
|
2138
2365
|
return undefined;
|
|
2139
2366
|
}
|
|
2140
2367
|
}
|
|
@@ -2664,8 +2891,129 @@ export class TelegramNotificationDaemon {
|
|
|
2664
2891
|
this.runtime.stopInterval("telegram-typing");
|
|
2665
2892
|
}
|
|
2666
2893
|
|
|
2894
|
+
/** Render successful `/model` lists as session-bound, one-shot inline choices. */
|
|
2895
|
+
async #renderModelChoices(session: SessionSocket, msg: Record<string, unknown>): Promise<boolean> {
|
|
2896
|
+
const logicalSessionId = this.#logicalSessionId(session);
|
|
2897
|
+
if (
|
|
2898
|
+
msg.type !== "control_command_result" ||
|
|
2899
|
+
msg.status !== "ok" ||
|
|
2900
|
+
msg.sessionId !== logicalSessionId ||
|
|
2901
|
+
!Array.isArray(msg.modelChoices) ||
|
|
2902
|
+
this.sessions.get(session.sessionId) !== session
|
|
2903
|
+
)
|
|
2904
|
+
return false;
|
|
2905
|
+
|
|
2906
|
+
const choices: RenderedModelChoice[] = [];
|
|
2907
|
+
for (const choice of msg.modelChoices) {
|
|
2908
|
+
if (!choice || typeof choice !== "object") continue;
|
|
2909
|
+
const { selector, label } = choice as { selector?: unknown; label?: unknown };
|
|
2910
|
+
const safeLabel = safeModelButtonLabel(label);
|
|
2911
|
+
if (typeof selector !== "string" || !selector.trim() || !safeLabel) continue;
|
|
2912
|
+
choices.push({ selector, label: safeLabel });
|
|
2913
|
+
}
|
|
2914
|
+
if (choices.length === 0) return false;
|
|
2915
|
+
|
|
2916
|
+
const rendered = renderThreadedFrame({ ...msg, type: "control_command_result" });
|
|
2917
|
+
if (!rendered?.text) return false;
|
|
2918
|
+
const topicId =
|
|
2919
|
+
(await this.existingTopicForPrivateChat(session.sessionId)) ??
|
|
2920
|
+
(await this.ensureTopic(session.sessionId, this.topicNameFor(session.sessionId, msg)));
|
|
2921
|
+
if (!topicId) return false;
|
|
2922
|
+
|
|
2923
|
+
// Each logical session owns only its most recently rendered menu.
|
|
2924
|
+
this.#clearModelChoiceAliases(logicalSessionId);
|
|
2925
|
+
const aliases = choices.map(choice =>
|
|
2926
|
+
this.#putModelChoiceAlias({ session, sessionId: logicalSessionId, selector: choice.selector }),
|
|
2927
|
+
);
|
|
2928
|
+
const inline_keyboard = buildButtonGrid(
|
|
2929
|
+
choices.map(choice => choice.label),
|
|
2930
|
+
index => aliases[index]!,
|
|
2931
|
+
);
|
|
2932
|
+
try {
|
|
2933
|
+
const response = await this.botApi.call("sendMessage", {
|
|
2934
|
+
chat_id: this.opts.chatId,
|
|
2935
|
+
message_thread_id: Number(topicId),
|
|
2936
|
+
text: rendered.text,
|
|
2937
|
+
parse_mode: TELEGRAM_PARSE_MODE,
|
|
2938
|
+
reply_markup: { inline_keyboard },
|
|
2939
|
+
});
|
|
2940
|
+
if (response && typeof response === "object" && (response as { ok?: unknown }).ok === false) {
|
|
2941
|
+
for (const alias of aliases) this.#modelChoiceAliases.delete(alias);
|
|
2942
|
+
logger.warn("notifications: failed to send model selection keyboard");
|
|
2943
|
+
return false;
|
|
2944
|
+
}
|
|
2945
|
+
} catch {
|
|
2946
|
+
for (const alias of aliases) this.#modelChoiceAliases.delete(alias);
|
|
2947
|
+
logger.warn("notifications: failed to send model selection keyboard");
|
|
2948
|
+
return false;
|
|
2949
|
+
}
|
|
2950
|
+
return true;
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2667
2953
|
async handleSessionMessage(session: SessionSocket, msg: any): Promise<void> {
|
|
2954
|
+
if (session.replayPending) {
|
|
2955
|
+
const matchingReplay = msg?.type === "event_replay_result" && msg.id === session.replayId;
|
|
2956
|
+
if (!matchingReplay) {
|
|
2957
|
+
session.replayQueue.push(msg as Record<string, unknown>);
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2960
|
+
session.replayPending = false;
|
|
2961
|
+
const replayValid =
|
|
2962
|
+
Number.isSafeInteger(msg.generation) &&
|
|
2963
|
+
msg.generation >= 1 &&
|
|
2964
|
+
Number.isSafeInteger(msg.lastSeq) &&
|
|
2965
|
+
msg.lastSeq >= 0 &&
|
|
2966
|
+
Array.isArray(msg.events);
|
|
2967
|
+
const replayed: Record<string, unknown>[] = replayValid
|
|
2968
|
+
? (msg.events as unknown[]).flatMap((event: unknown): Record<string, unknown>[] => {
|
|
2969
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) return [];
|
|
2970
|
+
const envelope = event as Record<string, unknown>;
|
|
2971
|
+
const payload = envelope.payload;
|
|
2972
|
+
return [
|
|
2973
|
+
payload && typeof payload === "object" && !Array.isArray(payload)
|
|
2974
|
+
? (payload as Record<string, unknown>)
|
|
2975
|
+
: envelope,
|
|
2976
|
+
];
|
|
2977
|
+
})
|
|
2978
|
+
: [];
|
|
2979
|
+
// Replay restores durable attachment state only. Live notification effects
|
|
2980
|
+
// (turn streams, context updates, lifecycle messages) may already have been
|
|
2981
|
+
// delivered before a reconnect and must never be rendered a second time.
|
|
2982
|
+
const identityIndex = replayed.findLastIndex(frame => frame.type === "identity_header");
|
|
2983
|
+
const currentGeneration = identityIndex < 0 ? replayed : replayed.slice(identityIndex);
|
|
2984
|
+
const latestIdentity = identityIndex < 0 ? undefined : replayed[identityIndex];
|
|
2985
|
+
const latestActions = new Map<string, Record<string, unknown>>();
|
|
2986
|
+
for (const frame of currentGeneration) {
|
|
2987
|
+
if ((frame.type === "action_needed" || frame.type === "action_resolved") && typeof frame.id === "string")
|
|
2988
|
+
latestActions.set(frame.id, frame);
|
|
2989
|
+
}
|
|
2990
|
+
const replayState = [...(latestIdentity ? [latestIdentity] : []), ...latestActions.values()];
|
|
2991
|
+
const replayCounts = new Map<string, number>();
|
|
2992
|
+
for (const frame of replayState) {
|
|
2993
|
+
const fingerprint = JSON.stringify(frame);
|
|
2994
|
+
replayCounts.set(fingerprint, (replayCounts.get(fingerprint) ?? 0) + 1);
|
|
2995
|
+
await this.handleSessionMessage(session, frame);
|
|
2996
|
+
}
|
|
2997
|
+
const queued = session.replayQueue.splice(0);
|
|
2998
|
+
for (const frame of queued) {
|
|
2999
|
+
const fingerprint = JSON.stringify(frame);
|
|
3000
|
+
const remaining = replayCounts.get(fingerprint) ?? 0;
|
|
3001
|
+
if (remaining > 0) {
|
|
3002
|
+
if (remaining === 1) replayCounts.delete(fingerprint);
|
|
3003
|
+
else replayCounts.set(fingerprint, remaining - 1);
|
|
3004
|
+
continue;
|
|
3005
|
+
}
|
|
3006
|
+
await this.handleSessionMessage(session, frame);
|
|
3007
|
+
}
|
|
3008
|
+
if (replayValid && this.topics.markReplayCursor(session.sessionId, msg.generation, msg.lastSeq))
|
|
3009
|
+
await this.persistTopics();
|
|
3010
|
+
return;
|
|
3011
|
+
}
|
|
3012
|
+
if (msg?.type === "event_replay_result") return;
|
|
3013
|
+
if (msg && typeof msg === "object") this.#updateLogicalSessionForThreadedFrame(session, msg);
|
|
2668
3014
|
if (await this.sessionRouter.dispatch(session, msg as Record<string, unknown>)) return;
|
|
3015
|
+
if (await this.#renderModelChoices(session, msg as Record<string, unknown>)) return;
|
|
3016
|
+
|
|
2669
3017
|
if (typeof msg?.type === "string" && TelegramNotificationDaemon.THREADED_FRAMES.has(msg.type)) {
|
|
2670
3018
|
const send = renderThreadedFrame(msg);
|
|
2671
3019
|
if (!send) return;
|
|
@@ -2864,6 +3212,80 @@ export class TelegramNotificationDaemon {
|
|
|
2864
3212
|
}
|
|
2865
3213
|
}
|
|
2866
3214
|
|
|
3215
|
+
/** Claim and forward one session-bound model alias before acknowledging its callback. */
|
|
3216
|
+
async #handleModelChoiceCallback(update: unknown, callbackId: unknown): Promise<boolean> {
|
|
3217
|
+
const callback = (
|
|
3218
|
+
update as {
|
|
3219
|
+
callback_query?: { data?: unknown; message?: { chat?: { id?: unknown } } };
|
|
3220
|
+
}
|
|
3221
|
+
).callback_query;
|
|
3222
|
+
const alias = callback?.data;
|
|
3223
|
+
if (typeof alias !== "string" || !alias.startsWith(MODEL_CALLBACK_PREFIX)) return false;
|
|
3224
|
+
this.#sweepExpiredModelChoiceAliases();
|
|
3225
|
+
if (String(callback?.message?.chat?.id) !== String(this.opts.chatId)) {
|
|
3226
|
+
await this.answerCallbackQueryBestEffort(callbackId, "Not authorized");
|
|
3227
|
+
return true;
|
|
3228
|
+
}
|
|
3229
|
+
if (!(await this.pairedChatIsPrivate())) {
|
|
3230
|
+
await this.answerCallbackQueryBestEffort(callbackId, "Not authorized");
|
|
3231
|
+
return true;
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
const route = this.#modelChoiceAliases.get(alias);
|
|
3235
|
+
if (!route) {
|
|
3236
|
+
await this.#sendModelStaleGuidance(callbackId);
|
|
3237
|
+
return true;
|
|
3238
|
+
}
|
|
3239
|
+
const session = route.session;
|
|
3240
|
+
if (
|
|
3241
|
+
this.sessions.get(session.sessionId) !== session ||
|
|
3242
|
+
this.#logicalSessionId(session) !== route.sessionId ||
|
|
3243
|
+
session.ws.readyState !== WebSocket.OPEN
|
|
3244
|
+
) {
|
|
3245
|
+
this.#modelChoiceAliases.delete(alias);
|
|
3246
|
+
await this.#sendModelStaleGuidance(callbackId);
|
|
3247
|
+
return true;
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
// Delete before send so a duplicate tap can only become stale, never duplicate a control command.
|
|
3251
|
+
this.#modelChoiceAliases.delete(alias);
|
|
3252
|
+
const updateId = (update as { update_id?: unknown }).update_id;
|
|
3253
|
+
const safeUpdateId =
|
|
3254
|
+
typeof updateId === "number" && Number.isSafeInteger(updateId) && updateId >= 0 ? updateId : undefined;
|
|
3255
|
+
try {
|
|
3256
|
+
session.ws.send(
|
|
3257
|
+
JSON.stringify({
|
|
3258
|
+
type: "control_command",
|
|
3259
|
+
sessionId: route.sessionId,
|
|
3260
|
+
token: session.token,
|
|
3261
|
+
requestId: safeUpdateId === undefined ? `tg:model:${alias}` : `tg:model:${safeUpdateId}`,
|
|
3262
|
+
...(safeUpdateId === undefined ? {} : { updateId: safeUpdateId }),
|
|
3263
|
+
command: { name: "model", action: "set", selector: route.selector },
|
|
3264
|
+
}),
|
|
3265
|
+
);
|
|
3266
|
+
} catch {
|
|
3267
|
+
await this.#sendModelStaleGuidance(callbackId);
|
|
3268
|
+
return true;
|
|
3269
|
+
}
|
|
3270
|
+
await this.answerCallbackQueryBestEffort(callbackId);
|
|
3271
|
+
return true;
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
async #sendModelStaleGuidance(callbackId: unknown): Promise<void> {
|
|
3275
|
+
const text = "Button is stale. Run /model again.";
|
|
3276
|
+
await this.answerCallbackQueryBestEffort(callbackId, text);
|
|
3277
|
+
if (!(await this.pairedChatIsPrivate())) return;
|
|
3278
|
+
try {
|
|
3279
|
+
await this.botApi.call("sendMessage", {
|
|
3280
|
+
chat_id: this.opts.chatId,
|
|
3281
|
+
text,
|
|
3282
|
+
parse_mode: TELEGRAM_PARSE_MODE,
|
|
3283
|
+
});
|
|
3284
|
+
} catch {
|
|
3285
|
+
// Best-effort stale guidance must not reject a callback.
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
|
|
2867
3289
|
private async sendStaleGuidance(callbackId: unknown): Promise<void> {
|
|
2868
3290
|
await this.answerCallbackQueryBestEffort(callbackId, "Button is stale");
|
|
2869
3291
|
if (!(await this.pairedChatIsPrivate())) return;
|
|
@@ -3040,7 +3462,7 @@ export class TelegramNotificationDaemon {
|
|
|
3040
3462
|
// update_id dedupe are all enforced by decideThreadedInbound.
|
|
3041
3463
|
const raw = update as {
|
|
3042
3464
|
callback_query?: unknown;
|
|
3043
|
-
message?: { reply_to_message?: { message_id?: unknown } };
|
|
3465
|
+
message?: { text?: unknown; reply_to_message?: { message_id?: unknown } };
|
|
3044
3466
|
};
|
|
3045
3467
|
// A reply to a known ask message routes to that ask (below). Any OTHER
|
|
3046
3468
|
// message in a topic (plain text, or a reply to a non-ask message) is a
|
|
@@ -3048,7 +3470,12 @@ export class TelegramNotificationDaemon {
|
|
|
3048
3470
|
const replyTo = raw.message?.reply_to_message?.message_id;
|
|
3049
3471
|
const isAskReply =
|
|
3050
3472
|
replyTo !== undefined && (this.messageRoutes.has(String(replyTo)) || this.messageRoutes.has(Number(replyTo)));
|
|
3051
|
-
|
|
3473
|
+
const directControl =
|
|
3474
|
+
typeof raw.message?.text === "string"
|
|
3475
|
+
? parseTelegramControlCommand(raw.message.text, this.botUsername)
|
|
3476
|
+
: ({ kind: "none" } as const);
|
|
3477
|
+
const interceptAskControl = isAskReply && directControl.kind !== "none";
|
|
3478
|
+
if (!raw.callback_query && (!isAskReply || interceptAskControl)) {
|
|
3052
3479
|
const inbound = decideThreadedInbound(update as never, {
|
|
3053
3480
|
pairedChatId: this.opts.chatId,
|
|
3054
3481
|
topicToSession: t => this.topics.sessionForTopic(t),
|
|
@@ -3056,7 +3483,27 @@ export class TelegramNotificationDaemon {
|
|
|
3056
3483
|
});
|
|
3057
3484
|
if (inbound.kind === "duplicate") return;
|
|
3058
3485
|
if (inbound.kind === "inject") {
|
|
3486
|
+
if (!(await this.pairedChatIsPrivate())) return;
|
|
3487
|
+
const preliminaryControl = inbound.attachment
|
|
3488
|
+
? { kind: "none" as const }
|
|
3489
|
+
: parseTelegramControlCommand(inbound.text, this.botUsername);
|
|
3490
|
+
if (preliminaryControl.kind === "ignored") return;
|
|
3059
3491
|
const session = this.sessions.get(inbound.sessionId);
|
|
3492
|
+
if (preliminaryControl.kind === "invalid" && session?.ws.readyState !== WebSocket.OPEN) return;
|
|
3493
|
+
if (preliminaryControl.kind === "command" && session?.ws.readyState !== WebSocket.OPEN) {
|
|
3494
|
+
if (await this.rememberSeenUpdateIdForUnavailableNotice(inbound.updateId)) {
|
|
3495
|
+
try {
|
|
3496
|
+
await this.botApi.call("sendMessage", {
|
|
3497
|
+
chat_id: this.opts.chatId,
|
|
3498
|
+
message_thread_id: Number(inbound.threadId),
|
|
3499
|
+
text: "Session control unavailable: this local GJC session is disconnected.",
|
|
3500
|
+
});
|
|
3501
|
+
} catch {
|
|
3502
|
+
logger.warn("notifications: unavailable-control notice delivery failed");
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
return;
|
|
3506
|
+
}
|
|
3060
3507
|
if (session?.ws.readyState === WebSocket.OPEN) {
|
|
3061
3508
|
const attachmentResult = inbound.attachment
|
|
3062
3509
|
? await this.resolveInboundAttachment(inbound.attachment, inbound.sessionId)
|
|
@@ -3075,9 +3522,7 @@ export class TelegramNotificationDaemon {
|
|
|
3075
3522
|
const injectedText = repliedOriginal
|
|
3076
3523
|
? `> replied-to message:\n${repliedOriginal}\n\n${baseInjectedText}`
|
|
3077
3524
|
: baseInjectedText;
|
|
3078
|
-
const control = hasMedia
|
|
3079
|
-
? { kind: "none" as const }
|
|
3080
|
-
: parseTelegramControlCommand(inbound.text, this.botUsername);
|
|
3525
|
+
const control = hasMedia ? { kind: "none" as const } : preliminaryControl;
|
|
3081
3526
|
if (control.kind !== "none") {
|
|
3082
3527
|
await this.rememberSeenUpdateId(inbound.updateId);
|
|
3083
3528
|
const sendControlNotice = async (body: string): Promise<void> => {
|
|
@@ -3092,7 +3537,6 @@ export class TelegramNotificationDaemon {
|
|
|
3092
3537
|
// Best-effort control feedback; never convert to user input.
|
|
3093
3538
|
}
|
|
3094
3539
|
};
|
|
3095
|
-
if (control.kind === "ignored") return;
|
|
3096
3540
|
if (control.kind === "invalid") {
|
|
3097
3541
|
await sendControlNotice(control.usage);
|
|
3098
3542
|
return;
|
|
@@ -3104,7 +3548,7 @@ export class TelegramNotificationDaemon {
|
|
|
3104
3548
|
session.ws.send(
|
|
3105
3549
|
JSON.stringify({
|
|
3106
3550
|
type: "control_command",
|
|
3107
|
-
sessionId:
|
|
3551
|
+
sessionId: this.#logicalSessionId(session),
|
|
3108
3552
|
token: session.token,
|
|
3109
3553
|
requestId: `tg:${inbound.updateId}`,
|
|
3110
3554
|
updateId: inbound.updateId,
|
|
@@ -3161,6 +3605,7 @@ export class TelegramNotificationDaemon {
|
|
|
3161
3605
|
}
|
|
3162
3606
|
}
|
|
3163
3607
|
const callbackId = (update as { callback_query?: { id?: unknown } }).callback_query?.id;
|
|
3608
|
+
if (await this.#handleModelChoiceCallback(update, callbackId)) return;
|
|
3164
3609
|
const decision = routeInboundUpdate(update, {
|
|
3165
3610
|
aliasTable: this.aliasTable,
|
|
3166
3611
|
messageRoutes: this.messageRoutes,
|
|
@@ -3196,6 +3641,7 @@ export class TelegramNotificationDaemon {
|
|
|
3196
3641
|
{ command: "rich", description: "Toggle rich Telegram delivery: /rich <on|off>" },
|
|
3197
3642
|
{ command: "reasoning", description: "Show or change reasoning effort in this session" },
|
|
3198
3643
|
{ command: "usage", description: "Show provider/local usage for this session" },
|
|
3644
|
+
{ command: "model", description: "Select a model for this session" },
|
|
3199
3645
|
{ command: "context", description: "Show current context usage for this session" },
|
|
3200
3646
|
{ command: "compact", description: "Compact this session: /compact [instructions]" },
|
|
3201
3647
|
{ command: "session_create", description: "Create a GJC session: path, worktree, or dir [--mpreset]" },
|
|
@@ -3268,7 +3714,9 @@ export class TelegramNotificationDaemon {
|
|
|
3268
3714
|
// Back off (bounded, below the heartbeat TTL) and keep renewing
|
|
3269
3715
|
// ownership at the loop top.
|
|
3270
3716
|
const backoffMs = this.loopBackoff.next();
|
|
3271
|
-
logger.warn(
|
|
3717
|
+
logger.warn(
|
|
3718
|
+
`notifications: getUpdates failed, backing off ${backoffMs}ms: ${sanitizeDiagnostic(String(e))}`,
|
|
3719
|
+
);
|
|
3272
3720
|
await this.runtime.sleep(backoffMs);
|
|
3273
3721
|
continue;
|
|
3274
3722
|
} finally {
|