@gajae-code/coding-agent 0.10.2 → 0.11.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 +58 -2
- package/dist/types/append-only-mode.d.ts +1 -1
- package/dist/types/async/job-manager.d.ts +46 -7
- 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/ultragoal-receipt-freshness.d.ts +11 -0
- 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/command-controller.d.ts +2 -2
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -0
- 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 +5 -2
- 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 +170 -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 +7 -2
- 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 +42 -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 +50 -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 +78 -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/dist/types/tools/subagent-render.d.ts +1 -0
- package/dist/types/tools/subagent.d.ts +6 -1
- package/package.json +34 -13
- package/scripts/build-sdk-package-smoke.ts +99 -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/async/job-manager.ts +406 -63
- 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 +108 -14
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +102 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +227 -11
- 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 +10 -17
- package/src/modes/controllers/event-controller.ts +11 -16
- package/src/modes/controllers/extension-ui-controller.ts +396 -28
- package/src/modes/controllers/selector-controller.ts +674 -36
- package/src/modes/index.ts +0 -22
- package/src/modes/interactive-mode.ts +87 -36
- 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 +1258 -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 +7 -2
- 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 +113 -0
- package/src/sdk/broker/ensure.ts +307 -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 +355 -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 +2292 -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 +3979 -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 +603 -63
- 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 +2650 -944
- 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/task/index.ts +26 -16
- 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/subagent-render.ts +19 -8
- package/src/tools/subagent.ts +84 -32
- 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
|
/**
|
|
@@ -843,23 +941,96 @@ type PairedChatPrivacy = "private" | "non-private" | "indeterminate";
|
|
|
843
941
|
|
|
844
942
|
export type TelegramUpdateOutcome = "consumed" | "retry";
|
|
845
943
|
|
|
944
|
+
export type TelegramPollResult =
|
|
945
|
+
| { kind: "success"; updateCount: number }
|
|
946
|
+
| { kind: "aborted" }
|
|
947
|
+
| { kind: "getUpdates_failed"; error: string }
|
|
948
|
+
| { kind: "api_failure"; errorCode?: number; description: string }
|
|
949
|
+
| { kind: "conflict"; description: string; backoffMs: number };
|
|
950
|
+
|
|
846
951
|
export interface TelegramUpdatePollerOptions {
|
|
847
952
|
botApi: BotApi;
|
|
848
953
|
runtime: NotificationOperatorRuntime;
|
|
849
954
|
backoff: OperatorBackoffPolicy;
|
|
850
955
|
processUpdate: (update: unknown) => Promise<TelegramUpdateOutcome>;
|
|
956
|
+
health?: TelegramPollHealth;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
type TelegramPollHealthStatus = "healthy" | "getUpdates_failed" | "api_failure" | "conflict";
|
|
960
|
+
|
|
961
|
+
export class TelegramPollHealth {
|
|
962
|
+
#status: TelegramPollHealthStatus = "healthy";
|
|
963
|
+
#suppressedCount = 0;
|
|
964
|
+
|
|
965
|
+
record(result: TelegramPollResult): void {
|
|
966
|
+
if (result.kind === "aborted") return;
|
|
967
|
+
if (result.kind === "success") {
|
|
968
|
+
if (this.#status !== "healthy") {
|
|
969
|
+
logger.info("notifications daemon: Telegram getUpdates recovered", {
|
|
970
|
+
from: this.#status,
|
|
971
|
+
suppressedCount: this.#suppressedCount,
|
|
972
|
+
updateCount: result.updateCount,
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
this.#status = "healthy";
|
|
976
|
+
this.#suppressedCount = 0;
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const previousStatus = this.#status;
|
|
981
|
+
if (previousStatus === result.kind) {
|
|
982
|
+
this.#suppressedCount += 1;
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const suppressedCount = this.#suppressedCount;
|
|
987
|
+
this.#status = result.kind;
|
|
988
|
+
this.#suppressedCount = 0;
|
|
989
|
+
if (result.kind === "conflict") {
|
|
990
|
+
logger.error("notifications daemon: Telegram getUpdates 409 conflict", {
|
|
991
|
+
description: result.description,
|
|
992
|
+
backoffMs: result.backoffMs,
|
|
993
|
+
previousStatus,
|
|
994
|
+
suppressedCount,
|
|
995
|
+
});
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
if (result.kind === "api_failure") {
|
|
999
|
+
logger.error("notifications daemon: Telegram getUpdates API failed", {
|
|
1000
|
+
errorCode: result.errorCode,
|
|
1001
|
+
description: result.description,
|
|
1002
|
+
previousStatus,
|
|
1003
|
+
suppressedCount,
|
|
1004
|
+
});
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
logger.error("notifications daemon: getUpdates failed", {
|
|
1009
|
+
error: result.error,
|
|
1010
|
+
previousStatus,
|
|
1011
|
+
suppressedCount,
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
851
1014
|
}
|
|
852
1015
|
|
|
853
1016
|
/** Owns getUpdates offset, conflict backoff, and per-update error isolation. */
|
|
854
1017
|
export class TelegramUpdatePoller {
|
|
855
1018
|
#offset = 0;
|
|
856
1019
|
#opts: TelegramUpdatePollerOptions;
|
|
1020
|
+
#health: TelegramPollHealth;
|
|
857
1021
|
|
|
858
1022
|
constructor(opts: TelegramUpdatePollerOptions) {
|
|
859
1023
|
this.#opts = opts;
|
|
1024
|
+
this.#health = opts.health ?? new TelegramPollHealth();
|
|
860
1025
|
}
|
|
861
1026
|
|
|
862
1027
|
async pollOnce(signal?: AbortSignal): Promise<number> {
|
|
1028
|
+
const result = await this.pollOnceResult(signal);
|
|
1029
|
+
this.#health.record(result);
|
|
1030
|
+
return result.kind === "success" ? result.updateCount : 0;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
async pollOnceResult(signal?: AbortSignal): Promise<TelegramPollResult> {
|
|
863
1034
|
let body: {
|
|
864
1035
|
ok?: boolean;
|
|
865
1036
|
error_code?: number;
|
|
@@ -874,24 +1045,37 @@ export class TelegramUpdatePoller {
|
|
|
874
1045
|
)) as typeof body;
|
|
875
1046
|
} catch (err) {
|
|
876
1047
|
// A cooperative stop aborts the in-flight long poll; treat as a clean wake.
|
|
877
|
-
if (isAbortError(err)) return
|
|
1048
|
+
if (isAbortError(err)) return { kind: "aborted" };
|
|
878
1049
|
// A transient Telegram API failure must never crash the daemon.
|
|
879
|
-
logger.error("notifications daemon: getUpdates failed", { error: String(err) });
|
|
880
1050
|
await this.#opts.runtime.sleep(POLL_BACKOFF_MS, signal);
|
|
881
|
-
return
|
|
1051
|
+
return { kind: "getUpdates_failed", error: sanitizeDiagnostic(String(err)) };
|
|
882
1052
|
}
|
|
883
1053
|
// Telegram allows only one active getUpdates poller per bot. A 409 means
|
|
884
1054
|
// another poller is live; back off boundedly instead of hot-looping.
|
|
885
1055
|
if (body && body.ok === false && (body.error_code === 409 || /409|conflict/i.test(body.description ?? ""))) {
|
|
886
1056
|
const backoffMs = this.#opts.backoff.next();
|
|
887
|
-
logger.error(
|
|
888
|
-
`notifications daemon: Telegram getUpdates 409 conflict (${body.description ?? "no description"}); backing off ${backoffMs}ms`,
|
|
889
|
-
);
|
|
890
1057
|
await this.#opts.runtime.sleep(backoffMs, signal);
|
|
891
|
-
return
|
|
1058
|
+
return { kind: "conflict", description: sanitizeDiagnostic(body.description ?? "no description"), backoffMs };
|
|
1059
|
+
}
|
|
1060
|
+
if (body?.ok !== true || !Array.isArray(body.result)) {
|
|
1061
|
+
await this.#opts.runtime.sleep(POLL_BACKOFF_MS, signal);
|
|
1062
|
+
return {
|
|
1063
|
+
kind: "api_failure",
|
|
1064
|
+
errorCode: typeof body?.error_code === "number" ? body.error_code : undefined,
|
|
1065
|
+
description: sanitizeDiagnostic(body?.description ?? "Malformed getUpdates response"),
|
|
1066
|
+
};
|
|
892
1067
|
}
|
|
893
1068
|
this.#opts.backoff.reset();
|
|
894
|
-
|
|
1069
|
+
let malformedSeen = false;
|
|
1070
|
+
for (const update of body.result) {
|
|
1071
|
+
// A single malformed update_id must not wedge the poller. Skip the
|
|
1072
|
+
// bad entry (surfaced below as an api_failure health signal) while
|
|
1073
|
+
// still processing valid updates and advancing the offset past them,
|
|
1074
|
+
// so one poisoned item cannot stall an otherwise-valid stream.
|
|
1075
|
+
if (!Number.isSafeInteger(update?.update_id)) {
|
|
1076
|
+
malformedSeen = true;
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
895
1079
|
try {
|
|
896
1080
|
const outcome = await this.#opts.processUpdate(update);
|
|
897
1081
|
if (outcome === "retry") {
|
|
@@ -900,11 +1084,17 @@ export class TelegramUpdatePoller {
|
|
|
900
1084
|
}
|
|
901
1085
|
this.#offset = update.update_id + 1;
|
|
902
1086
|
} catch (err) {
|
|
903
|
-
logger.error("notifications daemon: handleTelegramUpdate failed", {
|
|
1087
|
+
logger.error("notifications daemon: handleTelegramUpdate failed", {
|
|
1088
|
+
error: sanitizeDiagnostic(String(err)),
|
|
1089
|
+
});
|
|
904
1090
|
this.#offset = update.update_id + 1;
|
|
905
1091
|
}
|
|
906
1092
|
}
|
|
907
|
-
|
|
1093
|
+
if (malformedSeen) {
|
|
1094
|
+
await this.#opts.runtime.sleep(POLL_BACKOFF_MS, signal);
|
|
1095
|
+
return { kind: "api_failure", description: "Malformed getUpdates response" };
|
|
1096
|
+
}
|
|
1097
|
+
return { kind: "success", updateCount: body.result.length };
|
|
908
1098
|
}
|
|
909
1099
|
}
|
|
910
1100
|
|
|
@@ -955,6 +1145,16 @@ export interface TelegramDaemonOptions {
|
|
|
955
1145
|
* default applies (e.g. lifecycle control disabled), no control server starts.
|
|
956
1146
|
*/
|
|
957
1147
|
createLifecycleControlServer?: LifecycleControlServerFactory | null;
|
|
1148
|
+
/**
|
|
1149
|
+
* Test seam for observing the exact production lifecycle dependency input.
|
|
1150
|
+
* Production defaults to {@link buildOrchestratorDeps}.
|
|
1151
|
+
*/
|
|
1152
|
+
createLifecycleOrchestratorDeps?: (input: {
|
|
1153
|
+
pairedChatId: string;
|
|
1154
|
+
agentNotificationsDir: string;
|
|
1155
|
+
sessionsRoot: string;
|
|
1156
|
+
auditRedactionKey: Uint8Array;
|
|
1157
|
+
}) => OrchestratorDeps;
|
|
958
1158
|
/** Rich text promotion (enabled by default; see rich-render.ts). */
|
|
959
1159
|
rich?: { enabled: boolean };
|
|
960
1160
|
/** Opt-in rich-draft streaming of live turn previews (off by default; see rich-draft.ts). */
|
|
@@ -968,7 +1168,10 @@ export interface TelegramDaemonOptions {
|
|
|
968
1168
|
}
|
|
969
1169
|
|
|
970
1170
|
interface SessionSocket {
|
|
1171
|
+
/** Immutable key of the transport endpoint that owns this socket. */
|
|
971
1172
|
sessionId: string;
|
|
1173
|
+
/** Current logical session carried by valid threaded frames on this transport. */
|
|
1174
|
+
logicalSessionId: string;
|
|
972
1175
|
token: string;
|
|
973
1176
|
endpointKey: string;
|
|
974
1177
|
ws: WebSocket;
|
|
@@ -981,6 +1184,25 @@ interface SessionSocket {
|
|
|
981
1184
|
awaitingNonce: string | undefined;
|
|
982
1185
|
/** Per-session liveness interval handle (only set for capable sessions). */
|
|
983
1186
|
pingTimer: ReturnType<typeof setInterval> | undefined;
|
|
1187
|
+
/** Correlation id for the startup replay barrier. */
|
|
1188
|
+
replayId: string;
|
|
1189
|
+
/** Queues live frames until startup replay is applied. */
|
|
1190
|
+
replayPending: boolean;
|
|
1191
|
+
replayQueue: Record<string, unknown>[];
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
interface ModelChoiceRoute {
|
|
1195
|
+
/** Exact transport socket that rendered this choice. */
|
|
1196
|
+
session: SessionSocket;
|
|
1197
|
+
/** Logical session current when this choice was rendered. */
|
|
1198
|
+
sessionId: string;
|
|
1199
|
+
selector: string;
|
|
1200
|
+
expiresAt: number;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
interface RenderedModelChoice {
|
|
1204
|
+
selector: string;
|
|
1205
|
+
label: string;
|
|
984
1206
|
}
|
|
985
1207
|
|
|
986
1208
|
interface PendingThreadedFrame {
|
|
@@ -1017,6 +1239,9 @@ export class TelegramNotificationDaemon {
|
|
|
1017
1239
|
/** Telegram message id backing each streamed `${sessionId}:${coalesceKey}`, for in-place edits. */
|
|
1018
1240
|
private readonly liveMessages = new Map<string, number>();
|
|
1019
1241
|
readonly sessions = new Map<string, SessionSocket>();
|
|
1242
|
+
/** Ephemeral aliases for model choices; deliberately never serialized across daemon restarts. */
|
|
1243
|
+
#modelChoiceAliases = new Map<string, ModelChoiceRoute>();
|
|
1244
|
+
|
|
1020
1245
|
private readonly runtime: NotificationOperatorRuntime;
|
|
1021
1246
|
private readonly sessionRouter: OperatorEventRouter<SessionSocket>;
|
|
1022
1247
|
private readonly pollConflictBackoff = new OperatorBackoffPolicy({ initialMs: 500, maxMs: 5_000 });
|
|
@@ -1140,10 +1365,11 @@ export class TelegramNotificationDaemon {
|
|
|
1140
1365
|
const token = crypto.randomBytes(32).toString("base64url");
|
|
1141
1366
|
const agentDir = this.opts.settings.getAgentDir();
|
|
1142
1367
|
server = factory({ token, ownerId: this.opts.ownerId, agentDir });
|
|
1143
|
-
const deps = buildOrchestratorDeps({
|
|
1368
|
+
const deps = (this.opts.createLifecycleOrchestratorDeps ?? buildOrchestratorDeps)({
|
|
1144
1369
|
pairedChatId: this.opts.chatId,
|
|
1145
1370
|
agentNotificationsDir: daemonPaths(agentDir).dir,
|
|
1146
1371
|
sessionsRoot: path.join(agentDir, "sessions"),
|
|
1372
|
+
auditRedactionKey: deriveLifecycleAuditRedactionKey(this.opts.botToken),
|
|
1147
1373
|
});
|
|
1148
1374
|
// Register the lifecycle-request handler BEFORE start(): the native
|
|
1149
1375
|
// control server captures the callback at start time, so wiring must
|
|
@@ -1697,6 +1923,26 @@ export class TelegramNotificationDaemon {
|
|
|
1697
1923
|
logger.warn(`notifications: failed to persist Telegram update id ${updateId}: ${String(err)}`);
|
|
1698
1924
|
}
|
|
1699
1925
|
}
|
|
1926
|
+
private async rememberSeenUpdateIdForUnavailableNotice(updateId: number): Promise<boolean> {
|
|
1927
|
+
if (!Number.isSafeInteger(updateId) || updateId < 0) return false;
|
|
1928
|
+
const candidate = new Set(this.dispatchState.seenUpdateIds);
|
|
1929
|
+
candidate.add(updateId);
|
|
1930
|
+
while (candidate.size > SEEN_UPDATE_ID_LIMIT) candidate.delete(candidate.values().next().value!);
|
|
1931
|
+
try {
|
|
1932
|
+
const paths = daemonPaths(this.opts.settings.getAgentDir());
|
|
1933
|
+
await ensureDir(this.fsImpl, paths.dir);
|
|
1934
|
+
await writeJsonAtomic(this.fsImpl, paths.seenUpdates, {
|
|
1935
|
+
version: 1,
|
|
1936
|
+
updateIds: [...candidate],
|
|
1937
|
+
});
|
|
1938
|
+
} catch {
|
|
1939
|
+
logger.warn("notifications: unavailable-control notice state publication failed");
|
|
1940
|
+
return false;
|
|
1941
|
+
}
|
|
1942
|
+
this.dispatchState.seenUpdateIds.clear();
|
|
1943
|
+
for (const seenId of candidate) this.dispatchState.seenUpdateIds.add(seenId);
|
|
1944
|
+
return true;
|
|
1945
|
+
}
|
|
1700
1946
|
|
|
1701
1947
|
async scanRoots(): Promise<void> {
|
|
1702
1948
|
const paths = daemonPaths(this.opts.settings.getAgentDir());
|
|
@@ -1704,7 +1950,7 @@ export class TelegramNotificationDaemon {
|
|
|
1704
1950
|
const endpointSessionIds = new Set<string>();
|
|
1705
1951
|
let allRootsReadable = true;
|
|
1706
1952
|
for (const root of rootState?.roots ?? []) {
|
|
1707
|
-
const dir = path.join(root, "
|
|
1953
|
+
const dir = path.join(root, "sdk");
|
|
1708
1954
|
let files: string[];
|
|
1709
1955
|
try {
|
|
1710
1956
|
files = await this.fsImpl.readdir(dir);
|
|
@@ -1748,8 +1994,14 @@ export class TelegramNotificationDaemon {
|
|
|
1748
1994
|
const ws = new WS(`${url}/?token=${encodeURIComponent(token)}`);
|
|
1749
1995
|
const endpointKey = endpointGenerationKey(url, token);
|
|
1750
1996
|
this.closedEndpointKeys.delete(sessionId);
|
|
1997
|
+
const existing = this.sessions.get(sessionId);
|
|
1998
|
+
if (existing) this.#clearModelChoiceAliasesForSocket(existing);
|
|
1999
|
+
this.#clearModelChoiceAliases(sessionId);
|
|
2000
|
+
|
|
1751
2001
|
const session: SessionSocket = {
|
|
1752
2002
|
sessionId,
|
|
2003
|
+
logicalSessionId: sessionId,
|
|
2004
|
+
|
|
1753
2005
|
token,
|
|
1754
2006
|
endpointKey,
|
|
1755
2007
|
ws,
|
|
@@ -1758,12 +2010,18 @@ export class TelegramNotificationDaemon {
|
|
|
1758
2010
|
lastPongAt: 0,
|
|
1759
2011
|
awaitingNonce: undefined,
|
|
1760
2012
|
pingTimer: undefined,
|
|
2013
|
+
replayId: `telegram-startup-replay:${sessionId}`,
|
|
2014
|
+
replayPending: false,
|
|
2015
|
+
replayQueue: [],
|
|
1761
2016
|
};
|
|
1762
2017
|
this.sessions.set(sessionId, session);
|
|
1763
2018
|
// Bidirectional capability advertisement: announce client_ping_pong once the
|
|
1764
2019
|
// socket is open. Sent on "open" only — a real WHATWG WebSocket cannot send
|
|
1765
2020
|
// while CONNECTING — and liveness starts only after a capable ServerHello.
|
|
1766
2021
|
ws.addEventListener("open", () => {
|
|
2022
|
+
session.replayPending = true;
|
|
2023
|
+
session.replayQueue = [];
|
|
2024
|
+
const replayCursor = this.topics.replayCursor(sessionId);
|
|
1767
2025
|
if (session.ws.readyState === WebSocket.OPEN) {
|
|
1768
2026
|
try {
|
|
1769
2027
|
session.ws.send(
|
|
@@ -1774,6 +2032,16 @@ export class TelegramNotificationDaemon {
|
|
|
1774
2032
|
}),
|
|
1775
2033
|
);
|
|
1776
2034
|
} catch {}
|
|
2035
|
+
try {
|
|
2036
|
+
session.ws.send(
|
|
2037
|
+
JSON.stringify({
|
|
2038
|
+
type: "event_replay",
|
|
2039
|
+
id: session.replayId,
|
|
2040
|
+
sinceGeneration: replayCursor?.generation ?? 1,
|
|
2041
|
+
sinceSeq: replayCursor?.seq ?? 0,
|
|
2042
|
+
}),
|
|
2043
|
+
);
|
|
2044
|
+
} catch {}
|
|
1777
2045
|
}
|
|
1778
2046
|
// Eagerly create the session's Telegram topic as soon as it connects, so
|
|
1779
2047
|
// a thread exists the moment a notifications-enabled session is live —
|
|
@@ -1843,8 +2111,10 @@ export class TelegramNotificationDaemon {
|
|
|
1843
2111
|
this.deleteMessageRoutes(session.sessionId);
|
|
1844
2112
|
}
|
|
1845
2113
|
if (isCurrentSession) {
|
|
2114
|
+
this.#clearModelChoiceAliasesForSocket(session);
|
|
1846
2115
|
this.sessions.delete(session.sessionId);
|
|
1847
2116
|
}
|
|
2117
|
+
|
|
1848
2118
|
for (const item of new Set(this.selectedAckPending.values())) {
|
|
1849
2119
|
if (item.session !== session) continue;
|
|
1850
2120
|
if (item.state === "queued") this.pool.removeById(item.itemId);
|
|
@@ -1866,6 +2136,39 @@ export class TelegramNotificationDaemon {
|
|
|
1866
2136
|
}
|
|
1867
2137
|
}
|
|
1868
2138
|
|
|
2139
|
+
#logicalSessionId(session: SessionSocket): string {
|
|
2140
|
+
return session.logicalSessionId ?? session.sessionId;
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
#clearModelChoiceAliases(sessionId: string): void {
|
|
2144
|
+
for (const [alias, route] of this.#modelChoiceAliases) {
|
|
2145
|
+
if (route.sessionId === sessionId) this.#modelChoiceAliases.delete(alias);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
#clearModelChoiceAliasesForSocket(session: SessionSocket): void {
|
|
2150
|
+
for (const [alias, route] of this.#modelChoiceAliases) {
|
|
2151
|
+
if (route.session === session) this.#modelChoiceAliases.delete(alias);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
#sweepExpiredModelChoiceAliases(): void {
|
|
2156
|
+
const now = this.runtime.now();
|
|
2157
|
+
for (const [alias, route] of this.#modelChoiceAliases) {
|
|
2158
|
+
if (route.expiresAt <= now) this.#modelChoiceAliases.delete(alias);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
#putModelChoiceAlias(route: Omit<ModelChoiceRoute, "expiresAt">): string {
|
|
2163
|
+
this.#sweepExpiredModelChoiceAliases();
|
|
2164
|
+
let alias: string;
|
|
2165
|
+
do {
|
|
2166
|
+
alias = `${MODEL_CALLBACK_PREFIX}${crypto.randomBytes(16).toString("base64url")}`;
|
|
2167
|
+
} while (this.#modelChoiceAliases.has(alias));
|
|
2168
|
+
this.#modelChoiceAliases.set(alias, { ...route, expiresAt: this.runtime.now() + MODEL_CHOICE_TTL_MS });
|
|
2169
|
+
return alias;
|
|
2170
|
+
}
|
|
2171
|
+
|
|
1869
2172
|
private static readonly THREADED_FRAMES = new Set([
|
|
1870
2173
|
"identity_header",
|
|
1871
2174
|
"context_update",
|
|
@@ -1876,6 +2179,22 @@ export class TelegramNotificationDaemon {
|
|
|
1876
2179
|
"control_command_result",
|
|
1877
2180
|
]);
|
|
1878
2181
|
|
|
2182
|
+
/** Rekey model controls before a valid threaded frame can render or a silent config update can route output. */
|
|
2183
|
+
#updateLogicalSessionForThreadedFrame(session: SessionSocket, msg: Record<string, unknown>): void {
|
|
2184
|
+
if (
|
|
2185
|
+
typeof msg.type !== "string" ||
|
|
2186
|
+
!TelegramNotificationDaemon.THREADED_FRAMES.has(msg.type) ||
|
|
2187
|
+
typeof msg.sessionId !== "string" ||
|
|
2188
|
+
!msg.sessionId.trim() ||
|
|
2189
|
+
msg.sessionId === this.#logicalSessionId(session) ||
|
|
2190
|
+
(msg.type !== "config_update" && !renderThreadedFrame(msg))
|
|
2191
|
+
)
|
|
2192
|
+
return;
|
|
2193
|
+
this.#clearModelChoiceAliasesForSocket(session);
|
|
2194
|
+
this.#clearModelChoiceAliases(msg.sessionId);
|
|
2195
|
+
session.logicalSessionId = msg.sessionId;
|
|
2196
|
+
}
|
|
2197
|
+
|
|
1879
2198
|
private topicNameFor(sessionId: string, msg: { title?: unknown; repo?: unknown; branch?: unknown }): string {
|
|
1880
2199
|
const repo = typeof msg?.repo === "string" && msg.repo ? msg.repo : undefined;
|
|
1881
2200
|
const branch = typeof msg?.branch === "string" && msg.branch ? msg.branch : undefined;
|
|
@@ -2134,7 +2453,7 @@ export class TelegramNotificationDaemon {
|
|
|
2134
2453
|
if (!res.ok) return undefined;
|
|
2135
2454
|
return Buffer.from(await res.arrayBuffer());
|
|
2136
2455
|
} catch (e) {
|
|
2137
|
-
logger.warn(`notifications: file download failed: ${String(e)}`);
|
|
2456
|
+
logger.warn(`notifications: file download failed: ${sanitizeDiagnostic(String(e))}`);
|
|
2138
2457
|
return undefined;
|
|
2139
2458
|
}
|
|
2140
2459
|
}
|
|
@@ -2664,8 +2983,129 @@ export class TelegramNotificationDaemon {
|
|
|
2664
2983
|
this.runtime.stopInterval("telegram-typing");
|
|
2665
2984
|
}
|
|
2666
2985
|
|
|
2986
|
+
/** Render successful `/model` lists as session-bound, one-shot inline choices. */
|
|
2987
|
+
async #renderModelChoices(session: SessionSocket, msg: Record<string, unknown>): Promise<boolean> {
|
|
2988
|
+
const logicalSessionId = this.#logicalSessionId(session);
|
|
2989
|
+
if (
|
|
2990
|
+
msg.type !== "control_command_result" ||
|
|
2991
|
+
msg.status !== "ok" ||
|
|
2992
|
+
msg.sessionId !== logicalSessionId ||
|
|
2993
|
+
!Array.isArray(msg.modelChoices) ||
|
|
2994
|
+
this.sessions.get(session.sessionId) !== session
|
|
2995
|
+
)
|
|
2996
|
+
return false;
|
|
2997
|
+
|
|
2998
|
+
const choices: RenderedModelChoice[] = [];
|
|
2999
|
+
for (const choice of msg.modelChoices) {
|
|
3000
|
+
if (!choice || typeof choice !== "object") continue;
|
|
3001
|
+
const { selector, label } = choice as { selector?: unknown; label?: unknown };
|
|
3002
|
+
const safeLabel = safeModelButtonLabel(label);
|
|
3003
|
+
if (typeof selector !== "string" || !selector.trim() || !safeLabel) continue;
|
|
3004
|
+
choices.push({ selector, label: safeLabel });
|
|
3005
|
+
}
|
|
3006
|
+
if (choices.length === 0) return false;
|
|
3007
|
+
|
|
3008
|
+
const rendered = renderThreadedFrame({ ...msg, type: "control_command_result" });
|
|
3009
|
+
if (!rendered?.text) return false;
|
|
3010
|
+
const topicId =
|
|
3011
|
+
(await this.existingTopicForPrivateChat(session.sessionId)) ??
|
|
3012
|
+
(await this.ensureTopic(session.sessionId, this.topicNameFor(session.sessionId, msg)));
|
|
3013
|
+
if (!topicId) return false;
|
|
3014
|
+
|
|
3015
|
+
// Each logical session owns only its most recently rendered menu.
|
|
3016
|
+
this.#clearModelChoiceAliases(logicalSessionId);
|
|
3017
|
+
const aliases = choices.map(choice =>
|
|
3018
|
+
this.#putModelChoiceAlias({ session, sessionId: logicalSessionId, selector: choice.selector }),
|
|
3019
|
+
);
|
|
3020
|
+
const inline_keyboard = buildButtonGrid(
|
|
3021
|
+
choices.map(choice => choice.label),
|
|
3022
|
+
index => aliases[index]!,
|
|
3023
|
+
);
|
|
3024
|
+
try {
|
|
3025
|
+
const response = await this.botApi.call("sendMessage", {
|
|
3026
|
+
chat_id: this.opts.chatId,
|
|
3027
|
+
message_thread_id: Number(topicId),
|
|
3028
|
+
text: rendered.text,
|
|
3029
|
+
parse_mode: TELEGRAM_PARSE_MODE,
|
|
3030
|
+
reply_markup: { inline_keyboard },
|
|
3031
|
+
});
|
|
3032
|
+
if (response && typeof response === "object" && (response as { ok?: unknown }).ok === false) {
|
|
3033
|
+
for (const alias of aliases) this.#modelChoiceAliases.delete(alias);
|
|
3034
|
+
logger.warn("notifications: failed to send model selection keyboard");
|
|
3035
|
+
return false;
|
|
3036
|
+
}
|
|
3037
|
+
} catch {
|
|
3038
|
+
for (const alias of aliases) this.#modelChoiceAliases.delete(alias);
|
|
3039
|
+
logger.warn("notifications: failed to send model selection keyboard");
|
|
3040
|
+
return false;
|
|
3041
|
+
}
|
|
3042
|
+
return true;
|
|
3043
|
+
}
|
|
3044
|
+
|
|
2667
3045
|
async handleSessionMessage(session: SessionSocket, msg: any): Promise<void> {
|
|
3046
|
+
if (session.replayPending) {
|
|
3047
|
+
const matchingReplay = msg?.type === "event_replay_result" && msg.id === session.replayId;
|
|
3048
|
+
if (!matchingReplay) {
|
|
3049
|
+
session.replayQueue.push(msg as Record<string, unknown>);
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
session.replayPending = false;
|
|
3053
|
+
const replayValid =
|
|
3054
|
+
Number.isSafeInteger(msg.generation) &&
|
|
3055
|
+
msg.generation >= 1 &&
|
|
3056
|
+
Number.isSafeInteger(msg.lastSeq) &&
|
|
3057
|
+
msg.lastSeq >= 0 &&
|
|
3058
|
+
Array.isArray(msg.events);
|
|
3059
|
+
const replayed: Record<string, unknown>[] = replayValid
|
|
3060
|
+
? (msg.events as unknown[]).flatMap((event: unknown): Record<string, unknown>[] => {
|
|
3061
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) return [];
|
|
3062
|
+
const envelope = event as Record<string, unknown>;
|
|
3063
|
+
const payload = envelope.payload;
|
|
3064
|
+
return [
|
|
3065
|
+
payload && typeof payload === "object" && !Array.isArray(payload)
|
|
3066
|
+
? (payload as Record<string, unknown>)
|
|
3067
|
+
: envelope,
|
|
3068
|
+
];
|
|
3069
|
+
})
|
|
3070
|
+
: [];
|
|
3071
|
+
// Replay restores durable attachment state only. Live notification effects
|
|
3072
|
+
// (turn streams, context updates, lifecycle messages) may already have been
|
|
3073
|
+
// delivered before a reconnect and must never be rendered a second time.
|
|
3074
|
+
const identityIndex = replayed.findLastIndex(frame => frame.type === "identity_header");
|
|
3075
|
+
const currentGeneration = identityIndex < 0 ? replayed : replayed.slice(identityIndex);
|
|
3076
|
+
const latestIdentity = identityIndex < 0 ? undefined : replayed[identityIndex];
|
|
3077
|
+
const latestActions = new Map<string, Record<string, unknown>>();
|
|
3078
|
+
for (const frame of currentGeneration) {
|
|
3079
|
+
if ((frame.type === "action_needed" || frame.type === "action_resolved") && typeof frame.id === "string")
|
|
3080
|
+
latestActions.set(frame.id, frame);
|
|
3081
|
+
}
|
|
3082
|
+
const replayState = [...(latestIdentity ? [latestIdentity] : []), ...latestActions.values()];
|
|
3083
|
+
const replayCounts = new Map<string, number>();
|
|
3084
|
+
for (const frame of replayState) {
|
|
3085
|
+
const fingerprint = JSON.stringify(frame);
|
|
3086
|
+
replayCounts.set(fingerprint, (replayCounts.get(fingerprint) ?? 0) + 1);
|
|
3087
|
+
await this.handleSessionMessage(session, frame);
|
|
3088
|
+
}
|
|
3089
|
+
const queued = session.replayQueue.splice(0);
|
|
3090
|
+
for (const frame of queued) {
|
|
3091
|
+
const fingerprint = JSON.stringify(frame);
|
|
3092
|
+
const remaining = replayCounts.get(fingerprint) ?? 0;
|
|
3093
|
+
if (remaining > 0) {
|
|
3094
|
+
if (remaining === 1) replayCounts.delete(fingerprint);
|
|
3095
|
+
else replayCounts.set(fingerprint, remaining - 1);
|
|
3096
|
+
continue;
|
|
3097
|
+
}
|
|
3098
|
+
await this.handleSessionMessage(session, frame);
|
|
3099
|
+
}
|
|
3100
|
+
if (replayValid && this.topics.markReplayCursor(session.sessionId, msg.generation, msg.lastSeq))
|
|
3101
|
+
await this.persistTopics();
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
if (msg?.type === "event_replay_result") return;
|
|
3105
|
+
if (msg && typeof msg === "object") this.#updateLogicalSessionForThreadedFrame(session, msg);
|
|
2668
3106
|
if (await this.sessionRouter.dispatch(session, msg as Record<string, unknown>)) return;
|
|
3107
|
+
if (await this.#renderModelChoices(session, msg as Record<string, unknown>)) return;
|
|
3108
|
+
|
|
2669
3109
|
if (typeof msg?.type === "string" && TelegramNotificationDaemon.THREADED_FRAMES.has(msg.type)) {
|
|
2670
3110
|
const send = renderThreadedFrame(msg);
|
|
2671
3111
|
if (!send) return;
|
|
@@ -2864,6 +3304,80 @@ export class TelegramNotificationDaemon {
|
|
|
2864
3304
|
}
|
|
2865
3305
|
}
|
|
2866
3306
|
|
|
3307
|
+
/** Claim and forward one session-bound model alias before acknowledging its callback. */
|
|
3308
|
+
async #handleModelChoiceCallback(update: unknown, callbackId: unknown): Promise<boolean> {
|
|
3309
|
+
const callback = (
|
|
3310
|
+
update as {
|
|
3311
|
+
callback_query?: { data?: unknown; message?: { chat?: { id?: unknown } } };
|
|
3312
|
+
}
|
|
3313
|
+
).callback_query;
|
|
3314
|
+
const alias = callback?.data;
|
|
3315
|
+
if (typeof alias !== "string" || !alias.startsWith(MODEL_CALLBACK_PREFIX)) return false;
|
|
3316
|
+
this.#sweepExpiredModelChoiceAliases();
|
|
3317
|
+
if (String(callback?.message?.chat?.id) !== String(this.opts.chatId)) {
|
|
3318
|
+
await this.answerCallbackQueryBestEffort(callbackId, "Not authorized");
|
|
3319
|
+
return true;
|
|
3320
|
+
}
|
|
3321
|
+
if (!(await this.pairedChatIsPrivate())) {
|
|
3322
|
+
await this.answerCallbackQueryBestEffort(callbackId, "Not authorized");
|
|
3323
|
+
return true;
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
const route = this.#modelChoiceAliases.get(alias);
|
|
3327
|
+
if (!route) {
|
|
3328
|
+
await this.#sendModelStaleGuidance(callbackId);
|
|
3329
|
+
return true;
|
|
3330
|
+
}
|
|
3331
|
+
const session = route.session;
|
|
3332
|
+
if (
|
|
3333
|
+
this.sessions.get(session.sessionId) !== session ||
|
|
3334
|
+
this.#logicalSessionId(session) !== route.sessionId ||
|
|
3335
|
+
session.ws.readyState !== WebSocket.OPEN
|
|
3336
|
+
) {
|
|
3337
|
+
this.#modelChoiceAliases.delete(alias);
|
|
3338
|
+
await this.#sendModelStaleGuidance(callbackId);
|
|
3339
|
+
return true;
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
// Delete before send so a duplicate tap can only become stale, never duplicate a control command.
|
|
3343
|
+
this.#modelChoiceAliases.delete(alias);
|
|
3344
|
+
const updateId = (update as { update_id?: unknown }).update_id;
|
|
3345
|
+
const safeUpdateId =
|
|
3346
|
+
typeof updateId === "number" && Number.isSafeInteger(updateId) && updateId >= 0 ? updateId : undefined;
|
|
3347
|
+
try {
|
|
3348
|
+
session.ws.send(
|
|
3349
|
+
JSON.stringify({
|
|
3350
|
+
type: "control_command",
|
|
3351
|
+
sessionId: route.sessionId,
|
|
3352
|
+
token: session.token,
|
|
3353
|
+
requestId: safeUpdateId === undefined ? `tg:model:${alias}` : `tg:model:${safeUpdateId}`,
|
|
3354
|
+
...(safeUpdateId === undefined ? {} : { updateId: safeUpdateId }),
|
|
3355
|
+
command: { name: "model", action: "set", selector: route.selector },
|
|
3356
|
+
}),
|
|
3357
|
+
);
|
|
3358
|
+
} catch {
|
|
3359
|
+
await this.#sendModelStaleGuidance(callbackId);
|
|
3360
|
+
return true;
|
|
3361
|
+
}
|
|
3362
|
+
await this.answerCallbackQueryBestEffort(callbackId);
|
|
3363
|
+
return true;
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
async #sendModelStaleGuidance(callbackId: unknown): Promise<void> {
|
|
3367
|
+
const text = "Button is stale. Run /model again.";
|
|
3368
|
+
await this.answerCallbackQueryBestEffort(callbackId, text);
|
|
3369
|
+
if (!(await this.pairedChatIsPrivate())) return;
|
|
3370
|
+
try {
|
|
3371
|
+
await this.botApi.call("sendMessage", {
|
|
3372
|
+
chat_id: this.opts.chatId,
|
|
3373
|
+
text,
|
|
3374
|
+
parse_mode: TELEGRAM_PARSE_MODE,
|
|
3375
|
+
});
|
|
3376
|
+
} catch {
|
|
3377
|
+
// Best-effort stale guidance must not reject a callback.
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
|
|
2867
3381
|
private async sendStaleGuidance(callbackId: unknown): Promise<void> {
|
|
2868
3382
|
await this.answerCallbackQueryBestEffort(callbackId, "Button is stale");
|
|
2869
3383
|
if (!(await this.pairedChatIsPrivate())) return;
|
|
@@ -3040,7 +3554,7 @@ export class TelegramNotificationDaemon {
|
|
|
3040
3554
|
// update_id dedupe are all enforced by decideThreadedInbound.
|
|
3041
3555
|
const raw = update as {
|
|
3042
3556
|
callback_query?: unknown;
|
|
3043
|
-
message?: { reply_to_message?: { message_id?: unknown } };
|
|
3557
|
+
message?: { text?: unknown; reply_to_message?: { message_id?: unknown } };
|
|
3044
3558
|
};
|
|
3045
3559
|
// A reply to a known ask message routes to that ask (below). Any OTHER
|
|
3046
3560
|
// message in a topic (plain text, or a reply to a non-ask message) is a
|
|
@@ -3048,7 +3562,12 @@ export class TelegramNotificationDaemon {
|
|
|
3048
3562
|
const replyTo = raw.message?.reply_to_message?.message_id;
|
|
3049
3563
|
const isAskReply =
|
|
3050
3564
|
replyTo !== undefined && (this.messageRoutes.has(String(replyTo)) || this.messageRoutes.has(Number(replyTo)));
|
|
3051
|
-
|
|
3565
|
+
const directControl =
|
|
3566
|
+
typeof raw.message?.text === "string"
|
|
3567
|
+
? parseTelegramControlCommand(raw.message.text, this.botUsername)
|
|
3568
|
+
: ({ kind: "none" } as const);
|
|
3569
|
+
const interceptAskControl = isAskReply && directControl.kind !== "none";
|
|
3570
|
+
if (!raw.callback_query && (!isAskReply || interceptAskControl)) {
|
|
3052
3571
|
const inbound = decideThreadedInbound(update as never, {
|
|
3053
3572
|
pairedChatId: this.opts.chatId,
|
|
3054
3573
|
topicToSession: t => this.topics.sessionForTopic(t),
|
|
@@ -3056,7 +3575,27 @@ export class TelegramNotificationDaemon {
|
|
|
3056
3575
|
});
|
|
3057
3576
|
if (inbound.kind === "duplicate") return;
|
|
3058
3577
|
if (inbound.kind === "inject") {
|
|
3578
|
+
if (!(await this.pairedChatIsPrivate())) return;
|
|
3579
|
+
const preliminaryControl = inbound.attachment
|
|
3580
|
+
? { kind: "none" as const }
|
|
3581
|
+
: parseTelegramControlCommand(inbound.text, this.botUsername);
|
|
3582
|
+
if (preliminaryControl.kind === "ignored") return;
|
|
3059
3583
|
const session = this.sessions.get(inbound.sessionId);
|
|
3584
|
+
if (preliminaryControl.kind === "invalid" && session?.ws.readyState !== WebSocket.OPEN) return;
|
|
3585
|
+
if (preliminaryControl.kind === "command" && session?.ws.readyState !== WebSocket.OPEN) {
|
|
3586
|
+
if (await this.rememberSeenUpdateIdForUnavailableNotice(inbound.updateId)) {
|
|
3587
|
+
try {
|
|
3588
|
+
await this.botApi.call("sendMessage", {
|
|
3589
|
+
chat_id: this.opts.chatId,
|
|
3590
|
+
message_thread_id: Number(inbound.threadId),
|
|
3591
|
+
text: "Session control unavailable: this local GJC session is disconnected.",
|
|
3592
|
+
});
|
|
3593
|
+
} catch {
|
|
3594
|
+
logger.warn("notifications: unavailable-control notice delivery failed");
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
return;
|
|
3598
|
+
}
|
|
3060
3599
|
if (session?.ws.readyState === WebSocket.OPEN) {
|
|
3061
3600
|
const attachmentResult = inbound.attachment
|
|
3062
3601
|
? await this.resolveInboundAttachment(inbound.attachment, inbound.sessionId)
|
|
@@ -3075,9 +3614,7 @@ export class TelegramNotificationDaemon {
|
|
|
3075
3614
|
const injectedText = repliedOriginal
|
|
3076
3615
|
? `> replied-to message:\n${repliedOriginal}\n\n${baseInjectedText}`
|
|
3077
3616
|
: baseInjectedText;
|
|
3078
|
-
const control = hasMedia
|
|
3079
|
-
? { kind: "none" as const }
|
|
3080
|
-
: parseTelegramControlCommand(inbound.text, this.botUsername);
|
|
3617
|
+
const control = hasMedia ? { kind: "none" as const } : preliminaryControl;
|
|
3081
3618
|
if (control.kind !== "none") {
|
|
3082
3619
|
await this.rememberSeenUpdateId(inbound.updateId);
|
|
3083
3620
|
const sendControlNotice = async (body: string): Promise<void> => {
|
|
@@ -3092,7 +3629,6 @@ export class TelegramNotificationDaemon {
|
|
|
3092
3629
|
// Best-effort control feedback; never convert to user input.
|
|
3093
3630
|
}
|
|
3094
3631
|
};
|
|
3095
|
-
if (control.kind === "ignored") return;
|
|
3096
3632
|
if (control.kind === "invalid") {
|
|
3097
3633
|
await sendControlNotice(control.usage);
|
|
3098
3634
|
return;
|
|
@@ -3104,7 +3640,7 @@ export class TelegramNotificationDaemon {
|
|
|
3104
3640
|
session.ws.send(
|
|
3105
3641
|
JSON.stringify({
|
|
3106
3642
|
type: "control_command",
|
|
3107
|
-
sessionId:
|
|
3643
|
+
sessionId: this.#logicalSessionId(session),
|
|
3108
3644
|
token: session.token,
|
|
3109
3645
|
requestId: `tg:${inbound.updateId}`,
|
|
3110
3646
|
updateId: inbound.updateId,
|
|
@@ -3161,6 +3697,7 @@ export class TelegramNotificationDaemon {
|
|
|
3161
3697
|
}
|
|
3162
3698
|
}
|
|
3163
3699
|
const callbackId = (update as { callback_query?: { id?: unknown } }).callback_query?.id;
|
|
3700
|
+
if (await this.#handleModelChoiceCallback(update, callbackId)) return;
|
|
3164
3701
|
const decision = routeInboundUpdate(update, {
|
|
3165
3702
|
aliasTable: this.aliasTable,
|
|
3166
3703
|
messageRoutes: this.messageRoutes,
|
|
@@ -3196,6 +3733,7 @@ export class TelegramNotificationDaemon {
|
|
|
3196
3733
|
{ command: "rich", description: "Toggle rich Telegram delivery: /rich <on|off>" },
|
|
3197
3734
|
{ command: "reasoning", description: "Show or change reasoning effort in this session" },
|
|
3198
3735
|
{ command: "usage", description: "Show provider/local usage for this session" },
|
|
3736
|
+
{ command: "model", description: "Select a model for this session" },
|
|
3199
3737
|
{ command: "context", description: "Show current context usage for this session" },
|
|
3200
3738
|
{ command: "compact", description: "Compact this session: /compact [instructions]" },
|
|
3201
3739
|
{ command: "session_create", description: "Create a GJC session: path, worktree, or dir [--mpreset]" },
|
|
@@ -3268,7 +3806,9 @@ export class TelegramNotificationDaemon {
|
|
|
3268
3806
|
// Back off (bounded, below the heartbeat TTL) and keep renewing
|
|
3269
3807
|
// ownership at the loop top.
|
|
3270
3808
|
const backoffMs = this.loopBackoff.next();
|
|
3271
|
-
logger.warn(
|
|
3809
|
+
logger.warn(
|
|
3810
|
+
`notifications: getUpdates failed, backing off ${backoffMs}ms: ${sanitizeDiagnostic(String(e))}`,
|
|
3811
|
+
);
|
|
3272
3812
|
await this.runtime.sleep(backoffMs);
|
|
3273
3813
|
continue;
|
|
3274
3814
|
} finally {
|