pi-harness-runtime 1.1.398 → 1.1.402
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/AGENTS.md +44 -2
- package/CHANGELOG.md +21 -0
- package/RULES.md +28 -0
- package/harness/agent-handoff.ts +2 -3
- package/harness/autonomous-review.ts +1 -1
- package/harness/blackboard.ts +2 -3
- package/harness/context-compact-orchestrator.ts +1 -1
- package/harness/context-window-manager.ts +2 -2
- package/harness/e2e/playwright-runner.ts +6 -4
- package/harness/herdr-agents.ts +1 -1
- package/harness/job-state-machine.ts +1 -1
- package/harness/langchain/daemon.ts +3 -3
- package/harness/langchain/graph.ts +2 -2
- package/harness/langchain/ping-pong-middleware.ts +2 -2
- package/harness/loop-code-agent.ts +6 -4
- package/harness/loop-controller.ts +1 -1
- package/harness/loop-review-agent.ts +1 -1
- package/harness/loop-types.ts +1 -1
- package/harness/master-planner.ts +1 -1
- package/harness/project-detector/detector.ts +1 -2
- package/harness/repair-engine.ts +2 -4
- package/harness/service-diagnostics.ts +1 -1
- package/harness/skill-sync.ts +3 -3
- package/index.ts +276 -55
- package/mirror.ts +1 -1
- package/package.json +5 -3
- package/packages/a2a-adapter/dist/discovery.d.ts.map +1 -1
- package/packages/a2a-adapter/dist/discovery.js +0 -1
- package/packages/a2a-adapter/dist/discovery.js.map +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/a2a-adapter/src/discovery.ts +0 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/dist/index.d.ts +9 -0
- package/packages/auth/dist/index.d.ts.map +1 -0
- package/packages/auth/dist/index.js +9 -0
- package/packages/auth/dist/index.js.map +1 -0
- package/packages/auth/package.json +1 -1
- package/packages/auth/src/index.ts +9 -0
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/dist/index.d.ts +52 -0
- package/packages/config-capture/dist/index.d.ts.map +1 -0
- package/packages/config-capture/dist/index.js +261 -0
- package/packages/config-capture/dist/index.js.map +1 -0
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/dist/extension.d.ts +15 -0
- package/packages/file-copy-helper/dist/extension.d.ts.map +1 -0
- package/packages/file-copy-helper/dist/extension.debug.d.ts +15 -0
- package/packages/file-copy-helper/dist/extension.debug.d.ts.map +1 -0
- package/packages/file-copy-helper/dist/extension.debug.js +215 -0
- package/packages/file-copy-helper/dist/extension.debug.js.map +1 -0
- package/packages/file-copy-helper/dist/extension.js +222 -0
- package/packages/file-copy-helper/dist/extension.js.map +1 -0
- package/packages/file-copy-helper/dist/index.d.ts +3 -0
- package/packages/file-copy-helper/dist/index.d.ts.map +1 -0
- package/packages/file-copy-helper/dist/index.js +3 -0
- package/packages/file-copy-helper/dist/index.js.map +1 -0
- package/packages/file-copy-helper/dist/intent-detector.d.ts +21 -0
- package/packages/file-copy-helper/dist/intent-detector.d.ts.map +1 -0
- package/packages/file-copy-helper/dist/intent-detector.js +348 -0
- package/packages/file-copy-helper/dist/intent-detector.js.map +1 -0
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/dist/auto-continue.d.ts +161 -2
- package/packages/jev-judge/dist/auto-continue.d.ts.map +1 -1
- package/packages/jev-judge/dist/auto-continue.js +267 -22
- package/packages/jev-judge/dist/auto-continue.js.map +1 -1
- package/packages/jev-judge/dist/index.d.ts +2 -0
- package/packages/jev-judge/dist/index.d.ts.map +1 -1
- package/packages/jev-judge/dist/index.js +2 -0
- package/packages/jev-judge/dist/index.js.map +1 -1
- package/packages/jev-judge/dist/question-risk.d.ts +50 -0
- package/packages/jev-judge/dist/question-risk.d.ts.map +1 -0
- package/packages/jev-judge/dist/question-risk.js +100 -0
- package/packages/jev-judge/dist/question-risk.js.map +1 -0
- package/packages/jev-judge/package.json +1 -1
- package/packages/jev-judge/src/auto-continue.ts +426 -29
- package/packages/jev-judge/src/index.ts +4 -0
- package/packages/jev-judge/src/question-risk.ts +151 -0
- package/packages/jev-judge/tests/auto-continue-context.test.ts +221 -0
- package/packages/jev-judge/tests/outcome-application.test.ts +132 -0
- package/packages/jev-judge/tests/question-risk.test.ts +116 -0
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/logging/README.md +36 -3
- package/packages/logging/dist/config.d.ts +7 -0
- package/packages/logging/dist/config.d.ts.map +1 -1
- package/packages/logging/dist/config.js +14 -0
- package/packages/logging/dist/config.js.map +1 -1
- package/packages/logging/dist/incidents/schema.d.ts +111 -0
- package/packages/logging/dist/incidents/schema.d.ts.map +1 -0
- package/packages/logging/dist/incidents/schema.js +0 -0
- package/packages/logging/dist/incidents/schema.js.map +1 -0
- package/packages/logging/dist/index.d.ts +2 -0
- package/packages/logging/dist/index.d.ts.map +1 -1
- package/packages/logging/dist/index.js +2 -0
- package/packages/logging/dist/index.js.map +1 -1
- package/packages/logging/dist/server/http.d.ts +4 -3
- package/packages/logging/dist/server/http.d.ts.map +1 -1
- package/packages/logging/dist/server/http.js +35 -3
- package/packages/logging/dist/server/http.js.map +1 -1
- package/packages/logging/dist/server/incident-qdrant.d.ts +83 -0
- package/packages/logging/dist/server/incident-qdrant.d.ts.map +1 -0
- package/packages/logging/dist/server/incident-qdrant.js +129 -0
- package/packages/logging/dist/server/incident-qdrant.js.map +1 -0
- package/packages/logging/dist/server/incident-store.d.ts +76 -0
- package/packages/logging/dist/server/incident-store.d.ts.map +1 -0
- package/packages/logging/dist/server/incident-store.js +157 -0
- package/packages/logging/dist/server/incident-store.js.map +1 -0
- package/packages/logging/dist/server/index.d.ts +4 -0
- package/packages/logging/dist/server/index.d.ts.map +1 -1
- package/packages/logging/dist/server/index.js +2 -0
- package/packages/logging/dist/server/index.js.map +1 -1
- package/packages/logging/dist/server/service.d.ts +33 -0
- package/packages/logging/dist/server/service.d.ts.map +1 -1
- package/packages/logging/dist/server/service.js +63 -0
- package/packages/logging/dist/server/service.js.map +1 -1
- package/packages/logging/dist/shipper.d.ts +74 -3
- package/packages/logging/dist/shipper.d.ts.map +1 -1
- package/packages/logging/dist/shipper.js +88 -5
- package/packages/logging/dist/shipper.js.map +1 -1
- package/packages/logging/dist/singleton.d.ts.map +1 -1
- package/packages/logging/dist/singleton.js +1 -0
- package/packages/logging/dist/singleton.js.map +1 -1
- package/packages/logging/package.json +1 -1
- package/packages/logging/src/config.ts +22 -0
- package/packages/logging/src/incidents/schema.ts +0 -0
- package/packages/logging/src/index.ts +17 -0
- package/packages/logging/src/server/http.ts +42 -3
- package/packages/logging/src/server/incident-qdrant.ts +177 -0
- package/packages/logging/src/server/incident-store.ts +192 -0
- package/packages/logging/src/server/index.ts +21 -0
- package/packages/logging/src/server/service.ts +79 -0
- package/packages/logging/src/shipper.ts +120 -5
- package/packages/logging/src/singleton.ts +1 -0
- package/packages/logging/test/fanout.test.ts +186 -0
- package/packages/logging/test/incidents.test.ts +404 -0
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/dist/adapters/detail-format.d.ts +10 -2
- package/packages/notification/dist/adapters/detail-format.d.ts.map +1 -1
- package/packages/notification/dist/adapters/detail-format.js +39 -0
- package/packages/notification/dist/adapters/detail-format.js.map +1 -1
- package/packages/notification/dist/adapters/line-adapter.d.ts +33 -0
- package/packages/notification/dist/adapters/line-adapter.d.ts.map +1 -1
- package/packages/notification/dist/adapters/line-adapter.js +84 -7
- package/packages/notification/dist/adapters/line-adapter.js.map +1 -1
- package/packages/notification/dist/adapters/telegram-adapter.d.ts.map +1 -1
- package/packages/notification/dist/adapters/telegram-adapter.js +6 -2
- package/packages/notification/dist/adapters/telegram-adapter.js.map +1 -1
- package/packages/notification/dist/notification-center.d.ts.map +1 -1
- package/packages/notification/dist/notification-center.js +59 -7
- package/packages/notification/dist/notification-center.js.map +1 -1
- package/packages/notification/dist/pending-question.d.ts +69 -2
- package/packages/notification/dist/pending-question.d.ts.map +1 -1
- package/packages/notification/dist/pending-question.js +88 -2
- package/packages/notification/dist/pending-question.js.map +1 -1
- package/packages/notification/dist/types.d.ts +18 -0
- package/packages/notification/dist/types.d.ts.map +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/notification/src/adapters/detail-format.ts +42 -2
- package/packages/notification/src/adapters/line-adapter.ts +94 -7
- package/packages/notification/src/adapters/telegram-adapter.ts +6 -2
- package/packages/notification/src/notification-center.ts +65 -8
- package/packages/notification/src/pending-question.ts +160 -8
- package/packages/notification/src/types.ts +18 -0
- package/packages/notification/test/approval-payload.test.ts +269 -0
- package/packages/notification/test/pending-question-lifecycle.test.ts +195 -0
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/dist/index.d.ts +9 -0
- package/packages/okf-indexer/dist/index.d.ts.map +1 -0
- package/packages/okf-indexer/dist/index.js +9 -0
- package/packages/okf-indexer/dist/index.js.map +1 -0
- package/packages/okf-indexer/package.json +1 -1
- package/packages/okf-indexer/src/index.ts +9 -0
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/dist/provider-router.d.ts.map +1 -1
- package/packages/provider-router/dist/types.d.ts +2 -2
- package/packages/provider-router/dist/types.d.ts.map +1 -1
- package/packages/provider-router/dist/types.js +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-router/src/types.ts +2 -2
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/dist/index.d.ts +9 -0
- package/packages/scheduler/dist/index.d.ts.map +1 -0
- package/packages/scheduler/dist/index.js +9 -0
- package/packages/scheduler/dist/index.js.map +1 -0
- package/packages/scheduler/dist/scheduler.d.ts +17 -0
- package/packages/scheduler/dist/scheduler.d.ts.map +1 -0
- package/packages/scheduler/dist/scheduler.js +41 -0
- package/packages/scheduler/dist/scheduler.js.map +1 -0
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler/src/index.ts +9 -0
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/dist/index.d.ts +7 -0
- package/packages/shared-context/dist/index.d.ts.map +1 -0
- package/packages/shared-context/dist/index.js +7 -0
- package/packages/shared-context/dist/index.js.map +1 -0
- package/packages/shared-context/dist/shared-context.d.ts +12 -0
- package/packages/shared-context/dist/shared-context.d.ts.map +1 -0
- package/packages/shared-context/dist/shared-context.js +39 -0
- package/packages/shared-context/dist/shared-context.js.map +1 -0
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/dist/client.d.ts +56 -0
- package/packages/skill-mcp-client/dist/client.d.ts.map +1 -0
- package/packages/skill-mcp-client/dist/client.js +101 -0
- package/packages/skill-mcp-client/dist/client.js.map +1 -0
- package/packages/skill-mcp-client/dist/errors.d.ts +44 -0
- package/packages/skill-mcp-client/dist/errors.d.ts.map +1 -0
- package/packages/skill-mcp-client/dist/errors.js +68 -0
- package/packages/skill-mcp-client/dist/errors.js.map +1 -0
- package/packages/skill-mcp-client/dist/index.d.ts +9 -0
- package/packages/skill-mcp-client/dist/index.d.ts.map +1 -0
- package/packages/skill-mcp-client/dist/index.js +9 -0
- package/packages/skill-mcp-client/dist/index.js.map +1 -0
- package/packages/skill-mcp-client/dist/types.d.ts +106 -0
- package/packages/skill-mcp-client/dist/types.d.ts.map +1 -0
- package/packages/skill-mcp-client/dist/types.js +14 -0
- package/packages/skill-mcp-client/dist/types.js.map +1 -0
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -5
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/telegram/src/agent-messages.d.ts +28 -0
- package/packages/telegram/src/agent-messages.js +90 -0
- package/packages/telegram/src/bindings.d.ts +296 -0
- package/packages/telegram/src/bindings.js +1086 -0
- package/packages/telegram/src/bus.d.ts +759 -0
- package/packages/telegram/src/bus.js +2732 -0
- package/packages/telegram/src/commands.d.ts +761 -0
- package/packages/telegram/src/commands.js +2622 -0
- package/packages/telegram/src/config.d.ts +222 -0
- package/packages/telegram/src/config.js +844 -0
- package/packages/telegram/src/delivery.d.ts +161 -0
- package/packages/telegram/src/delivery.js +550 -0
- package/packages/telegram/src/extension.d.ts +7 -0
- package/packages/telegram/src/extension.js +2444 -0
- package/packages/telegram/src/inbound.d.ts +94 -0
- package/packages/telegram/src/inbound.js +478 -0
- package/packages/telegram/src/index.d.ts +6 -0
- package/packages/telegram/src/index.js +6 -0
- package/packages/telegram/src/keyboard.d.ts +21 -0
- package/packages/telegram/src/keyboard.js +37 -0
- package/packages/telegram/src/lifecycle.d.ts +210 -0
- package/packages/telegram/src/lifecycle.js +538 -0
- package/packages/telegram/src/pi.d.ts +76 -0
- package/packages/telegram/src/pi.js +130 -0
- package/packages/telegram/src/polling.d.ts +332 -0
- package/packages/telegram/src/polling.js +1219 -0
- package/packages/telegram/src/replies.d.ts +210 -0
- package/packages/telegram/src/replies.js +752 -0
- package/packages/telegram/src/routing.d.ts +388 -0
- package/packages/telegram/src/routing.js +7886 -0
- package/packages/telegram/src/runtime.d.ts +159 -0
- package/packages/telegram/src/runtime.js +351 -0
- package/packages/telegram/src/telegram-api.d.ts +564 -0
- package/packages/telegram/src/telegram-api.js +1332 -0
- package/packages/telegram/src/turns.d.ts +108 -0
- package/packages/telegram/src/turns.js +482 -0
- package/packages/telegram/src/updates.d.ts +1545 -0
- package/packages/telegram/src/updates.js +6420 -0
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/dist/detector.d.ts +54 -0
- package/packages/todo-bd-sync/dist/detector.d.ts.map +1 -0
- package/packages/todo-bd-sync/dist/detector.js +132 -0
- package/packages/todo-bd-sync/dist/detector.js.map +1 -0
- package/packages/todo-bd-sync/dist/extension.d.ts +30 -0
- package/packages/todo-bd-sync/dist/extension.d.ts.map +1 -0
- package/packages/todo-bd-sync/dist/extension.js +35 -0
- package/packages/todo-bd-sync/dist/extension.js.map +1 -0
- package/packages/todo-bd-sync/dist/index.d.ts +87 -0
- package/packages/todo-bd-sync/dist/index.d.ts.map +1 -0
- package/packages/todo-bd-sync/dist/index.js +296 -0
- package/packages/todo-bd-sync/dist/index.js.map +1 -0
- package/packages/todo-bd-sync/dist/sync.d.ts +72 -0
- package/packages/todo-bd-sync/dist/sync.d.ts.map +1 -0
- package/packages/todo-bd-sync/dist/sync.js +290 -0
- package/packages/todo-bd-sync/dist/sync.js.map +1 -0
- package/packages/todo-bd-sync/dist/task-analyzer.d.ts +49 -0
- package/packages/todo-bd-sync/dist/task-analyzer.d.ts.map +1 -0
- package/packages/todo-bd-sync/dist/task-analyzer.js +240 -0
- package/packages/todo-bd-sync/dist/task-analyzer.js.map +1 -0
- package/packages/todo-bd-sync/dist/todo-reminder.d.ts +126 -0
- package/packages/todo-bd-sync/dist/todo-reminder.d.ts.map +1 -0
- package/packages/todo-bd-sync/dist/todo-reminder.js +329 -0
- package/packages/todo-bd-sync/dist/todo-reminder.js.map +1 -0
- package/packages/todo-bd-sync/dist/types.d.ts +65 -0
- package/packages/todo-bd-sync/dist/types.d.ts.map +1 -0
- package/packages/todo-bd-sync/dist/types.js +67 -0
- package/packages/todo-bd-sync/dist/types.js.map +1 -0
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/dist/index.d.ts +94 -0
- package/packages/token-estimation/dist/index.d.ts.map +1 -0
- package/packages/token-estimation/dist/index.js +115 -0
- package/packages/token-estimation/dist/index.js.map +1 -0
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/dist/ai-providers.d.ts +36 -0
- package/packages/types/dist/ai-providers.d.ts.map +1 -0
- package/packages/types/dist/ai-providers.js +66 -0
- package/packages/types/dist/ai-providers.js.map +1 -0
- package/packages/types/dist/index.d.ts +12 -0
- package/packages/types/dist/index.d.ts.map +1 -0
- package/packages/types/dist/index.js +12 -0
- package/packages/types/dist/index.js.map +1 -0
- package/packages/types/package.json +5 -5
- package/packages/types/src/index.ts +12 -0
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/worktree/worktree.ts +1 -1
- package/packages/write-review/package.json +1 -1
- package/renderer.ts +1 -1
- package/scripts/build-packages.ts +162 -0
- package/scripts/relay-client.ts +2 -2
- package/scripts/typecheck-packages.mjs +3 -3
- package/scripts/unified-webhook-server.ts +22 -10
- package/scripts/verify-package-contents.ts +345 -0
- package/scripts/ws-server.ts +1 -1
- package/tracker.ts +1 -1
- package/harness/browser-scrapers/base-browser-scraper.d.ts +0 -91
- package/harness/browser-scrapers/chrome-profile-manager.d.ts +0 -80
- package/harness/browser-scrapers/codex-browser-scraper.d.ts +0 -60
- package/harness/browser-scrapers/index.d.ts +0 -29
- package/harness/browser-scrapers/zai-browser-scraper.d.ts +0 -79
- package/harness/e2e/browser-scrapers/base-browser-scraper.d.ts +0 -92
- package/harness/e2e/browser-scrapers/chrome-profile-manager.d.ts +0 -81
- package/harness/e2e/browser-scrapers/codex-browser-scraper.d.ts +0 -61
- package/harness/e2e/browser-scrapers/index.d.ts +0 -30
- package/harness/e2e/browser-scrapers/zai-browser-scraper.d.ts +0 -80
- package/harness/langchain/agents.d.ts +0 -143
- package/harness/langchain/agents.test.d.ts +0 -2
- package/harness/langchain/checkpointer.d.ts +0 -48
- package/harness/langchain/checkpointer.test.d.ts +0 -8
- package/harness/langchain/cron-cli.d.ts +0 -12
- package/harness/langchain/daemon.d.ts +0 -161
- package/harness/langchain/daemon.test.d.ts +0 -11
- package/harness/langchain/file-tools.d.ts +0 -97
- package/harness/langchain/graph.d.ts +0 -706
- package/harness/langchain/model-router.d.ts +0 -63
- package/harness/langchain/model-router.test.d.ts +0 -2
- package/harness/langchain/models.d.ts +0 -72
- package/harness/langchain/ping-pong-middleware.d.ts +0 -28
- package/harness/langchain/ping-pong-middleware.test.d.ts +0 -2
- package/harness/langchain/run.d.ts +0 -15
- package/harness/langchain/scoreboard.test.d.ts +0 -13
- package/harness/langchain/status-line.d.ts +0 -91
- package/harness/langchain/status-line.test.d.ts +0 -8
- package/harness/langchain/surge.d.ts +0 -111
- package/harness/langchain/surge.test.d.ts +0 -10
- package/harness/langchain/widget.d.ts +0 -105
- package/harness/langchain/widget.test.d.ts +0 -8
- package/packages/autonomous-runtime/dist/worker.d.ts +0 -63
- package/packages/autonomous-runtime/dist/worker.js +0 -133
- package/packages/capability-registry/src/types.d.ts +0 -47
- package/packages/clipboard/dist/clipboard.d.ts +0 -11
- package/packages/clipboard/dist/clipboard.js +0 -40
- package/packages/clipboard/dist/index.d.ts +0 -2
- package/packages/clipboard/dist/index.js +0 -2
- package/packages/clipboard/dist/types.d.ts +0 -13
- package/packages/clipboard/dist/types.js +0 -5
- package/packages/context-discovery/dist/discover.d.ts +0 -3
- package/packages/context-discovery/dist/discover.js +0 -42
- package/packages/context-discovery/dist/discovery.d.ts +0 -28
- package/packages/context-discovery/dist/discovery.js +0 -21
- package/packages/context-discovery/dist/index.d.ts +0 -3
- package/packages/context-discovery/dist/index.js +0 -2
- package/packages/context-discovery/dist/types.d.ts +0 -16
- package/packages/context-discovery/dist/types.js +0 -2
- package/packages/event-bus/dist/bus.d.ts +0 -15
- package/packages/event-bus/dist/bus.js +0 -90
- package/packages/event-bus/dist/index.d.ts +0 -4
- package/packages/event-bus/dist/index.js +0 -3
- package/packages/event-bus/dist/test/bus.test.js +0 -52
- package/packages/event-bus/dist/types.d.ts +0 -27
- package/packages/event-bus/dist/types.js +0 -10
- package/packages/event-store/dist/index.d.ts +0 -3
- package/packages/event-store/dist/index.js +0 -2
- package/packages/event-store/dist/store.d.ts +0 -15
- package/packages/event-store/dist/store.js +0 -131
- package/packages/event-store/dist/test/store.test.js +0 -57
- package/packages/event-store/dist/types.d.ts +0 -48
- package/packages/event-store/dist/types.js +0 -5
- package/packages/event-store/src/types.d.ts +0 -48
- package/packages/file-copy-helper/dist/src/extension.js +0 -186
- package/packages/model-registry/src/types.d.ts +0 -63
- package/packages/notification/dist/src/adapters/email-adapter.d.ts +0 -17
- package/packages/notification/dist/src/adapters/email-adapter.d.ts.map +0 -1
- package/packages/notification/dist/src/adapters/email-adapter.js +0 -89
- package/packages/notification/dist/src/adapters/email-adapter.js.map +0 -1
- package/packages/notification/dist/src/adapters/line-adapter.d.ts +0 -17
- package/packages/notification/dist/src/adapters/line-adapter.d.ts.map +0 -1
- package/packages/notification/dist/src/adapters/line-adapter.js +0 -99
- package/packages/notification/dist/src/adapters/line-adapter.js.map +0 -1
- package/packages/notification/dist/src/adapters/ntfy-adapter.d.ts +0 -17
- package/packages/notification/dist/src/adapters/ntfy-adapter.d.ts.map +0 -1
- package/packages/notification/dist/src/adapters/ntfy-adapter.js +0 -84
- package/packages/notification/dist/src/adapters/ntfy-adapter.js.map +0 -1
- package/packages/notification/dist/src/adapters/telegram-adapter.d.ts +0 -56
- package/packages/notification/dist/src/adapters/telegram-adapter.d.ts.map +0 -1
- package/packages/notification/dist/src/adapters/telegram-adapter.js +0 -251
- package/packages/notification/dist/src/adapters/telegram-adapter.js.map +0 -1
- package/packages/notification/dist/src/adapters/webhook-adapter.d.ts +0 -15
- package/packages/notification/dist/src/adapters/webhook-adapter.d.ts.map +0 -1
- package/packages/notification/dist/src/adapters/webhook-adapter.js +0 -64
- package/packages/notification/dist/src/adapters/webhook-adapter.js.map +0 -1
- package/packages/notification/dist/src/base-adapter.d.ts +0 -36
- package/packages/notification/dist/src/base-adapter.d.ts.map +0 -1
- package/packages/notification/dist/src/base-adapter.js +0 -39
- package/packages/notification/dist/src/base-adapter.js.map +0 -1
- package/packages/notification/dist/src/index.d.ts +0 -12
- package/packages/notification/dist/src/index.d.ts.map +0 -1
- package/packages/notification/dist/src/index.js +0 -12
- package/packages/notification/dist/src/index.js.map +0 -1
- package/packages/notification/dist/src/notification-center.d.ts +0 -89
- package/packages/notification/dist/src/notification-center.d.ts.map +0 -1
- package/packages/notification/dist/src/notification-center.js +0 -356
- package/packages/notification/dist/src/notification-center.js.map +0 -1
- package/packages/notification/dist/src/notification-policy.d.ts +0 -66
- package/packages/notification/dist/src/notification-policy.d.ts.map +0 -1
- package/packages/notification/dist/src/notification-policy.js +0 -153
- package/packages/notification/dist/src/notification-policy.js.map +0 -1
- package/packages/notification/dist/src/telegram-webhook-handler.d.ts +0 -86
- package/packages/notification/dist/src/telegram-webhook-handler.d.ts.map +0 -1
- package/packages/notification/dist/src/telegram-webhook-handler.js +0 -149
- package/packages/notification/dist/src/telegram-webhook-handler.js.map +0 -1
- package/packages/notification/dist/src/types.d.ts +0 -106
- package/packages/notification/dist/src/types.d.ts.map +0 -1
- package/packages/notification/dist/src/types.js +0 -7
- package/packages/notification/dist/src/types.js.map +0 -1
- package/packages/projection-engine/dist/engine.d.ts +0 -61
- package/packages/projection-engine/dist/engine.js +0 -76
- package/packages/projection-engine/dist/index.d.ts +0 -2
- package/packages/projection-engine/dist/index.js +0 -2
- package/packages/projection-engine/dist/project.d.ts +0 -3
- package/packages/projection-engine/dist/project.js +0 -24
- package/packages/projection-engine/dist/types.d.ts +0 -55
- package/packages/projection-engine/dist/types.js +0 -5
- package/packages/projection-engine/src/types.d.ts +0 -55
- package/packages/provider-adapter-sdk/dist/src/builder.d.ts +0 -140
- package/packages/provider-adapter-sdk/dist/src/builder.d.ts.map +0 -1
- package/packages/provider-adapter-sdk/dist/src/builder.js +0 -332
- package/packages/provider-adapter-sdk/dist/src/builder.js.map +0 -1
- package/packages/provider-adapter-sdk/dist/src/errors.d.ts +0 -71
- package/packages/provider-adapter-sdk/dist/src/errors.d.ts.map +0 -1
- package/packages/provider-adapter-sdk/dist/src/errors.js +0 -111
- package/packages/provider-adapter-sdk/dist/src/errors.js.map +0 -1
- package/packages/provider-adapter-sdk/dist/src/index.d.ts +0 -15
- package/packages/provider-adapter-sdk/dist/src/index.d.ts.map +0 -1
- package/packages/provider-adapter-sdk/dist/src/index.js +0 -19
- package/packages/provider-adapter-sdk/dist/src/index.js.map +0 -1
- package/packages/provider-adapter-sdk/dist/src/registry.d.ts +0 -108
- package/packages/provider-adapter-sdk/dist/src/registry.d.ts.map +0 -1
- package/packages/provider-adapter-sdk/dist/src/registry.js +0 -301
- package/packages/provider-adapter-sdk/dist/src/registry.js.map +0 -1
- package/packages/provider-adapter-sdk/dist/src/tester.d.ts +0 -71
- package/packages/provider-adapter-sdk/dist/src/tester.d.ts.map +0 -1
- package/packages/provider-adapter-sdk/dist/src/tester.js +0 -392
- package/packages/provider-adapter-sdk/dist/src/tester.js.map +0 -1
- package/packages/provider-adapter-sdk/dist/src/types.d.ts +0 -182
- package/packages/provider-adapter-sdk/dist/src/types.d.ts.map +0 -1
- package/packages/provider-adapter-sdk/dist/src/types.js +0 -11
- package/packages/provider-adapter-sdk/dist/src/types.js.map +0 -1
- package/packages/provider-router/dist/capability-registry/src/types.d.ts +0 -47
- package/packages/provider-router/dist/capability-registry/src/types.d.ts.map +0 -1
- package/packages/provider-router/dist/capability-registry/src/types.js +0 -10
- package/packages/provider-router/dist/capability-registry/src/types.js.map +0 -1
- package/packages/provider-router/dist/provider-router/src/index.d.ts +0 -23
- package/packages/provider-router/dist/provider-router/src/index.d.ts.map +0 -1
- package/packages/provider-router/dist/provider-router/src/index.js +0 -25
- package/packages/provider-router/dist/provider-router/src/index.js.map +0 -1
- package/packages/provider-router/dist/provider-router/src/policy.d.ts +0 -10
- package/packages/provider-router/dist/provider-router/src/policy.d.ts.map +0 -1
- package/packages/provider-router/dist/provider-router/src/policy.js +0 -45
- package/packages/provider-router/dist/provider-router/src/policy.js.map +0 -1
- package/packages/provider-router/dist/provider-router/src/provider-router.d.ts +0 -56
- package/packages/provider-router/dist/provider-router/src/provider-router.d.ts.map +0 -1
- package/packages/provider-router/dist/provider-router/src/provider-router.js +0 -222
- package/packages/provider-router/dist/provider-router/src/provider-router.js.map +0 -1
- package/packages/provider-router/dist/provider-router/src/strategies.d.ts +0 -19
- package/packages/provider-router/dist/provider-router/src/strategies.d.ts.map +0 -1
- package/packages/provider-router/dist/provider-router/src/strategies.js +0 -110
- package/packages/provider-router/dist/provider-router/src/strategies.js.map +0 -1
- package/packages/provider-router/dist/provider-router/src/types.d.ts +0 -91
- package/packages/provider-router/dist/provider-router/src/types.d.ts.map +0 -1
- package/packages/provider-router/dist/provider-router/src/types.js +0 -10
- package/packages/provider-router/dist/provider-router/src/types.js.map +0 -1
- package/packages/provider-router/dist/src/index.d.ts +0 -23
- package/packages/provider-router/dist/src/index.d.ts.map +0 -1
- package/packages/provider-router/dist/src/index.js +0 -25
- package/packages/provider-router/dist/src/index.js.map +0 -1
- package/packages/provider-router/dist/src/policy.d.ts +0 -10
- package/packages/provider-router/dist/src/policy.d.ts.map +0 -1
- package/packages/provider-router/dist/src/policy.js +0 -45
- package/packages/provider-router/dist/src/policy.js.map +0 -1
- package/packages/provider-router/dist/src/provider-router.d.ts +0 -56
- package/packages/provider-router/dist/src/provider-router.d.ts.map +0 -1
- package/packages/provider-router/dist/src/provider-router.js +0 -222
- package/packages/provider-router/dist/src/provider-router.js.map +0 -1
- package/packages/provider-router/dist/src/strategies.d.ts +0 -19
- package/packages/provider-router/dist/src/strategies.d.ts.map +0 -1
- package/packages/provider-router/dist/src/strategies.js +0 -110
- package/packages/provider-router/dist/src/strategies.js.map +0 -1
- package/packages/provider-router/dist/src/types.d.ts +0 -91
- package/packages/provider-router/dist/src/types.d.ts.map +0 -1
- package/packages/provider-router/dist/src/types.js +0 -10
- package/packages/provider-router/dist/src/types.js.map +0 -1
- package/packages/provider-router/dist/types/src/runtime-types.d.ts +0 -412
- package/packages/provider-router/dist/types/src/runtime-types.d.ts.map +0 -1
- package/packages/provider-router/dist/types/src/runtime-types.js +0 -2
- package/packages/provider-router/dist/types/src/runtime-types.js.map +0 -1
- package/packages/session-api/dist/api.d.ts +0 -83
- package/packages/session-api/dist/api.js +0 -35
- package/packages/session-api/dist/index.d.ts +0 -2
- package/packages/session-api/dist/index.js +0 -2
- package/packages/session-api/dist/types.d.ts +0 -22
- package/packages/session-api/dist/types.js +0 -5
- package/packages/session-export/dist/exporter.d.ts +0 -28
- package/packages/session-export/dist/exporter.js +0 -38
- package/packages/session-export/dist/index.d.ts +0 -2
- package/packages/session-export/dist/index.js +0 -2
- package/packages/session-export/dist/types.d.ts +0 -20
- package/packages/session-export/dist/types.js +0 -5
- package/packages/subscription-engine/dist/engine.d.ts +0 -13
- package/packages/subscription-engine/dist/engine.js +0 -40
- package/packages/subscription-engine/dist/index.d.ts +0 -2
- package/packages/subscription-engine/dist/index.js +0 -2
- package/packages/subscription-engine/dist/subscriptions.d.ts +0 -19
- package/packages/subscription-engine/dist/subscriptions.js +0 -32
- package/packages/subscription-engine/dist/types.d.ts +0 -24
- package/packages/subscription-engine/dist/types.js +0 -5
- package/packages/trajectory/test/store.test.d.ts +0 -2
- package/packages/workflow-events/dist/events.d.ts +0 -38
- package/packages/workflow-events/dist/events.js +0 -46
- package/packages/workflow-events/dist/index.d.ts +0 -2
- package/packages/workflow-events/dist/index.js +0 -2
- package/packages/workflow-events/dist/pipeline.d.ts +0 -21
- package/packages/workflow-events/dist/pipeline.js +0 -27
- package/packages/workflow-events/dist/types.d.ts +0 -110
- package/packages/workflow-events/dist/types.js +0 -5
- package/packages/workflow-events/src/types.d.ts +0 -110
|
@@ -0,0 +1,2622 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram command routing helpers
|
|
3
|
+
* Zones: telegram controls, pi agent commands, queue controls
|
|
4
|
+
* Owns Telegram slash-command normalization, bot command metadata, pi-side command registration, and command-initiated session replacement orchestration behind runtime ports
|
|
5
|
+
*/
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
+
import { addAbortListener } from "node:events";
|
|
8
|
+
import { isDeepStrictEqual } from "node:util";
|
|
9
|
+
import { pairTelegramUserIfNeeded, TELEGRAM_DEFAULT_PROFILE_NAME, } from "./config.js";
|
|
10
|
+
import { isPiStaleContextError } from "./pi.js";
|
|
11
|
+
import { createTelegramControlItemBuilder, createTelegramControlQueueController, createTelegramQueueAdmissionReceipt, } from "./queue.js";
|
|
12
|
+
import { escapeHtml } from "./rendering.js";
|
|
13
|
+
import { formatTelegramConnectionFailure, } from "./status.js";
|
|
14
|
+
import { TelegramApiAuthorityError, } from "./telegram-api.js";
|
|
15
|
+
import { acquireTelegramUpdateRouting, carryTelegramUpdateExecutionFence, getTelegramUpdateExecutionFence, inspectTelegramDeferredSource, prepareTelegramDeferredQueueAdmission, reportTelegramQueueAdmission, } from "./updates.js";
|
|
16
|
+
/** Exact singleton operator-private text command original eligible for a held follower plan. */
|
|
17
|
+
export function isTelegramSelectedHeldOriginal(update, target, operator, name) {
|
|
18
|
+
if (!update ||
|
|
19
|
+
typeof update !== "object" ||
|
|
20
|
+
Array.isArray(update) ||
|
|
21
|
+
Object.keys(update).some((key) => !["update_id", "message"].includes(key)))
|
|
22
|
+
return false;
|
|
23
|
+
const value = Reflect.get(update, "message");
|
|
24
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
25
|
+
return false;
|
|
26
|
+
const text = Reflect.get(value, "text"), chat = Reflect.get(value, "chat"), from = Reflect.get(value, "from");
|
|
27
|
+
return (typeof text === "string" &&
|
|
28
|
+
parseTelegramCommand(text)?.name === name &&
|
|
29
|
+
value.caption === undefined &&
|
|
30
|
+
value.media_group_id === undefined &&
|
|
31
|
+
chat?.type === "private" &&
|
|
32
|
+
from?.is_bot === false &&
|
|
33
|
+
from.id === operator &&
|
|
34
|
+
chat.id === operator &&
|
|
35
|
+
Number.isSafeInteger(value.message_id) &&
|
|
36
|
+
value.message_id > 0 &&
|
|
37
|
+
typeof operator === "number" &&
|
|
38
|
+
Number.isSafeInteger(operator) &&
|
|
39
|
+
operator > 0 &&
|
|
40
|
+
target.chatId === operator &&
|
|
41
|
+
Number.isSafeInteger(target.threadId) &&
|
|
42
|
+
target.threadId > 0);
|
|
43
|
+
}
|
|
44
|
+
const TELEGRAM_EXTENSION_COMMAND_REGISTRY_KEY = "__piTelegramCommandRegistry__";
|
|
45
|
+
const TELEGRAM_BOT_COMMAND_NAME_PATTERN = /^[a-z0-9_]{1,32}$/;
|
|
46
|
+
function getOrCreateTelegramCommandRegistry() {
|
|
47
|
+
const existing = globalThis[TELEGRAM_EXTENSION_COMMAND_REGISTRY_KEY];
|
|
48
|
+
if (existing &&
|
|
49
|
+
typeof existing === "object" &&
|
|
50
|
+
existing !== null &&
|
|
51
|
+
"commands" in existing &&
|
|
52
|
+
existing.commands instanceof Map) {
|
|
53
|
+
return existing;
|
|
54
|
+
}
|
|
55
|
+
const registry = { commands: new Map() };
|
|
56
|
+
globalThis[TELEGRAM_EXTENSION_COMMAND_REGISTRY_KEY] = registry;
|
|
57
|
+
return registry;
|
|
58
|
+
}
|
|
59
|
+
function normalizeTelegramExtensionCommandName(name) {
|
|
60
|
+
return name.trim().replace(/^\/+/, "").toLowerCase();
|
|
61
|
+
}
|
|
62
|
+
function isTelegramExtensionCommandName(name) {
|
|
63
|
+
return TELEGRAM_BOT_COMMAND_NAME_PATTERN.test(name);
|
|
64
|
+
}
|
|
65
|
+
function normalizeTelegramExtensionCommandEmoji(emoji) {
|
|
66
|
+
const normalized = emoji?.trim();
|
|
67
|
+
return normalized ? normalized : undefined;
|
|
68
|
+
}
|
|
69
|
+
export function registerTelegramCommand(registration) {
|
|
70
|
+
const name = normalizeTelegramExtensionCommandName(registration.name);
|
|
71
|
+
const showInMenu = registration.showInMenu ?? false;
|
|
72
|
+
const emoji = normalizeTelegramExtensionCommandEmoji(registration.emoji);
|
|
73
|
+
if (!isTelegramExtensionCommandName(name)) {
|
|
74
|
+
throw new Error(`Invalid Telegram command name: ${registration.name}`);
|
|
75
|
+
}
|
|
76
|
+
if (showInMenu && !emoji) {
|
|
77
|
+
throw new Error(`Visible Telegram command requires emoji: ${name}`);
|
|
78
|
+
}
|
|
79
|
+
if (emoji && emoji.length > 8) {
|
|
80
|
+
throw new Error(`Telegram command emoji is too long: ${name}`);
|
|
81
|
+
}
|
|
82
|
+
if (isTelegramReservedCommandName(name)) {
|
|
83
|
+
throw new Error(`Telegram command conflicts with built-in command: ${name}`);
|
|
84
|
+
}
|
|
85
|
+
const registry = getOrCreateTelegramCommandRegistry();
|
|
86
|
+
if (registry.commands.has(name)) {
|
|
87
|
+
throw new Error(`Telegram command is already registered: ${name}`);
|
|
88
|
+
}
|
|
89
|
+
const command = {
|
|
90
|
+
name,
|
|
91
|
+
description: registration.description,
|
|
92
|
+
order: registration.order ?? 0,
|
|
93
|
+
showInMenu,
|
|
94
|
+
emoji,
|
|
95
|
+
handler: registration.handler,
|
|
96
|
+
selected: registration.selected
|
|
97
|
+
? Object.freeze({ prepare: registration.selected.prepare })
|
|
98
|
+
: undefined,
|
|
99
|
+
};
|
|
100
|
+
registry.commands.set(name, command);
|
|
101
|
+
return () => {
|
|
102
|
+
if (registry.commands.get(name) === command)
|
|
103
|
+
registry.commands.delete(name);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function getTelegramExtensionCommands() {
|
|
107
|
+
return Array.from(getOrCreateTelegramCommandRegistry().commands.values()).sort((a, b) => a.order - b.order || a.name.localeCompare(b.name));
|
|
108
|
+
}
|
|
109
|
+
export function findTelegramExtensionCommand(name) {
|
|
110
|
+
if (!name)
|
|
111
|
+
return undefined;
|
|
112
|
+
return getOrCreateTelegramCommandRegistry().commands.get(normalizeTelegramExtensionCommandName(name));
|
|
113
|
+
}
|
|
114
|
+
/** Preparation only: no execution, admission, source freeze or binding effects. Routing owns those later. */
|
|
115
|
+
export async function prepareTelegramSelectedExtensionCommand(command, authority) {
|
|
116
|
+
const registration = findTelegramExtensionCommand(command.name), selected = registration?.selected;
|
|
117
|
+
const prepare = selected?.prepare;
|
|
118
|
+
if (!registration || !selected || typeof prepare !== "function")
|
|
119
|
+
return undefined;
|
|
120
|
+
const input = Object.freeze({ name: registration.name, args: command.args });
|
|
121
|
+
const { assertSourceCurrent, assertRecipientCurrent } = authority;
|
|
122
|
+
const assertRegistrationCurrent = () => {
|
|
123
|
+
if (findTelegramExtensionCommand(input.name) !== registration ||
|
|
124
|
+
registration.selected !== selected ||
|
|
125
|
+
selected.prepare !== prepare)
|
|
126
|
+
throw new Error("Selected extension command registration changed.");
|
|
127
|
+
};
|
|
128
|
+
const assertCurrent = () => {
|
|
129
|
+
assertSourceCurrent();
|
|
130
|
+
assertRecipientCurrent();
|
|
131
|
+
assertRegistrationCurrent();
|
|
132
|
+
};
|
|
133
|
+
try {
|
|
134
|
+
assertCurrent();
|
|
135
|
+
const prepared = await prepare(input);
|
|
136
|
+
assertCurrent();
|
|
137
|
+
if (!prepared || typeof prepared !== "object" || Array.isArray(prepared))
|
|
138
|
+
return undefined;
|
|
139
|
+
const keys = Reflect.ownKeys(prepared), kind = prepared.kind;
|
|
140
|
+
let plan;
|
|
141
|
+
if (kind === "command-only" &&
|
|
142
|
+
keys.length === 2 &&
|
|
143
|
+
keys.includes("kind") &&
|
|
144
|
+
keys.includes("execute")) {
|
|
145
|
+
const execute = prepared.execute;
|
|
146
|
+
if (typeof execute !== "function")
|
|
147
|
+
return undefined;
|
|
148
|
+
plan = { kind, execute };
|
|
149
|
+
}
|
|
150
|
+
else if (kind === "generated-prompt" &&
|
|
151
|
+
keys.length === 2 &&
|
|
152
|
+
keys.includes("kind") &&
|
|
153
|
+
keys.includes("prompt")) {
|
|
154
|
+
const prompt = prepared.prompt;
|
|
155
|
+
if (typeof prompt !== "string" || !prompt.trim())
|
|
156
|
+
return undefined;
|
|
157
|
+
plan = { kind, prompt };
|
|
158
|
+
}
|
|
159
|
+
else
|
|
160
|
+
return undefined;
|
|
161
|
+
// Producer accessors can invalidate authority while the plan is being copied.
|
|
162
|
+
assertCurrent();
|
|
163
|
+
return Object.freeze({
|
|
164
|
+
plan: Object.freeze(plan),
|
|
165
|
+
assertRegistrationCurrent,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
export function clearTelegramExtensionCommands() {
|
|
173
|
+
getOrCreateTelegramCommandRegistry().commands.clear();
|
|
174
|
+
}
|
|
175
|
+
export const TELEGRAM_COMMAND_EMOJI = {
|
|
176
|
+
start: "🟢",
|
|
177
|
+
status: "📊",
|
|
178
|
+
model: "🤖",
|
|
179
|
+
thinking: "🧠",
|
|
180
|
+
compact: "🗜",
|
|
181
|
+
queue: "🔢",
|
|
182
|
+
thread: "🧵",
|
|
183
|
+
next: "⏩",
|
|
184
|
+
continue: "▶️",
|
|
185
|
+
abort: "⏹️",
|
|
186
|
+
stop: "🟥",
|
|
187
|
+
name: "🏷️",
|
|
188
|
+
new: "🆕",
|
|
189
|
+
};
|
|
190
|
+
function getTelegramCommandEmoji(command) {
|
|
191
|
+
return TELEGRAM_COMMAND_EMOJI[command];
|
|
192
|
+
}
|
|
193
|
+
export function formatTelegramCommandEmojiPrefix(command) {
|
|
194
|
+
return `${getTelegramCommandEmoji(command)} `;
|
|
195
|
+
}
|
|
196
|
+
export function formatTelegramPiCommandHtml(command) {
|
|
197
|
+
return `<code>${escapeHtml(command)}</code>`;
|
|
198
|
+
}
|
|
199
|
+
export function formatTelegramInformationHeading(emoji, text) {
|
|
200
|
+
return `<b>${escapeHtml(emoji)} ${escapeHtml(text)}</b>`;
|
|
201
|
+
}
|
|
202
|
+
export function formatTelegramInvalidInstanceName(validationError) {
|
|
203
|
+
const details = validationError.replace(/^Invalid Telegram (?:instance name|Thread display name):\s*/, "");
|
|
204
|
+
const items = details
|
|
205
|
+
.split(/;\s+|(?<=\.)\s+(?=[A-Z])/)
|
|
206
|
+
.map((item) => item.trim())
|
|
207
|
+
.filter(Boolean)
|
|
208
|
+
.map((item) => (/[.!?]$/.test(item) ? item : `${item}.`))
|
|
209
|
+
.map((item) => item[0].toUpperCase() + item.slice(1));
|
|
210
|
+
return [
|
|
211
|
+
"<b>⚠️ Invalid Thread Display Name:</b>\n",
|
|
212
|
+
...items.map((item) => `<code>-</code> ${escapeHtml(item)}`),
|
|
213
|
+
].join("\n");
|
|
214
|
+
}
|
|
215
|
+
export function formatTelegramThreadDisplayNameSavedHeading(name) {
|
|
216
|
+
return `<b>✅ Thread display name saved as <i>${escapeHtml(name)}</i>.</b>`;
|
|
217
|
+
}
|
|
218
|
+
export function formatTelegramAutomaticThreadDisplayNameRestoredHeading(name) {
|
|
219
|
+
return `<b>✅ Automatic Thread display name restored as <i>${escapeHtml(name)}</i>.</b>`;
|
|
220
|
+
}
|
|
221
|
+
const TELEGRAM_COMPACTION_STARTED_TEXT = formatTelegramInformationHeading(getTelegramCommandEmoji("compact"), "Compaction started.");
|
|
222
|
+
const TELEGRAM_COMPACTION_COMPLETED_TEXT = formatTelegramInformationHeading("✅", "Compaction completed.");
|
|
223
|
+
export const TELEGRAM_COMPACTION_STARTED_MARKDOWN = `**${formatTelegramCommandEmojiPrefix("compact")}Compaction started.**`;
|
|
224
|
+
export const TELEGRAM_COMPACTION_COMPLETED_MARKDOWN = "**✅ Compaction completed.**";
|
|
225
|
+
function formatTelegramBotCommandDescription(command, description) {
|
|
226
|
+
return `${formatTelegramCommandEmojiPrefix(command)}${description}`;
|
|
227
|
+
}
|
|
228
|
+
const TELEGRAM_BUILTIN_BOT_COMMANDS = [
|
|
229
|
+
{
|
|
230
|
+
command: "start",
|
|
231
|
+
description: formatTelegramBotCommandDescription("start", "Open menu / Pair bridge"),
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
command: "compact",
|
|
235
|
+
description: formatTelegramBotCommandDescription("compact", "Compact current session"),
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
command: "new",
|
|
239
|
+
description: formatTelegramBotCommandDescription("new", "Start a new session"),
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
command: "continue",
|
|
243
|
+
description: formatTelegramBotCommandDescription("continue", "Queue continue prompt"),
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
command: "next",
|
|
247
|
+
description: formatTelegramBotCommandDescription("next", "Force next turn"),
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
command: "abort",
|
|
251
|
+
description: formatTelegramBotCommandDescription("abort", "Abort Pi"),
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
command: "stop",
|
|
255
|
+
description: formatTelegramBotCommandDescription("stop", "Abort Pi & Clear queue"),
|
|
256
|
+
},
|
|
257
|
+
];
|
|
258
|
+
export const TELEGRAM_BOT_COMMANDS = TELEGRAM_BUILTIN_BOT_COMMANDS;
|
|
259
|
+
function getVisibleTelegramExtensionBotCommands() {
|
|
260
|
+
return getTelegramExtensionCommands()
|
|
261
|
+
.filter((command) => command.showInMenu && command.description)
|
|
262
|
+
.map((command) => ({
|
|
263
|
+
command: command.name,
|
|
264
|
+
description: `${command.emoji} ${command.description ?? command.name}`,
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
export function getTelegramReservedCommandNames() {
|
|
268
|
+
return [
|
|
269
|
+
...TELEGRAM_RESERVED_COMMAND_NAMES,
|
|
270
|
+
...getTelegramExtensionCommands().map((command) => command.name),
|
|
271
|
+
];
|
|
272
|
+
}
|
|
273
|
+
export async function registerTelegramBotCommands(deps, options) {
|
|
274
|
+
const assertAuthority = options?.assertAuthority, send = deps.setMyCommands;
|
|
275
|
+
const capturedOptions = assertAuthority ? { assertAuthority } : undefined;
|
|
276
|
+
const setMyCommands = async (commands) => {
|
|
277
|
+
assertAuthority?.();
|
|
278
|
+
await send(commands, capturedOptions);
|
|
279
|
+
assertAuthority?.();
|
|
280
|
+
};
|
|
281
|
+
assertAuthority?.();
|
|
282
|
+
const extensionCommands = getVisibleTelegramExtensionBotCommands();
|
|
283
|
+
if (extensionCommands.length === 0) {
|
|
284
|
+
await setMyCommands(TELEGRAM_BOT_COMMANDS);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const nextCommandIndex = TELEGRAM_BOT_COMMANDS.findIndex((command) => command.command === "next");
|
|
288
|
+
if (nextCommandIndex === -1) {
|
|
289
|
+
await setMyCommands([...TELEGRAM_BOT_COMMANDS, ...extensionCommands]);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
await setMyCommands([
|
|
293
|
+
...TELEGRAM_BOT_COMMANDS.slice(0, nextCommandIndex + 1),
|
|
294
|
+
...extensionCommands,
|
|
295
|
+
...TELEGRAM_BOT_COMMANDS.slice(nextCommandIndex + 1),
|
|
296
|
+
]);
|
|
297
|
+
}
|
|
298
|
+
export function createTelegramBotCommandRegistrar(deps) {
|
|
299
|
+
// Only an identical local lifetime may share an in-flight sync; unguarded calls remain separate.
|
|
300
|
+
const pending = new Map();
|
|
301
|
+
return (options) => {
|
|
302
|
+
const assertAuthority = options?.assertAuthority;
|
|
303
|
+
try {
|
|
304
|
+
assertAuthority?.();
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
return Promise.reject(error);
|
|
308
|
+
}
|
|
309
|
+
const joined = pending.get(assertAuthority);
|
|
310
|
+
if (joined)
|
|
311
|
+
return joined;
|
|
312
|
+
let request;
|
|
313
|
+
request = registerTelegramBotCommands(deps, assertAuthority ? { assertAuthority } : undefined).finally(() => {
|
|
314
|
+
if (pending.get(assertAuthority) === request)
|
|
315
|
+
pending.delete(assertAuthority);
|
|
316
|
+
});
|
|
317
|
+
pending.set(assertAuthority, request);
|
|
318
|
+
return request;
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
export function createTelegramThreadDisplayNameResetBinding() {
|
|
322
|
+
let current, generation = 0;
|
|
323
|
+
return {
|
|
324
|
+
bind(reset) {
|
|
325
|
+
current = reset;
|
|
326
|
+
generation++;
|
|
327
|
+
},
|
|
328
|
+
async reset(target, options) {
|
|
329
|
+
const reset = current, assertAuthority = options?.assertAuthority, capturedGeneration = generation;
|
|
330
|
+
const capturedTarget = {
|
|
331
|
+
chatId: target.chatId,
|
|
332
|
+
...(target.threadId === undefined ? {} : { threadId: target.threadId }),
|
|
333
|
+
};
|
|
334
|
+
if (!reset)
|
|
335
|
+
return {
|
|
336
|
+
ok: false,
|
|
337
|
+
message: "Thread display name reset is unavailable.",
|
|
338
|
+
};
|
|
339
|
+
const assertCurrent = () => {
|
|
340
|
+
assertAuthority?.();
|
|
341
|
+
if (assertAuthority && generation !== capturedGeneration)
|
|
342
|
+
throw new Error("Thread display name reset binding changed.");
|
|
343
|
+
};
|
|
344
|
+
assertCurrent();
|
|
345
|
+
const result = assertAuthority
|
|
346
|
+
? await reset(capturedTarget, { assertAuthority: assertCurrent })
|
|
347
|
+
: await reset(capturedTarget);
|
|
348
|
+
assertCurrent();
|
|
349
|
+
return result;
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
export function createTelegramThreadDisplayNameRenameBinding() {
|
|
354
|
+
let current, generation = 0;
|
|
355
|
+
return {
|
|
356
|
+
bind(rename) {
|
|
357
|
+
current = rename;
|
|
358
|
+
generation++;
|
|
359
|
+
},
|
|
360
|
+
async rename(target, threadName, options) {
|
|
361
|
+
const rename = current, assertAuthority = options?.assertAuthority, capturedGeneration = generation;
|
|
362
|
+
const capturedTarget = {
|
|
363
|
+
chatId: target.chatId,
|
|
364
|
+
...(target.threadId === undefined ? {} : { threadId: target.threadId }),
|
|
365
|
+
};
|
|
366
|
+
if (!rename) {
|
|
367
|
+
return {
|
|
368
|
+
ok: false,
|
|
369
|
+
message: "Thread display naming is unavailable.",
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
const assertCurrent = () => {
|
|
373
|
+
assertAuthority?.();
|
|
374
|
+
if (assertAuthority && generation !== capturedGeneration)
|
|
375
|
+
throw new Error("Thread display naming binding changed.");
|
|
376
|
+
};
|
|
377
|
+
assertCurrent();
|
|
378
|
+
const result = assertAuthority
|
|
379
|
+
? await rename(capturedTarget, threadName, {
|
|
380
|
+
assertAuthority: assertCurrent,
|
|
381
|
+
})
|
|
382
|
+
: await rename(capturedTarget, threadName);
|
|
383
|
+
assertCurrent();
|
|
384
|
+
return result;
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function parseTelegramProfileArg(args) {
|
|
389
|
+
const word = args.trim().split(/\s+/)[0];
|
|
390
|
+
if (!word || word.length === 0)
|
|
391
|
+
return undefined;
|
|
392
|
+
if (word.startsWith("-") || /^as=/i.test(word))
|
|
393
|
+
return undefined;
|
|
394
|
+
return word === TELEGRAM_DEFAULT_PROFILE_NAME ? undefined : word;
|
|
395
|
+
}
|
|
396
|
+
function formatTelegramTakeoverTitle(ctx) {
|
|
397
|
+
return ctx.ui.theme.fg("accent", "pi-telegram");
|
|
398
|
+
}
|
|
399
|
+
function formatTelegramTakeoverPrompt(ctx, owner) {
|
|
400
|
+
const theme = ctx.ui.theme;
|
|
401
|
+
const action = theme.fg("warning", "move singleton lock here?");
|
|
402
|
+
const from = theme.fg("muted", "from:");
|
|
403
|
+
const to = theme.fg("muted", "to:");
|
|
404
|
+
const source = owner ?? "another Pi instance";
|
|
405
|
+
return `${action}\n\n${from} ${source}\n${to} ${ctx.cwd}`;
|
|
406
|
+
}
|
|
407
|
+
export function registerTelegramBridgeCommands(pi, deps) {
|
|
408
|
+
pi.registerCommand("telegram-setup", {
|
|
409
|
+
description: "<profile> — Configure Telegram bot token",
|
|
410
|
+
handler: async (args, ctx) => {
|
|
411
|
+
await deps.promptForConfig(ctx, parseTelegramProfileArg(args));
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
pi.registerCommand("telegram-status", {
|
|
415
|
+
description: "Show Telegram bridge status",
|
|
416
|
+
handler: async (args, ctx) => {
|
|
417
|
+
const verbose = /(^|\s)(--debug|debug|--verbose|verbose)(\s|$)/i.test(args);
|
|
418
|
+
ctx.ui.notify(deps.getStatusLines({ verbose }).join("\n"), "info");
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
pi.registerCommand("telegram-connect", {
|
|
422
|
+
description: "<profile> — Start Telegram bridge",
|
|
423
|
+
handler: async (args, ctx) => {
|
|
424
|
+
const sessionGeneration = deps.getSessionGeneration?.();
|
|
425
|
+
let intentId;
|
|
426
|
+
// Pi context getters throw after replacement; check plain intent/generation first.
|
|
427
|
+
const isCurrent = () => (!intentId || deps.connectionIntent?.isActive(intentId) !== false) &&
|
|
428
|
+
(sessionGeneration === undefined ||
|
|
429
|
+
deps.getSessionGeneration?.() === sessionGeneration) &&
|
|
430
|
+
deps.isContextCurrent?.(ctx) !== false;
|
|
431
|
+
if (!isCurrent())
|
|
432
|
+
return;
|
|
433
|
+
if (args
|
|
434
|
+
.trim()
|
|
435
|
+
.split(/\s+/)
|
|
436
|
+
.some((word) => /^as=/i.test(word))) {
|
|
437
|
+
ctx.ui.notify("Thread names are configured from Telegram, not from Pi commands.", "warning");
|
|
438
|
+
deps.updateStatus(ctx);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const profileName = parseTelegramProfileArg(args);
|
|
442
|
+
intentId = deps.connectionIntent?.begin(ctx.cwd, profileName);
|
|
443
|
+
try {
|
|
444
|
+
if (profileName && deps.activateProfileConfig) {
|
|
445
|
+
const ok = await deps.activateProfileConfig(ctx, profileName, isCurrent);
|
|
446
|
+
if (!isCurrent())
|
|
447
|
+
return;
|
|
448
|
+
if (!ok) {
|
|
449
|
+
ctx.ui.notify(`Profile "${profileName}" not found.`, "error");
|
|
450
|
+
deps.updateStatus(ctx);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
ctx.ui.notify(`Activated profile "${profileName}".`, "info");
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
await (deps.activateDefaultProfileConfig?.(ctx, isCurrent) ??
|
|
457
|
+
deps.reloadConfig());
|
|
458
|
+
if (!isCurrent())
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (!deps.hasBotToken()) {
|
|
462
|
+
const botTokenDiagnostic = deps.getBotTokenDiagnostic?.();
|
|
463
|
+
if (botTokenDiagnostic)
|
|
464
|
+
ctx.ui.notify(botTokenDiagnostic, "error");
|
|
465
|
+
const profileNames = deps.getProfileNames?.() ?? [];
|
|
466
|
+
if (!profileName && profileNames.length > 0) {
|
|
467
|
+
ctx.ui.notify(`No default Telegram profile configured. Available profiles: ${profileNames.join(", ")}. Use /telegram-connect <profileName> or /telegram-setup to create a default profile.`, "info");
|
|
468
|
+
deps.updateStatus(ctx);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
await deps.promptForConfig(ctx, profileName);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const startPolling = async (options) => {
|
|
475
|
+
try {
|
|
476
|
+
return await deps.startPolling(ctx, options);
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
if (!isCurrent())
|
|
480
|
+
return;
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
let result = await startPolling({ forceFreshLeaderThread: true });
|
|
485
|
+
if (!isCurrent())
|
|
486
|
+
return;
|
|
487
|
+
if (result && !result.ok && result.canTakeover) {
|
|
488
|
+
const confirmed = await ctx.ui.confirm(formatTelegramTakeoverTitle(ctx), formatTelegramTakeoverPrompt(ctx, result.owner));
|
|
489
|
+
if (!isCurrent())
|
|
490
|
+
return;
|
|
491
|
+
if (!confirmed) {
|
|
492
|
+
ctx.ui.notify("Telegram bridge takeover cancelled.", "info");
|
|
493
|
+
deps.updateStatus(ctx);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
result = await startPolling({
|
|
497
|
+
force: true,
|
|
498
|
+
forceFreshLeaderThread: true,
|
|
499
|
+
});
|
|
500
|
+
if (!isCurrent())
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (result && !result.ok) {
|
|
504
|
+
if (result.message)
|
|
505
|
+
deps.recordConnectionEvent?.(result.message, "connect-refused");
|
|
506
|
+
ctx.ui.notify(formatTelegramConnectionFailure(result.message), "warning");
|
|
507
|
+
}
|
|
508
|
+
else if (result?.message) {
|
|
509
|
+
ctx.ui.notify(result.message, "info");
|
|
510
|
+
}
|
|
511
|
+
if (!result || result.ok)
|
|
512
|
+
deps.queueAgentConnectionContext?.(true);
|
|
513
|
+
deps.updateStatus(ctx);
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
if (!isCurrent())
|
|
517
|
+
return;
|
|
518
|
+
deps.recordConnectionEvent?.(error, "connect");
|
|
519
|
+
ctx.ui.notify(formatTelegramConnectionFailure(error), "warning");
|
|
520
|
+
deps.updateStatus(ctx);
|
|
521
|
+
}
|
|
522
|
+
finally {
|
|
523
|
+
if (intentId)
|
|
524
|
+
deps.connectionIntent?.finish(intentId);
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
pi.registerCommand("telegram-disconnect", {
|
|
529
|
+
description: "Stop Telegram and delete current thread in Threaded Mode",
|
|
530
|
+
handler: async (_args, ctx) => {
|
|
531
|
+
const generation = deps.getSessionGeneration?.();
|
|
532
|
+
const isCurrent = () => (generation === undefined ||
|
|
533
|
+
deps.getSessionGeneration?.() === generation) &&
|
|
534
|
+
deps.isContextCurrent?.(ctx) !== false;
|
|
535
|
+
if (!isCurrent())
|
|
536
|
+
return;
|
|
537
|
+
deps.connectionIntent?.cancel();
|
|
538
|
+
try {
|
|
539
|
+
const threadName = deps.getDisconnectThreadName?.();
|
|
540
|
+
if (threadName) {
|
|
541
|
+
const confirmed = await ctx.ui.confirm(ctx.ui.theme.fg("accent", "pi-telegram"), `Delete Telegram thread ${ctx.ui.theme.fg("warning", threadName)} and disconnect this Pi session?`);
|
|
542
|
+
if (!isCurrent())
|
|
543
|
+
return;
|
|
544
|
+
if (!confirmed) {
|
|
545
|
+
ctx.ui.notify("Telegram disconnect cancelled.", "info");
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
const message = await deps.stopPolling();
|
|
550
|
+
if (!isCurrent())
|
|
551
|
+
return;
|
|
552
|
+
if (message)
|
|
553
|
+
ctx.ui.notify(message, "info");
|
|
554
|
+
deps.queueAgentConnectionContext?.(false);
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
deps.recordConnectionEvent?.(error, "disconnect");
|
|
558
|
+
if (!isCurrent())
|
|
559
|
+
return;
|
|
560
|
+
ctx.ui.notify("Telegram disconnect incomplete; keep Pi open. Check /telegram-status --debug.", "warning");
|
|
561
|
+
}
|
|
562
|
+
finally {
|
|
563
|
+
if (isCurrent())
|
|
564
|
+
deps.updateStatus(ctx);
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
export const TELEGRAM_RESERVED_COMMAND_NAMES = [
|
|
570
|
+
"stop",
|
|
571
|
+
"name",
|
|
572
|
+
"new",
|
|
573
|
+
"abort",
|
|
574
|
+
"next",
|
|
575
|
+
"continue",
|
|
576
|
+
"status",
|
|
577
|
+
"queue",
|
|
578
|
+
"compact",
|
|
579
|
+
"model",
|
|
580
|
+
"thinking",
|
|
581
|
+
"settings",
|
|
582
|
+
"help",
|
|
583
|
+
"start",
|
|
584
|
+
];
|
|
585
|
+
const TELEGRAM_RESERVED_COMMAND_NAME_SET = new Set(TELEGRAM_RESERVED_COMMAND_NAMES);
|
|
586
|
+
export function isTelegramReservedCommandName(commandName) {
|
|
587
|
+
return (commandName !== undefined &&
|
|
588
|
+
TELEGRAM_RESERVED_COMMAND_NAME_SET.has(commandName));
|
|
589
|
+
}
|
|
590
|
+
function isTelegramSessionBusy(ports) {
|
|
591
|
+
return (!ports.isIdle() ||
|
|
592
|
+
ports.hasPendingMessages() ||
|
|
593
|
+
ports.hasActiveTelegramTurn() ||
|
|
594
|
+
ports.hasDispatchPending() ||
|
|
595
|
+
ports.hasQueuedTelegramItems() ||
|
|
596
|
+
ports.isCompactionInProgress());
|
|
597
|
+
}
|
|
598
|
+
function canPairTelegramUserFromCommandMessage(message) {
|
|
599
|
+
return message.chat.type === undefined || message.chat.type === "private";
|
|
600
|
+
}
|
|
601
|
+
export function getTelegramCommandMessageTarget(message) {
|
|
602
|
+
return {
|
|
603
|
+
chatId: message.chat.id,
|
|
604
|
+
threadId: typeof message.message_thread_id === "number"
|
|
605
|
+
? message.message_thread_id
|
|
606
|
+
: undefined,
|
|
607
|
+
replyToMessageId: message.message_id,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
export function createTelegramCommandControlEnqueueAdapter(deps) {
|
|
611
|
+
return (target, ctx, controlType, statusSummary, execute, admissionReceipts, onQueued) => {
|
|
612
|
+
deps.enqueueControlItem(deps.createControlItem({
|
|
613
|
+
...target,
|
|
614
|
+
controlType,
|
|
615
|
+
statusSummary,
|
|
616
|
+
...(admissionReceipts?.length ? { admissionReceipts } : {}),
|
|
617
|
+
execute,
|
|
618
|
+
}), ctx, onQueued);
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
export function createTelegramCommandTargetQueueRuntime(deps) {
|
|
622
|
+
const { createControlItem, appendControlItem, dispatchNextQueuedTelegramTurn, } = deps;
|
|
623
|
+
const controlQueueController = createTelegramControlQueueController({
|
|
624
|
+
appendControlItem,
|
|
625
|
+
dispatchNextQueuedTelegramTurn,
|
|
626
|
+
});
|
|
627
|
+
return createTelegramCommandTargetRuntime({
|
|
628
|
+
enqueueControlItem: createTelegramCommandControlEnqueueAdapter({
|
|
629
|
+
createControlItem,
|
|
630
|
+
enqueueControlItem: controlQueueController.enqueue,
|
|
631
|
+
}),
|
|
632
|
+
getAdmissionScope: deps.getAdmissionScope,
|
|
633
|
+
getAdmissionJournalBinding: deps.getAdmissionJournalBinding,
|
|
634
|
+
onControlQueued: deps.onControlQueued,
|
|
635
|
+
showStatus: deps.showStatus,
|
|
636
|
+
openModelMenu: deps.openModelMenu,
|
|
637
|
+
openSettingsMenu: deps.openSettingsMenu,
|
|
638
|
+
sendTextReply: deps.sendTextReply,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
export function createTelegramCommandTargetRuntime(deps) {
|
|
642
|
+
return {
|
|
643
|
+
enqueueControlItem: (message, ctx, controlType, statusSummary, execute) => {
|
|
644
|
+
const sourceUpdateId = message.pi_telegram_source_update_id;
|
|
645
|
+
const baseReceipt = typeof sourceUpdateId === "number"
|
|
646
|
+
? createTelegramQueueAdmissionReceipt({
|
|
647
|
+
queueKind: "control",
|
|
648
|
+
scope: deps.getAdmissionScope?.() ?? "",
|
|
649
|
+
sourceUpdateIds: [sourceUpdateId],
|
|
650
|
+
})
|
|
651
|
+
: undefined;
|
|
652
|
+
const journalBindingKey = deps.getAdmissionJournalBinding?.();
|
|
653
|
+
const receipt = baseReceipt
|
|
654
|
+
? {
|
|
655
|
+
...baseReceipt,
|
|
656
|
+
...(journalBindingKey ? { journalBindingKey } : {}),
|
|
657
|
+
}
|
|
658
|
+
: undefined;
|
|
659
|
+
deps.enqueueControlItem(getTelegramCommandMessageTarget(message), ctx, controlType, statusSummary, execute, receipt ? [receipt] : undefined, receipt ? () => deps.onControlQueued?.(message, receipt) : undefined);
|
|
660
|
+
},
|
|
661
|
+
showStatus: (message, ctx, options) => {
|
|
662
|
+
const target = getTelegramCommandMessageTarget(message);
|
|
663
|
+
return deps.showStatus(target.chatId, target.replyToMessageId, ctx, target.threadId, options);
|
|
664
|
+
},
|
|
665
|
+
openModelMenu: (message, ctx, options) => {
|
|
666
|
+
const target = getTelegramCommandMessageTarget(message);
|
|
667
|
+
return deps.openModelMenu(target.chatId, target.replyToMessageId, ctx, target.threadId, options);
|
|
668
|
+
},
|
|
669
|
+
openSettingsMenu: async (message, ctx, options) => {
|
|
670
|
+
const target = getTelegramCommandMessageTarget(message);
|
|
671
|
+
if (!deps.openSettingsMenu) {
|
|
672
|
+
await deps.sendTextReply(target.chatId, target.replyToMessageId, formatTelegramInformationHeading("🚫", "Settings menu is unavailable."), { target, parseMode: "HTML" });
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
await deps.openSettingsMenu(target.chatId, target.replyToMessageId, ctx, target.threadId, options);
|
|
676
|
+
},
|
|
677
|
+
sendTextReply: async (message, text, options) => {
|
|
678
|
+
const target = getTelegramCommandMessageTarget(message);
|
|
679
|
+
const assertAuthority = options?.assertAuthority, send = deps.sendTextReply;
|
|
680
|
+
const capturedOptions = { ...options, target };
|
|
681
|
+
assertAuthority?.();
|
|
682
|
+
await send(target.chatId, target.replyToMessageId, text, capturedOptions);
|
|
683
|
+
assertAuthority?.();
|
|
684
|
+
},
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
/** Pure intro: built-in controls with registered extension lines before the abort controls. */
|
|
688
|
+
function formatTelegramAppMenuIntroHtml(extensionLines) {
|
|
689
|
+
const line = (name, label) => `${formatTelegramCommandEmojiPrefix(name)}/${name} — ${label}`;
|
|
690
|
+
return [
|
|
691
|
+
"<b>Pi Telegram</b>",
|
|
692
|
+
"",
|
|
693
|
+
line("start", "Open menu / Pair bridge"),
|
|
694
|
+
line("compact", "Compact current session"),
|
|
695
|
+
line("new", "Start a new session"),
|
|
696
|
+
line("continue", "Queue continue prompt"),
|
|
697
|
+
line("next", "Force next turn"),
|
|
698
|
+
...extensionLines,
|
|
699
|
+
line("abort", "Abort Pi"),
|
|
700
|
+
line("stop", "Abort Pi & Clear queue"),
|
|
701
|
+
].join("\n");
|
|
702
|
+
}
|
|
703
|
+
export const TELEGRAM_APP_MENU_INTRO_HTML = formatTelegramAppMenuIntroHtml([]);
|
|
704
|
+
function buildTelegramPromptTemplateMenuHtml(promptTemplates = []) {
|
|
705
|
+
if (promptTemplates.length === 0)
|
|
706
|
+
return "";
|
|
707
|
+
return promptTemplates
|
|
708
|
+
.map((template) => `🧩 /${escapeHtml(template.command)}`)
|
|
709
|
+
.join("\n");
|
|
710
|
+
}
|
|
711
|
+
function buildTelegramExtensionCommandMenuLines() {
|
|
712
|
+
return getTelegramExtensionCommands()
|
|
713
|
+
.filter((command) => command.showInMenu)
|
|
714
|
+
.map((command) => {
|
|
715
|
+
const prefix = `${escapeHtml(command.emoji ?? "")} /${escapeHtml(command.name)}`;
|
|
716
|
+
if (!command.description)
|
|
717
|
+
return prefix;
|
|
718
|
+
return `${prefix} — ${escapeHtml(command.description)}`;
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
function buildTelegramAppMenuIntroHtml() {
|
|
722
|
+
return formatTelegramAppMenuIntroHtml(buildTelegramExtensionCommandMenuLines());
|
|
723
|
+
}
|
|
724
|
+
export function buildTelegramAppMenuHtml(statusHtml, promptTemplates = []) {
|
|
725
|
+
const introHtml = buildTelegramAppMenuIntroHtml();
|
|
726
|
+
const promptTemplateHtml = buildTelegramPromptTemplateMenuHtml(promptTemplates);
|
|
727
|
+
if (!promptTemplateHtml)
|
|
728
|
+
return `${introHtml}\n\n${statusHtml}`;
|
|
729
|
+
return `${introHtml}\n\n${promptTemplateHtml}\n\n${statusHtml}`;
|
|
730
|
+
}
|
|
731
|
+
export function createTelegramAppMenuHtmlBuilder(deps) {
|
|
732
|
+
return (ctx) => {
|
|
733
|
+
return buildTelegramAppMenuHtml(deps.buildStatusHtml(ctx), deps.getPromptTemplateCommands?.());
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
function getTelegramCommandErrorMessage(error) {
|
|
737
|
+
return error instanceof Error ? error.message : String(error);
|
|
738
|
+
}
|
|
739
|
+
function formatTelegramCompactionFailure(error) {
|
|
740
|
+
let message = getTelegramCommandErrorMessage(error).trim();
|
|
741
|
+
const redundantPrefixes = [
|
|
742
|
+
"Compaction failed: ",
|
|
743
|
+
"Turn prefix summarization failed: ",
|
|
744
|
+
];
|
|
745
|
+
let stripped = true;
|
|
746
|
+
while (stripped) {
|
|
747
|
+
stripped = false;
|
|
748
|
+
for (const prefix of redundantPrefixes) {
|
|
749
|
+
if (!message.startsWith(prefix))
|
|
750
|
+
continue;
|
|
751
|
+
message = message.slice(prefix.length).trim();
|
|
752
|
+
stripped = true;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
const sentence = /[.!?]$/u.test(message) ? message : `${message}.`;
|
|
756
|
+
return `Compaction failed! ${sentence}`;
|
|
757
|
+
}
|
|
758
|
+
export function parseTelegramCommand(text) {
|
|
759
|
+
const trimmed = text.trim();
|
|
760
|
+
if (!trimmed.startsWith("/"))
|
|
761
|
+
return undefined;
|
|
762
|
+
const [head, ...tail] = trimmed.split(/\s+/);
|
|
763
|
+
const name = head.slice(1).split("@")[0]?.toLowerCase();
|
|
764
|
+
if (!name)
|
|
765
|
+
return undefined;
|
|
766
|
+
return { name, args: tail.join(" ").trim() };
|
|
767
|
+
}
|
|
768
|
+
export const TELEGRAM_COMMAND_ACTIONS = {
|
|
769
|
+
stop: { kind: "stop", executionMode: "immediate" },
|
|
770
|
+
name: { kind: "name", executionMode: "immediate" },
|
|
771
|
+
new: { kind: "new", executionMode: "immediate" },
|
|
772
|
+
abort: { kind: "abort", executionMode: "immediate" },
|
|
773
|
+
next: { kind: "next", executionMode: "immediate" },
|
|
774
|
+
continue: { kind: "continue", executionMode: "immediate" },
|
|
775
|
+
status: { kind: "status", executionMode: "immediate" },
|
|
776
|
+
queue: { kind: "queue", executionMode: "immediate" },
|
|
777
|
+
compact: { kind: "compact", executionMode: "immediate" },
|
|
778
|
+
model: { kind: "model", executionMode: "immediate" },
|
|
779
|
+
thinking: { kind: "thinking", executionMode: "immediate" },
|
|
780
|
+
settings: { kind: "settings", executionMode: "immediate" },
|
|
781
|
+
help: { kind: "help", commandName: "help", executionMode: "immediate" },
|
|
782
|
+
start: { kind: "help", commandName: "start", executionMode: "immediate" },
|
|
783
|
+
};
|
|
784
|
+
export function buildTelegramCommandAction(commandName) {
|
|
785
|
+
if (!isTelegramReservedCommandName(commandName)) {
|
|
786
|
+
return { kind: "ignore", executionMode: "ignored" };
|
|
787
|
+
}
|
|
788
|
+
return TELEGRAM_COMMAND_ACTIONS[commandName];
|
|
789
|
+
}
|
|
790
|
+
function formatTelegramQueuedTurnCount(count) {
|
|
791
|
+
return count === 1 ? "1 queued turn" : `${count} queued turns`;
|
|
792
|
+
}
|
|
793
|
+
export async function handleTelegramStopCommand(deps) {
|
|
794
|
+
deps.clearPendingModelSwitch();
|
|
795
|
+
deps.cancelNextTransitionAnnouncements?.();
|
|
796
|
+
const clearedCount = deps.clearQueuedTelegramItems();
|
|
797
|
+
deps.setFoldQueuedPromptsIntoHistory(false);
|
|
798
|
+
if (!deps.hasAbortHandler()) {
|
|
799
|
+
const clearedSuffix = clearedCount > 0
|
|
800
|
+
? ` Cleared ${formatTelegramQueuedTurnCount(clearedCount)}.`
|
|
801
|
+
: "";
|
|
802
|
+
if (clearedCount > 0)
|
|
803
|
+
deps.updateStatus();
|
|
804
|
+
await deps.sendTextReply(formatTelegramInformationHeading("💤", `No active turn.${clearedSuffix}`), { parseMode: "HTML" });
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
deps.abortCurrentTurn();
|
|
808
|
+
deps.updateStatus();
|
|
809
|
+
const clearedSuffix = clearedCount > 0
|
|
810
|
+
? ` Cleared ${formatTelegramQueuedTurnCount(clearedCount)}.`
|
|
811
|
+
: "";
|
|
812
|
+
await deps.sendTextReply(formatTelegramInformationHeading("⏹️", `Aborted current turn.${clearedSuffix}`), { parseMode: "HTML" });
|
|
813
|
+
}
|
|
814
|
+
export async function handleTelegramAbortCommand(deps) {
|
|
815
|
+
deps.clearPendingModelSwitch();
|
|
816
|
+
deps.cancelNextTransitionAnnouncements?.();
|
|
817
|
+
if (!deps.hasAbortHandler()) {
|
|
818
|
+
await deps.sendTextReply(formatTelegramInformationHeading("💤", "No active turn."), { parseMode: "HTML" });
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
deps.setFoldQueuedPromptsIntoHistory(deps.hasActiveTelegramTurn());
|
|
822
|
+
deps.abortCurrentTurn();
|
|
823
|
+
deps.updateStatus();
|
|
824
|
+
await deps.sendTextReply(formatTelegramInformationHeading("⏹️", "Aborted current turn."), { parseMode: "HTML" });
|
|
825
|
+
}
|
|
826
|
+
export async function handleTelegramNextCommand(deps) {
|
|
827
|
+
deps.clearPendingModelSwitch();
|
|
828
|
+
if (!deps.hasQueuedItems()) {
|
|
829
|
+
await deps.sendTextReply(formatTelegramInformationHeading("⌛", "Queue is empty"), { parseMode: "HTML" });
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
if (!deps.isIdle() && deps.hasAbortHandler()) {
|
|
833
|
+
deps.clearFoldForDispatch();
|
|
834
|
+
deps.requestNextDispatchAnnouncement?.();
|
|
835
|
+
deps.markActiveTurnNextAbortAnnouncement?.();
|
|
836
|
+
deps.abortCurrentTurn();
|
|
837
|
+
deps.updateStatus();
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (!deps.isIdle()) {
|
|
841
|
+
await deps.sendTextReply(formatTelegramInformationHeading("⏳", "Pi is busy. Send /abort or /stop first."), { parseMode: "HTML" });
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
deps.requestNextDispatchAnnouncement?.();
|
|
845
|
+
deps.dispatchNextQueuedTurn();
|
|
846
|
+
deps.updateStatus();
|
|
847
|
+
}
|
|
848
|
+
async function handleTelegramContinueCommand(message, ctx, deps) {
|
|
849
|
+
await deps.enqueueContinueTurn(message, ctx);
|
|
850
|
+
}
|
|
851
|
+
function dispatchNextQueuedTelegramTurnAfterCompact(deps) {
|
|
852
|
+
if (deps.requestDeferredDispatchNextQueuedTelegramTurn) {
|
|
853
|
+
deps.requestDeferredDispatchNextQueuedTelegramTurn(deps.dispatchNextQueuedTelegramTurn);
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
deps.dispatchNextQueuedTelegramTurn();
|
|
857
|
+
}
|
|
858
|
+
function buildTelegramNewConfirmationReplyMarkup() {
|
|
859
|
+
return {
|
|
860
|
+
inline_keyboard: [
|
|
861
|
+
[
|
|
862
|
+
{ text: "🆕 Yes, start new", callback_data: "new:confirm" },
|
|
863
|
+
{ text: "❌ No", callback_data: "new:cancel" },
|
|
864
|
+
],
|
|
865
|
+
],
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
/** Send one confirmation to an exact copied recipient, with authority checks around the issued request. */
|
|
869
|
+
async function sendTelegramConfirmation(target, deps, html, markup, assertAuthority) {
|
|
870
|
+
const recipientTarget = {
|
|
871
|
+
chatId: target.chatId,
|
|
872
|
+
...(target.threadId !== undefined ? { threadId: target.threadId } : {}),
|
|
873
|
+
};
|
|
874
|
+
const options = assertAuthority
|
|
875
|
+
? { target: recipientTarget, assertAuthority }
|
|
876
|
+
: target.threadId !== undefined
|
|
877
|
+
? { target: recipientTarget }
|
|
878
|
+
: undefined;
|
|
879
|
+
assertAuthority?.();
|
|
880
|
+
await deps.sendInteractiveMessage(recipientTarget.chatId, html, "html", markup, options);
|
|
881
|
+
assertAuthority?.();
|
|
882
|
+
}
|
|
883
|
+
function getTelegramNewConfirmationHtml() {
|
|
884
|
+
return "<b>Start a new session?</b>";
|
|
885
|
+
}
|
|
886
|
+
export async function openTelegramNewConfirmation(target, deps, assertAuthority) {
|
|
887
|
+
await sendTelegramConfirmation(target, deps, getTelegramNewConfirmationHtml(), buildTelegramNewConfirmationReplyMarkup(), assertAuthority);
|
|
888
|
+
}
|
|
889
|
+
export async function handleTelegramNewConfirmationCallback(query, deps) {
|
|
890
|
+
if (query.data !== "new:confirm" && query.data !== "new:cancel")
|
|
891
|
+
return false;
|
|
892
|
+
const chatId = query.message?.chat?.id;
|
|
893
|
+
const messageId = query.message?.message_id;
|
|
894
|
+
if (typeof chatId !== "number" || typeof messageId !== "number") {
|
|
895
|
+
await deps.answerCallbackQuery(query.id, "Interactive message expired");
|
|
896
|
+
return true;
|
|
897
|
+
}
|
|
898
|
+
if (query.data === "new:cancel") {
|
|
899
|
+
await deps.editInteractiveMessage(chatId, messageId, "<b>🚫 New session cancelled.</b>", "html", { inline_keyboard: [] });
|
|
900
|
+
await deps.answerCallbackQuery(query.id);
|
|
901
|
+
return true;
|
|
902
|
+
}
|
|
903
|
+
await deps.answerCallbackQuery(query.id);
|
|
904
|
+
await deps.deleteMessage(chatId, messageId);
|
|
905
|
+
await deps.runNew(deps.ctx);
|
|
906
|
+
return true;
|
|
907
|
+
}
|
|
908
|
+
function buildTelegramCompactConfirmationReplyMarkup() {
|
|
909
|
+
return {
|
|
910
|
+
inline_keyboard: [
|
|
911
|
+
[
|
|
912
|
+
{ text: "🗜 Yes, compact", callback_data: "compact:confirm" },
|
|
913
|
+
{ text: "❌ No", callback_data: "compact:cancel" },
|
|
914
|
+
],
|
|
915
|
+
],
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
function getTelegramCompactConfirmationHtml() {
|
|
919
|
+
return "<b>Compact session?</b>";
|
|
920
|
+
}
|
|
921
|
+
async function openTelegramCompactConfirmation(target, deps, assertAuthority) {
|
|
922
|
+
await sendTelegramConfirmation(target, deps, getTelegramCompactConfirmationHtml(), buildTelegramCompactConfirmationReplyMarkup(), assertAuthority);
|
|
923
|
+
}
|
|
924
|
+
export async function handleTelegramCompactConfirmationCallback(query, deps) {
|
|
925
|
+
if (query.data !== "compact:confirm" && query.data !== "compact:cancel") {
|
|
926
|
+
return false;
|
|
927
|
+
}
|
|
928
|
+
const callbackMessage = query.message;
|
|
929
|
+
const chatId = callbackMessage?.chat?.id;
|
|
930
|
+
const messageId = callbackMessage?.message_id;
|
|
931
|
+
if (typeof chatId !== "number" || typeof messageId !== "number") {
|
|
932
|
+
await deps.answerCallbackQuery(query.id, "Interactive message expired");
|
|
933
|
+
return true;
|
|
934
|
+
}
|
|
935
|
+
if (query.data === "compact:cancel") {
|
|
936
|
+
await deps.editInteractiveMessage(chatId, messageId, "<b>🚫 Compaction cancelled.</b>", "html", { inline_keyboard: [] });
|
|
937
|
+
await deps.answerCallbackQuery(query.id);
|
|
938
|
+
return true;
|
|
939
|
+
}
|
|
940
|
+
await deps.editInteractiveMessage(chatId, messageId, TELEGRAM_COMPACTION_STARTED_TEXT, "html", { inline_keyboard: [] });
|
|
941
|
+
await deps.answerCallbackQuery(query.id);
|
|
942
|
+
const threadId = callbackMessage?.message_thread_id;
|
|
943
|
+
await deps.runCompact(deps.ctx, chatId, messageId, typeof threadId === "number" ? { chatId, threadId } : { chatId });
|
|
944
|
+
return true;
|
|
945
|
+
}
|
|
946
|
+
export async function handleTelegramNewCommand(deps) {
|
|
947
|
+
if (isTelegramSessionBusy(deps)) {
|
|
948
|
+
await deps.sendTextReply(formatTelegramInformationHeading("⏳", "Cannot start a new session while Pi or the Telegram queue is busy. Wait for queued turns to finish or send /abort first."), { parseMode: "HTML" });
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
if (!deps.requestNewSession) {
|
|
952
|
+
await deps.sendTextReply(formatTelegramInformationHeading("🚫", "Session replacement is unavailable in this Pi runtime."), { parseMode: "HTML" });
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
deps.requestNewSession();
|
|
956
|
+
}
|
|
957
|
+
export async function handleTelegramCompactCommand(deps) {
|
|
958
|
+
if (isTelegramSessionBusy(deps)) {
|
|
959
|
+
await deps.sendTextReply(formatTelegramInformationHeading("⏳", "Cannot compact while Pi or the Telegram queue is busy. Wait for queued turns to finish or send /abort first."), { parseMode: "HTML" });
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
deps.setCompactionInProgress(true);
|
|
963
|
+
deps.updateStatus();
|
|
964
|
+
deps.startTypingLoop?.();
|
|
965
|
+
try {
|
|
966
|
+
deps.compact({
|
|
967
|
+
onComplete: () => {
|
|
968
|
+
deps.stopTypingLoop?.();
|
|
969
|
+
deps.setCompactionInProgress(false);
|
|
970
|
+
deps.updateStatus();
|
|
971
|
+
dispatchNextQueuedTelegramTurnAfterCompact(deps);
|
|
972
|
+
void deps.sendTextReply(TELEGRAM_COMPACTION_COMPLETED_TEXT, {
|
|
973
|
+
parseMode: "HTML",
|
|
974
|
+
});
|
|
975
|
+
},
|
|
976
|
+
onError: (error) => {
|
|
977
|
+
deps.stopTypingLoop?.();
|
|
978
|
+
deps.setCompactionInProgress(false);
|
|
979
|
+
deps.updateStatus();
|
|
980
|
+
dispatchNextQueuedTelegramTurnAfterCompact(deps);
|
|
981
|
+
deps.recordRuntimeEvent?.("compact", error);
|
|
982
|
+
void deps.sendTextReply(formatTelegramInformationHeading("⚠️", formatTelegramCompactionFailure(error)), { parseMode: "HTML" });
|
|
983
|
+
},
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
catch (error) {
|
|
987
|
+
deps.stopTypingLoop?.();
|
|
988
|
+
deps.setCompactionInProgress(false);
|
|
989
|
+
deps.updateStatus();
|
|
990
|
+
deps.recordRuntimeEvent?.("compact", error);
|
|
991
|
+
await deps.sendTextReply(formatTelegramInformationHeading("⚠️", formatTelegramCompactionFailure(error)), { parseMode: "HTML" });
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
if (!deps.suppressStartNotice) {
|
|
995
|
+
await deps.sendTextReply(TELEGRAM_COMPACTION_STARTED_TEXT, {
|
|
996
|
+
parseMode: "HTML",
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
export async function handleTelegramModelCommand(deps) {
|
|
1001
|
+
try {
|
|
1002
|
+
await deps.openModelMenu(deps.ctx);
|
|
1003
|
+
}
|
|
1004
|
+
catch (error) {
|
|
1005
|
+
if (!isPiStaleContextError(error))
|
|
1006
|
+
throw error;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
export async function executeTelegramCommandAction(action, message, ctx, deps, commandArgs = "") {
|
|
1010
|
+
switch (action.kind) {
|
|
1011
|
+
case "ignore":
|
|
1012
|
+
return false;
|
|
1013
|
+
case "stop":
|
|
1014
|
+
await deps.handleStop(message, ctx);
|
|
1015
|
+
return true;
|
|
1016
|
+
case "name":
|
|
1017
|
+
await deps.handleName(message, ctx, commandArgs);
|
|
1018
|
+
return true;
|
|
1019
|
+
case "new":
|
|
1020
|
+
await deps.handleNew(message, ctx);
|
|
1021
|
+
return true;
|
|
1022
|
+
case "abort":
|
|
1023
|
+
await deps.handleAbort(message, ctx);
|
|
1024
|
+
return true;
|
|
1025
|
+
case "next":
|
|
1026
|
+
await deps.handleNext(message, ctx);
|
|
1027
|
+
return true;
|
|
1028
|
+
case "continue":
|
|
1029
|
+
await deps.handleContinue(message, ctx);
|
|
1030
|
+
return true;
|
|
1031
|
+
case "queue":
|
|
1032
|
+
await deps.handleQueue(message, ctx);
|
|
1033
|
+
return true;
|
|
1034
|
+
case "compact":
|
|
1035
|
+
await deps.handleCompact(message, ctx);
|
|
1036
|
+
return true;
|
|
1037
|
+
case "status":
|
|
1038
|
+
await deps.handleStatus(message, ctx);
|
|
1039
|
+
return true;
|
|
1040
|
+
case "model":
|
|
1041
|
+
await deps.handleModel(message, ctx);
|
|
1042
|
+
return true;
|
|
1043
|
+
case "thinking":
|
|
1044
|
+
await deps.handleThinking(message, ctx);
|
|
1045
|
+
return true;
|
|
1046
|
+
case "settings":
|
|
1047
|
+
if (!deps.handleSettings)
|
|
1048
|
+
return false;
|
|
1049
|
+
await deps.handleSettings(message, ctx);
|
|
1050
|
+
return true;
|
|
1051
|
+
case "help":
|
|
1052
|
+
await deps.handleHelp(message, action.commandName, ctx);
|
|
1053
|
+
return true;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
export function createTelegramCommandHandlerTargetRuntime(deps) {
|
|
1057
|
+
const commandTargetRuntime = createTelegramCommandTargetQueueRuntime({
|
|
1058
|
+
createControlItem: createTelegramControlItemBuilder({
|
|
1059
|
+
allocateItemOrder: deps.allocateItemOrder,
|
|
1060
|
+
allocateControlOrder: deps.allocateControlOrder,
|
|
1061
|
+
}),
|
|
1062
|
+
appendControlItem: deps.appendControlItem,
|
|
1063
|
+
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
1064
|
+
getAdmissionScope: deps.getAdmissionScope,
|
|
1065
|
+
getAdmissionJournalBinding: deps.getAdmissionJournalBinding,
|
|
1066
|
+
onControlQueued: deps.onControlQueued,
|
|
1067
|
+
showStatus: deps.showStatus,
|
|
1068
|
+
openModelMenu: deps.openModelMenu,
|
|
1069
|
+
openSettingsMenu: deps.openSettingsMenu,
|
|
1070
|
+
sendTextReply: deps.sendTextReply,
|
|
1071
|
+
});
|
|
1072
|
+
return createTelegramCommandHandler({
|
|
1073
|
+
assertExecutionCurrent: deps.assertExecutionCurrent,
|
|
1074
|
+
hasAbortHandler: deps.hasAbortHandler,
|
|
1075
|
+
clearPendingModelSwitch: deps.clearPendingModelSwitch,
|
|
1076
|
+
hasQueuedTelegramItems: deps.hasQueuedTelegramItems,
|
|
1077
|
+
clearQueuedTelegramItems: deps.clearQueuedTelegramItems,
|
|
1078
|
+
setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
|
|
1079
|
+
abortCurrentTurn: deps.abortCurrentTurn,
|
|
1080
|
+
isIdle: deps.isIdle,
|
|
1081
|
+
hasPendingMessages: deps.hasPendingMessages,
|
|
1082
|
+
hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
|
|
1083
|
+
hasDispatchPending: deps.hasDispatchPending,
|
|
1084
|
+
isCompactionInProgress: deps.isCompactionInProgress,
|
|
1085
|
+
setCompactionInProgress: deps.setCompactionInProgress,
|
|
1086
|
+
updateStatus: deps.updateStatus,
|
|
1087
|
+
isContextActive: deps.isContextActive,
|
|
1088
|
+
beginCommandEffectWork: deps.beginCommandEffectWork,
|
|
1089
|
+
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
1090
|
+
requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
|
|
1091
|
+
markActiveTurnNextAbortAnnouncement: deps.markActiveTurnNextAbortAnnouncement,
|
|
1092
|
+
cancelNextTransitionAnnouncements: deps.cancelNextTransitionAnnouncements,
|
|
1093
|
+
startTypingLoop: deps.startTypingLoop,
|
|
1094
|
+
stopTypingLoop: deps.stopTypingLoop,
|
|
1095
|
+
enqueueContinueTurn: deps.enqueueContinueTurn,
|
|
1096
|
+
heldTurn: deps.heldTurn,
|
|
1097
|
+
compact: deps.compact,
|
|
1098
|
+
requestNewSession: deps.requestNewSession,
|
|
1099
|
+
sendInteractiveMessage: deps.sendInteractiveMessage,
|
|
1100
|
+
enqueueControlItem: commandTargetRuntime.enqueueControlItem,
|
|
1101
|
+
showStatus: commandTargetRuntime.showStatus,
|
|
1102
|
+
openModelMenu: commandTargetRuntime.openModelMenu,
|
|
1103
|
+
openThinkingMenu: deps.openThinkingMenu,
|
|
1104
|
+
openQueueMenu: deps.openQueueMenu,
|
|
1105
|
+
openSettingsMenu: commandTargetRuntime.openSettingsMenu,
|
|
1106
|
+
handleForumBootstrap: deps.handleForumBootstrap,
|
|
1107
|
+
getAllowedUserId: deps.getAllowedUserId,
|
|
1108
|
+
persistAllowedUserId: deps.persistAllowedUserId,
|
|
1109
|
+
registerBotCommands: createTelegramBotCommandRegistrar({
|
|
1110
|
+
setMyCommands: deps.setMyCommands,
|
|
1111
|
+
}),
|
|
1112
|
+
validateThreadName: deps.validateThreadName,
|
|
1113
|
+
renameCurrentThread: deps.renameCurrentThread,
|
|
1114
|
+
resetCurrentThreadName: deps.resetCurrentThreadName,
|
|
1115
|
+
openThreadNameDialog: deps.openThreadNameDialog,
|
|
1116
|
+
sendTextReply: commandTargetRuntime.sendTextReply,
|
|
1117
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Captures admission ports now and checks source → execution → recipient; `strict` rechecks the source after the
|
|
1122
|
+
* recipient, for owners whose effects outlive one synchronous step.
|
|
1123
|
+
*/
|
|
1124
|
+
function createSelectedSemanticGuard(admission, assertExecution, strict) {
|
|
1125
|
+
const assertSourceCurrent = admission.assertSourceCurrent, assertRecipientCurrent = admission.assertRecipientCurrent;
|
|
1126
|
+
return () => {
|
|
1127
|
+
assertSourceCurrent();
|
|
1128
|
+
assertExecution();
|
|
1129
|
+
assertRecipientCurrent();
|
|
1130
|
+
if (!strict)
|
|
1131
|
+
return;
|
|
1132
|
+
assertSourceCurrent();
|
|
1133
|
+
assertExecution();
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
/** Wrap one synchronous command effect between semantic checks. */
|
|
1137
|
+
function guardSelectedEffect(assertCurrent) {
|
|
1138
|
+
return (effect) => (...args) => {
|
|
1139
|
+
assertCurrent();
|
|
1140
|
+
const result = effect(...args);
|
|
1141
|
+
assertCurrent();
|
|
1142
|
+
return result;
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
/** One issuance only: later calls return false without assertions or effects, never replay. */
|
|
1146
|
+
function issueSelectedOnce(assertCurrent, run) {
|
|
1147
|
+
let issued = false;
|
|
1148
|
+
return async () => {
|
|
1149
|
+
if (issued)
|
|
1150
|
+
return false;
|
|
1151
|
+
assertCurrent();
|
|
1152
|
+
issued = true;
|
|
1153
|
+
return run();
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
/** Body-free reply anchor: chat, message and Thread only, never the original's admission symbols. */
|
|
1157
|
+
function copySelectedReplyAnchor(original) {
|
|
1158
|
+
return {
|
|
1159
|
+
chat: { ...original.chat },
|
|
1160
|
+
message_id: original.message_id,
|
|
1161
|
+
message_thread_id: original.message_thread_id,
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
function copySelectedMenuCarrier(original) {
|
|
1165
|
+
const message = Object.fromEntries(Object.entries(original).filter(([key]) => key !== "pi_telegram_source_update_id"));
|
|
1166
|
+
message.chat = { ...original.chat };
|
|
1167
|
+
return message;
|
|
1168
|
+
}
|
|
1169
|
+
export function createTelegramCommandHandler(deps) {
|
|
1170
|
+
const handle = async (commandName, message, ctx, commandArgs) => {
|
|
1171
|
+
return handleTelegramCommandRuntime(commandName, message, ctx, deps, commandArgs);
|
|
1172
|
+
};
|
|
1173
|
+
const heldQueueReceipts = new Map();
|
|
1174
|
+
const heldQueueOwners = new WeakSet();
|
|
1175
|
+
const watchHeldQueueOwner = (signal) => {
|
|
1176
|
+
if (heldQueueOwners.has(signal))
|
|
1177
|
+
return;
|
|
1178
|
+
addAbortListener(signal, () => {
|
|
1179
|
+
// Native owner end is irreversible; keep refusal identity, not its context/carrier closures or a fallback grant.
|
|
1180
|
+
for (const [id, record] of heldQueueReceipts)
|
|
1181
|
+
if (record?.signal === signal)
|
|
1182
|
+
heldQueueReceipts.set(id, null);
|
|
1183
|
+
});
|
|
1184
|
+
heldQueueOwners.add(signal);
|
|
1185
|
+
};
|
|
1186
|
+
const prepareHeldSelectedCommand = (name, readiness, admission, captured, portsCurrent, completion, run, inspectQueuedCompletion) => {
|
|
1187
|
+
const snapshot = readiness.snapshot, raw = snapshot.update.message, target = { ...admission.target };
|
|
1188
|
+
const bind = readiness.bindCarrier, isCurrent = readiness.isCurrent, assertSource = admission.assertSourceCurrent, assertRecipient = admission.assertRecipientCurrent;
|
|
1189
|
+
if (typeof bind !== "function" ||
|
|
1190
|
+
typeof isCurrent !== "function" ||
|
|
1191
|
+
typeof assertSource !== "function" ||
|
|
1192
|
+
typeof assertRecipient !== "function" ||
|
|
1193
|
+
!isCurrent.call(readiness) ||
|
|
1194
|
+
!raw ||
|
|
1195
|
+
typeof raw !== "object" ||
|
|
1196
|
+
Array.isArray(raw) ||
|
|
1197
|
+
Object.keys(snapshot.update).some((key) => !["update_id", "message"].includes(key)))
|
|
1198
|
+
return undefined;
|
|
1199
|
+
const value = raw, chat = value.chat, from = value.from;
|
|
1200
|
+
if (typeof captured.getAllowedUserId !== "function")
|
|
1201
|
+
return undefined;
|
|
1202
|
+
const command = typeof value.text === "string"
|
|
1203
|
+
? parseTelegramCommand(value.text)
|
|
1204
|
+
: undefined, operator = captured.getAllowedUserId();
|
|
1205
|
+
if (!command ||
|
|
1206
|
+
!chat ||
|
|
1207
|
+
!from ||
|
|
1208
|
+
!isTelegramSelectedHeldOriginal(snapshot.update, target, operator, name))
|
|
1209
|
+
return undefined;
|
|
1210
|
+
const source = { ...snapshot.source }, preparedCommand = { ...command };
|
|
1211
|
+
const message = {
|
|
1212
|
+
...structuredClone(value),
|
|
1213
|
+
chat: { ...chat },
|
|
1214
|
+
message_thread_id: target.threadId,
|
|
1215
|
+
};
|
|
1216
|
+
delete message.pi_telegram_source_update_id;
|
|
1217
|
+
let bound, issued = false;
|
|
1218
|
+
const ownerCurrent = () => portsCurrent() &&
|
|
1219
|
+
readiness.bindCarrier === bind &&
|
|
1220
|
+
readiness.isCurrent === isCurrent &&
|
|
1221
|
+
isCurrent.call(readiness) &&
|
|
1222
|
+
captured.getAllowedUserId() === operator;
|
|
1223
|
+
const completionCurrent = () => !!bound &&
|
|
1224
|
+
Object.entries(bound.ports).every(([key, port]) => Reflect.get(bound.completion, key) === port) &&
|
|
1225
|
+
bound.ports.isCurrent.call(bound.completion);
|
|
1226
|
+
const assertSourceCurrent = () => {
|
|
1227
|
+
assertSource.call(admission);
|
|
1228
|
+
if (!ownerCurrent() || !completionCurrent() || !bound)
|
|
1229
|
+
throw new Error(`Held ${name} source owner is unavailable.`);
|
|
1230
|
+
captured.assertExecutionCurrent?.(bound.carrier);
|
|
1231
|
+
const current = inspectTelegramDeferredSource(bound.carrier);
|
|
1232
|
+
if (!current ||
|
|
1233
|
+
current.updateId !== source.updateId ||
|
|
1234
|
+
current.journalBindingKey !== source.journalBindingKey ||
|
|
1235
|
+
current.sourceSha256 !== source.sourceSha256 ||
|
|
1236
|
+
!ownerCurrent() ||
|
|
1237
|
+
!completionCurrent())
|
|
1238
|
+
throw new Error(`Held ${name} original changed.`);
|
|
1239
|
+
};
|
|
1240
|
+
return {
|
|
1241
|
+
get source() {
|
|
1242
|
+
return { ...source };
|
|
1243
|
+
},
|
|
1244
|
+
get command() {
|
|
1245
|
+
return { ...preparedCommand };
|
|
1246
|
+
},
|
|
1247
|
+
bindCarrier(value) {
|
|
1248
|
+
if (!ownerCurrent())
|
|
1249
|
+
return false;
|
|
1250
|
+
if (bound)
|
|
1251
|
+
return value === bound.carrier && completionCurrent();
|
|
1252
|
+
const completion = bind.call(readiness, value);
|
|
1253
|
+
if (!completion ||
|
|
1254
|
+
completion.source.updateId !== source.updateId ||
|
|
1255
|
+
completion.source.journalBindingKey !== source.journalBindingKey ||
|
|
1256
|
+
completion.source.sourceSha256 !== source.sourceSha256 ||
|
|
1257
|
+
!ownerCurrent())
|
|
1258
|
+
return false;
|
|
1259
|
+
const ports = {
|
|
1260
|
+
isCurrent: completion.isCurrent,
|
|
1261
|
+
reportCompleted: completion.reportCompleted,
|
|
1262
|
+
inspectCompletion: completion.inspectCompletion,
|
|
1263
|
+
};
|
|
1264
|
+
if (Object.values(ports).some((port) => typeof port !== "function"))
|
|
1265
|
+
return false;
|
|
1266
|
+
bound = { carrier: value, completion, ports };
|
|
1267
|
+
return completionCurrent();
|
|
1268
|
+
},
|
|
1269
|
+
async execute() {
|
|
1270
|
+
if (issued)
|
|
1271
|
+
return false;
|
|
1272
|
+
assertSourceCurrent();
|
|
1273
|
+
assertRecipient.call(admission);
|
|
1274
|
+
assertSourceCurrent();
|
|
1275
|
+
issued = true;
|
|
1276
|
+
if (completion === "before-effect" &&
|
|
1277
|
+
!bound.ports.reportCompleted.call(bound.completion))
|
|
1278
|
+
return false;
|
|
1279
|
+
return run({
|
|
1280
|
+
message,
|
|
1281
|
+
carrier: bound.carrier,
|
|
1282
|
+
source: { ...source },
|
|
1283
|
+
command: { ...preparedCommand },
|
|
1284
|
+
assertRecipient,
|
|
1285
|
+
reportCompleted: () => bound.ports.reportCompleted.call(bound.completion),
|
|
1286
|
+
assertSemantic() {
|
|
1287
|
+
assertSourceCurrent();
|
|
1288
|
+
assertRecipient.call(admission);
|
|
1289
|
+
},
|
|
1290
|
+
});
|
|
1291
|
+
},
|
|
1292
|
+
inspectCompletion() {
|
|
1293
|
+
return ownerCurrent() && completionCurrent()
|
|
1294
|
+
? completion === "queue-receipt"
|
|
1295
|
+
? inspectQueuedCompletion?.()
|
|
1296
|
+
: bound.ports.inspectCompletion.call(bound.completion)
|
|
1297
|
+
: undefined;
|
|
1298
|
+
},
|
|
1299
|
+
};
|
|
1300
|
+
};
|
|
1301
|
+
const prepareHeldReplyPlan = (ctx, effects, run, replyDelivery = "detached") => {
|
|
1302
|
+
const send = effects.sendTextReply, captured = { ...deps };
|
|
1303
|
+
return {
|
|
1304
|
+
captured,
|
|
1305
|
+
portsCurrent: () => effects.sendTextReply === send,
|
|
1306
|
+
async run(input) {
|
|
1307
|
+
const semantic = guardSelectedEffect(input.assertSemantic);
|
|
1308
|
+
const complete = () => {
|
|
1309
|
+
input.assertSemantic();
|
|
1310
|
+
if (!input.reportCompleted())
|
|
1311
|
+
throw new Error(`Held ${input.command.name} source completion refused.`);
|
|
1312
|
+
input.assertRecipient();
|
|
1313
|
+
};
|
|
1314
|
+
await run(captured, semantic, complete, async (text, options) => {
|
|
1315
|
+
complete();
|
|
1316
|
+
const deliver = async () => {
|
|
1317
|
+
input.assertRecipient();
|
|
1318
|
+
await send.call(effects, input.message.chat.id, input.message.message_id, text, {
|
|
1319
|
+
...options,
|
|
1320
|
+
target: {
|
|
1321
|
+
chatId: input.message.chat.id,
|
|
1322
|
+
threadId: input.message.message_thread_id,
|
|
1323
|
+
},
|
|
1324
|
+
assertAuthority: input.assertRecipient,
|
|
1325
|
+
});
|
|
1326
|
+
input.assertRecipient();
|
|
1327
|
+
};
|
|
1328
|
+
// Transport waiting never changes the already reported semantic completion or licenses another effect.
|
|
1329
|
+
if (replyDelivery === "inline")
|
|
1330
|
+
await deliver();
|
|
1331
|
+
else
|
|
1332
|
+
scheduleTelegramCommandEffect(ctx, input.command.name, "selected-reply", captured, deliver, input.assertRecipient);
|
|
1333
|
+
});
|
|
1334
|
+
return true;
|
|
1335
|
+
},
|
|
1336
|
+
};
|
|
1337
|
+
};
|
|
1338
|
+
const prepareHeldQueuePlan = (ctx, template) => {
|
|
1339
|
+
const captured = { ...deps }, owner = captured.heldTurn, enqueue = owner.enqueue;
|
|
1340
|
+
let retained;
|
|
1341
|
+
const portsCurrent = () => deps.heldTurn === owner &&
|
|
1342
|
+
owner.enqueue === enqueue &&
|
|
1343
|
+
(!template || template.isCurrent()) &&
|
|
1344
|
+
deps.heldTurn === owner &&
|
|
1345
|
+
owner.enqueue === enqueue;
|
|
1346
|
+
return {
|
|
1347
|
+
captured,
|
|
1348
|
+
portsCurrent,
|
|
1349
|
+
async run(input) {
|
|
1350
|
+
input.assertSemantic();
|
|
1351
|
+
if (template && !isDeepStrictEqual(input.command, template.command))
|
|
1352
|
+
return false;
|
|
1353
|
+
const { carrier, source } = input;
|
|
1354
|
+
if (!carrier || !source)
|
|
1355
|
+
return false;
|
|
1356
|
+
const signal = getTelegramUpdateExecutionFence(carrier)?.signal;
|
|
1357
|
+
if (!signal || signal.aborted)
|
|
1358
|
+
return false;
|
|
1359
|
+
const queue = input.queue ?? prepareTelegramDeferredQueueAdmission(carrier);
|
|
1360
|
+
if (!queue || !queue.isCurrent())
|
|
1361
|
+
return false;
|
|
1362
|
+
const ports = {
|
|
1363
|
+
isCurrent: queue.isCurrent,
|
|
1364
|
+
prepareCompletionScope: queue.prepareCompletionScope,
|
|
1365
|
+
inspectCompletion: queue.inspectCompletion,
|
|
1366
|
+
};
|
|
1367
|
+
if (Object.values(ports).some((port) => typeof port !== "function"))
|
|
1368
|
+
return false;
|
|
1369
|
+
const current = () => {
|
|
1370
|
+
if (!portsCurrent() ||
|
|
1371
|
+
!Object.entries(ports).every(([key, value]) => Reflect.get(queue, key) === value) ||
|
|
1372
|
+
!ports.isCurrent.call(queue))
|
|
1373
|
+
return false;
|
|
1374
|
+
try {
|
|
1375
|
+
input.assertRecipient();
|
|
1376
|
+
return portsCurrent() && ports.isCurrent.call(queue);
|
|
1377
|
+
}
|
|
1378
|
+
catch {
|
|
1379
|
+
return false;
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
const sameTarget = carrier.message_thread_id === input.message.message_thread_id;
|
|
1383
|
+
const message = carryTelegramUpdateExecutionFence(carrier, {
|
|
1384
|
+
...carrier,
|
|
1385
|
+
...input.message,
|
|
1386
|
+
pi_telegram_source_update_id: source.updateId,
|
|
1387
|
+
...(template ? { text: template.expanded, caption: undefined } : {}),
|
|
1388
|
+
...(sameTarget ? {} : { message_id: 0, reply_to_message: undefined }),
|
|
1389
|
+
});
|
|
1390
|
+
const completionSha256 = createHash("sha256")
|
|
1391
|
+
.update(JSON.stringify([
|
|
1392
|
+
"held-command-v1",
|
|
1393
|
+
randomUUID(),
|
|
1394
|
+
source,
|
|
1395
|
+
input.command,
|
|
1396
|
+
getTelegramCommandMessageTarget(message),
|
|
1397
|
+
]))
|
|
1398
|
+
.digest("hex");
|
|
1399
|
+
const admission = {
|
|
1400
|
+
assertCurrent: input.assertSemantic,
|
|
1401
|
+
report(receipts) {
|
|
1402
|
+
input.assertSemantic();
|
|
1403
|
+
const receipt = receipts[0];
|
|
1404
|
+
if (retained ||
|
|
1405
|
+
receipts.length !== 1 ||
|
|
1406
|
+
!receipt ||
|
|
1407
|
+
receipt.queueKind !== "prompt" ||
|
|
1408
|
+
receipt.journalBindingKey !== source.journalBindingKey ||
|
|
1409
|
+
receipt.sourceUpdateIds.length !== 1 ||
|
|
1410
|
+
receipt.sourceUpdateIds[0] !== source.updateId ||
|
|
1411
|
+
heldQueueReceipts.has(receipt.receiptId) ||
|
|
1412
|
+
!current())
|
|
1413
|
+
throw new Error("Held command needs one exact original queue receipt.");
|
|
1414
|
+
const expected = structuredClone(receipt);
|
|
1415
|
+
retained = {
|
|
1416
|
+
ctx,
|
|
1417
|
+
signal,
|
|
1418
|
+
receipt,
|
|
1419
|
+
queue,
|
|
1420
|
+
completionSha256,
|
|
1421
|
+
current: () => current() && isDeepStrictEqual(receipt, expected),
|
|
1422
|
+
};
|
|
1423
|
+
watchHeldQueueOwner(signal);
|
|
1424
|
+
heldQueueReceipts.set(receipt.receiptId, signal.aborted ? null : retained);
|
|
1425
|
+
if (!reportTelegramQueueAdmission([carrier], [receipt]))
|
|
1426
|
+
throw new Error("Held command queue admission refused.");
|
|
1427
|
+
},
|
|
1428
|
+
};
|
|
1429
|
+
if (template)
|
|
1430
|
+
await enqueue.call(owner, message, ctx, "prompt", admission);
|
|
1431
|
+
else
|
|
1432
|
+
await handleTelegramContinueCommand(message, ctx, {
|
|
1433
|
+
enqueueContinueTurn: (value, context) => enqueue.call(owner, value, context, "continue", admission),
|
|
1434
|
+
});
|
|
1435
|
+
return !!retained && current();
|
|
1436
|
+
},
|
|
1437
|
+
inspectQueuedCompletion() {
|
|
1438
|
+
if (!retained || !retained.current())
|
|
1439
|
+
return undefined;
|
|
1440
|
+
const proof = retained.queue.inspectCompletion(retained.receipt);
|
|
1441
|
+
if (proof && retained.current()) {
|
|
1442
|
+
heldQueueReceipts.delete(retained.receipt.receiptId);
|
|
1443
|
+
return { ...proof };
|
|
1444
|
+
}
|
|
1445
|
+
return undefined;
|
|
1446
|
+
},
|
|
1447
|
+
};
|
|
1448
|
+
};
|
|
1449
|
+
const heldPlans = {
|
|
1450
|
+
status: {
|
|
1451
|
+
completion: "before-effect",
|
|
1452
|
+
effect: "showStatus",
|
|
1453
|
+
prepare(ctx, effects) {
|
|
1454
|
+
const scopedStatus = effects?.showStatus;
|
|
1455
|
+
const captured = {
|
|
1456
|
+
...deps,
|
|
1457
|
+
showStatus: scopedStatus
|
|
1458
|
+
? (message, context, options) => {
|
|
1459
|
+
const address = getTelegramCommandMessageTarget(message);
|
|
1460
|
+
return scopedStatus.call(effects, address.chatId, address.replyToMessageId, context, address.threadId, options);
|
|
1461
|
+
}
|
|
1462
|
+
: deps.showStatus,
|
|
1463
|
+
};
|
|
1464
|
+
return {
|
|
1465
|
+
captured,
|
|
1466
|
+
portsCurrent: () => !effects || effects.showStatus === scopedStatus,
|
|
1467
|
+
async run({ message, command, assertRecipient }) {
|
|
1468
|
+
const recipientOptions = { assertAuthority: assertRecipient };
|
|
1469
|
+
return handleTelegramCommandRuntime(command.name, message, ctx, {
|
|
1470
|
+
...captured,
|
|
1471
|
+
assertExecutionCurrent: assertRecipient,
|
|
1472
|
+
showStatus: (value, context) => captured.showStatus(value, context, recipientOptions),
|
|
1473
|
+
}, command.args);
|
|
1474
|
+
},
|
|
1475
|
+
};
|
|
1476
|
+
},
|
|
1477
|
+
},
|
|
1478
|
+
abort: {
|
|
1479
|
+
completion: "after-semantics",
|
|
1480
|
+
effect: "sendTextReply",
|
|
1481
|
+
prepare(ctx, effects, _command, delivery) {
|
|
1482
|
+
return prepareHeldReplyPlan(ctx, effects, async (captured, semantic, _complete, reply) => {
|
|
1483
|
+
await handleTelegramAbortCommand({
|
|
1484
|
+
hasAbortHandler: semantic(captured.hasAbortHandler),
|
|
1485
|
+
hasActiveTelegramTurn: semantic(captured.hasActiveTelegramTurn),
|
|
1486
|
+
clearPendingModelSwitch: semantic(captured.clearPendingModelSwitch),
|
|
1487
|
+
cancelNextTransitionAnnouncements: captured.cancelNextTransitionAnnouncements &&
|
|
1488
|
+
semantic(captured.cancelNextTransitionAnnouncements),
|
|
1489
|
+
abortCurrentTurn: semantic(captured.abortCurrentTurn),
|
|
1490
|
+
setFoldQueuedPromptsIntoHistory: semantic(captured.setFoldQueuedPromptsIntoHistory),
|
|
1491
|
+
updateStatus: semantic(() => captured.updateStatus(ctx)),
|
|
1492
|
+
sendTextReply: reply,
|
|
1493
|
+
});
|
|
1494
|
+
}, delivery);
|
|
1495
|
+
},
|
|
1496
|
+
},
|
|
1497
|
+
stop: {
|
|
1498
|
+
completion: "after-semantics",
|
|
1499
|
+
effect: "sendTextReply",
|
|
1500
|
+
available: () => typeof deps.cancelNextTransitionAnnouncements === "function" &&
|
|
1501
|
+
typeof deps.clearQueuedTelegramItems === "function",
|
|
1502
|
+
prepare(ctx, effects, _command, delivery) {
|
|
1503
|
+
return prepareHeldReplyPlan(ctx, effects, async (captured, semantic, _complete, reply) => {
|
|
1504
|
+
await handleTelegramStopCommand({
|
|
1505
|
+
hasAbortHandler: semantic(captured.hasAbortHandler),
|
|
1506
|
+
clearPendingModelSwitch: semantic(captured.clearPendingModelSwitch),
|
|
1507
|
+
cancelNextTransitionAnnouncements: semantic(captured.cancelNextTransitionAnnouncements),
|
|
1508
|
+
clearQueuedTelegramItems: semantic(() => captured.clearQueuedTelegramItems(ctx)),
|
|
1509
|
+
setFoldQueuedPromptsIntoHistory: semantic(captured.setFoldQueuedPromptsIntoHistory),
|
|
1510
|
+
abortCurrentTurn: semantic(captured.abortCurrentTurn),
|
|
1511
|
+
updateStatus: semantic(() => captured.updateStatus(ctx)),
|
|
1512
|
+
sendTextReply: reply,
|
|
1513
|
+
});
|
|
1514
|
+
}, delivery);
|
|
1515
|
+
},
|
|
1516
|
+
},
|
|
1517
|
+
next: {
|
|
1518
|
+
completion: "after-semantics",
|
|
1519
|
+
effect: "sendTextReply",
|
|
1520
|
+
available: () => typeof deps.requestNextDispatchAnnouncement === "function" &&
|
|
1521
|
+
typeof deps.markActiveTurnNextAbortAnnouncement === "function",
|
|
1522
|
+
prepare(ctx, effects, _command, delivery) {
|
|
1523
|
+
return prepareHeldReplyPlan(ctx, effects, async (captured, semantic, complete, reply) => {
|
|
1524
|
+
await handleTelegramNextCommand({
|
|
1525
|
+
hasAbortHandler: semantic(captured.hasAbortHandler),
|
|
1526
|
+
isIdle: semantic(() => captured.isIdle(ctx)),
|
|
1527
|
+
hasQueuedItems: semantic(captured.hasQueuedTelegramItems),
|
|
1528
|
+
clearPendingModelSwitch: semantic(captured.clearPendingModelSwitch),
|
|
1529
|
+
abortCurrentTurn: semantic(captured.abortCurrentTurn),
|
|
1530
|
+
dispatchNextQueuedTurn: semantic(() => captured.dispatchNextQueuedTelegramTurn(ctx)),
|
|
1531
|
+
requestNextDispatchAnnouncement: semantic(captured.requestNextDispatchAnnouncement),
|
|
1532
|
+
markActiveTurnNextAbortAnnouncement: semantic(captured.markActiveTurnNextAbortAnnouncement),
|
|
1533
|
+
clearFoldForDispatch: semantic(() => captured.setFoldQueuedPromptsIntoHistory(false)),
|
|
1534
|
+
updateStatus() {
|
|
1535
|
+
semantic(() => captured.updateStatus(ctx))();
|
|
1536
|
+
complete();
|
|
1537
|
+
},
|
|
1538
|
+
sendTextReply: reply,
|
|
1539
|
+
});
|
|
1540
|
+
}, delivery);
|
|
1541
|
+
},
|
|
1542
|
+
},
|
|
1543
|
+
continue: {
|
|
1544
|
+
completion: "queue-receipt",
|
|
1545
|
+
available: () => typeof deps.heldTurn?.enqueue === "function",
|
|
1546
|
+
prepare(ctx) {
|
|
1547
|
+
return prepareHeldQueuePlan(ctx);
|
|
1548
|
+
},
|
|
1549
|
+
},
|
|
1550
|
+
};
|
|
1551
|
+
const getHeldTemplatePlan = (name) => {
|
|
1552
|
+
const owner = deps.heldTurn, templates = owner?.templates, get = templates?.getCommands, expand = templates?.expand;
|
|
1553
|
+
if (!owner ||
|
|
1554
|
+
!templates ||
|
|
1555
|
+
typeof owner.enqueue !== "function" ||
|
|
1556
|
+
typeof get !== "function" ||
|
|
1557
|
+
typeof expand !== "function" ||
|
|
1558
|
+
getTelegramReservedCommandNames().includes(name) ||
|
|
1559
|
+
findTelegramExtensionCommand(name))
|
|
1560
|
+
return undefined;
|
|
1561
|
+
const getIdentity = () => {
|
|
1562
|
+
const matching = get
|
|
1563
|
+
.call(templates)
|
|
1564
|
+
.filter((value) => value.command === name);
|
|
1565
|
+
const selected = matching[0];
|
|
1566
|
+
return matching.length === 1 &&
|
|
1567
|
+
selected &&
|
|
1568
|
+
typeof selected.path === "string" &&
|
|
1569
|
+
selected.path.trim()
|
|
1570
|
+
? { command: selected.command, path: selected.path }
|
|
1571
|
+
: undefined;
|
|
1572
|
+
};
|
|
1573
|
+
let identity;
|
|
1574
|
+
try {
|
|
1575
|
+
identity = getIdentity();
|
|
1576
|
+
}
|
|
1577
|
+
catch {
|
|
1578
|
+
return undefined;
|
|
1579
|
+
}
|
|
1580
|
+
if (!identity)
|
|
1581
|
+
return undefined;
|
|
1582
|
+
const current = () => {
|
|
1583
|
+
if (deps.heldTurn !== owner ||
|
|
1584
|
+
owner.templates !== templates ||
|
|
1585
|
+
templates.getCommands !== get ||
|
|
1586
|
+
templates.expand !== expand ||
|
|
1587
|
+
findTelegramExtensionCommand(name))
|
|
1588
|
+
return false;
|
|
1589
|
+
try {
|
|
1590
|
+
return (isDeepStrictEqual(getIdentity(), identity) &&
|
|
1591
|
+
deps.heldTurn === owner &&
|
|
1592
|
+
owner.templates === templates &&
|
|
1593
|
+
templates.getCommands === get &&
|
|
1594
|
+
templates.expand === expand &&
|
|
1595
|
+
!findTelegramExtensionCommand(name));
|
|
1596
|
+
}
|
|
1597
|
+
catch {
|
|
1598
|
+
return false;
|
|
1599
|
+
}
|
|
1600
|
+
};
|
|
1601
|
+
if (!current())
|
|
1602
|
+
return undefined;
|
|
1603
|
+
return {
|
|
1604
|
+
completion: "queue-receipt",
|
|
1605
|
+
template: true,
|
|
1606
|
+
prepare(ctx, _effects, command) {
|
|
1607
|
+
if (!command || command.name !== name || !current())
|
|
1608
|
+
return undefined;
|
|
1609
|
+
const captured = { ...command };
|
|
1610
|
+
let expanded;
|
|
1611
|
+
try {
|
|
1612
|
+
expanded = expand.call(templates, captured.name, captured.args);
|
|
1613
|
+
}
|
|
1614
|
+
catch {
|
|
1615
|
+
return undefined;
|
|
1616
|
+
}
|
|
1617
|
+
if (typeof expanded !== "string" || !current())
|
|
1618
|
+
return undefined;
|
|
1619
|
+
return prepareHeldQueuePlan(ctx, {
|
|
1620
|
+
command: captured,
|
|
1621
|
+
expanded,
|
|
1622
|
+
isCurrent() {
|
|
1623
|
+
if (!current())
|
|
1624
|
+
return false;
|
|
1625
|
+
try {
|
|
1626
|
+
return (expand.call(templates, captured.name, captured.args) ===
|
|
1627
|
+
expanded && current());
|
|
1628
|
+
}
|
|
1629
|
+
catch {
|
|
1630
|
+
return false;
|
|
1631
|
+
}
|
|
1632
|
+
},
|
|
1633
|
+
});
|
|
1634
|
+
},
|
|
1635
|
+
};
|
|
1636
|
+
};
|
|
1637
|
+
const getHeldPlan = (name, effects) => {
|
|
1638
|
+
if (!Object.hasOwn(heldPlans, name))
|
|
1639
|
+
return getHeldTemplatePlan(name);
|
|
1640
|
+
const plan = heldPlans[name];
|
|
1641
|
+
if (plan.available?.() === false)
|
|
1642
|
+
return undefined;
|
|
1643
|
+
if ((plan.effect &&
|
|
1644
|
+
effects !== undefined &&
|
|
1645
|
+
(!effects || typeof effects[plan.effect] !== "function")) ||
|
|
1646
|
+
(plan.effect === "sendTextReply" && !effects) ||
|
|
1647
|
+
(plan.effect === "showStatus" &&
|
|
1648
|
+
effects === undefined &&
|
|
1649
|
+
typeof deps.showStatus !== "function"))
|
|
1650
|
+
return undefined;
|
|
1651
|
+
return plan;
|
|
1652
|
+
};
|
|
1653
|
+
const prepareSelectedPlan = (command, messages, ctx, admission) => {
|
|
1654
|
+
const original = messages[0];
|
|
1655
|
+
if (!["status", "abort", "stop", "next"].includes(command.name) ||
|
|
1656
|
+
messages.length !== 1 ||
|
|
1657
|
+
!original ||
|
|
1658
|
+
typeof admission.assertSourceCurrent !== "function" ||
|
|
1659
|
+
typeof admission.assertRecipientCurrent !== "function" ||
|
|
1660
|
+
typeof admission.reportCompleted !== "function")
|
|
1661
|
+
return undefined;
|
|
1662
|
+
const captured = { ...deps }, parsed = { ...command }, message = parsed.name === "status"
|
|
1663
|
+
? copySelectedMenuCarrier(original)
|
|
1664
|
+
: copySelectedReplyAnchor(original);
|
|
1665
|
+
const assertRecipientCurrent = admission.assertRecipientCurrent, report = admission.reportCompleted;
|
|
1666
|
+
const effects = parsed.name === "status"
|
|
1667
|
+
? undefined
|
|
1668
|
+
: {
|
|
1669
|
+
async sendTextReply(_chat, _anchor, text, options) {
|
|
1670
|
+
await captured.sendTextReply(message, text, {
|
|
1671
|
+
parseMode: options.parseMode,
|
|
1672
|
+
assertAuthority: options.assertAuthority,
|
|
1673
|
+
});
|
|
1674
|
+
},
|
|
1675
|
+
};
|
|
1676
|
+
const plan = getHeldPlan(parsed.name, effects);
|
|
1677
|
+
if (!plan ||
|
|
1678
|
+
plan.completion === "queue-receipt" ||
|
|
1679
|
+
(parsed.name !== "status" && typeof captured.sendTextReply !== "function"))
|
|
1680
|
+
return undefined;
|
|
1681
|
+
const prepared = plan.prepare(ctx, effects, parsed, "inline");
|
|
1682
|
+
if (!prepared)
|
|
1683
|
+
return undefined;
|
|
1684
|
+
const semantic = createSelectedSemanticGuard(admission, () => captured.assertExecutionCurrent?.(original), plan.completion === "before-effect");
|
|
1685
|
+
const assertCurrent = () => {
|
|
1686
|
+
semantic();
|
|
1687
|
+
if (!prepared.portsCurrent())
|
|
1688
|
+
throw new Error(`Selected ${parsed.name} plan owner is unavailable.`);
|
|
1689
|
+
};
|
|
1690
|
+
return issueSelectedOnce(assertCurrent, async () => {
|
|
1691
|
+
if (plan.completion === "before-effect" && !report.call(admission))
|
|
1692
|
+
return false;
|
|
1693
|
+
return prepared.run({
|
|
1694
|
+
message,
|
|
1695
|
+
command: parsed,
|
|
1696
|
+
assertSemantic: assertCurrent,
|
|
1697
|
+
assertRecipient: assertRecipientCurrent,
|
|
1698
|
+
reportCompleted: () => report.call(admission),
|
|
1699
|
+
});
|
|
1700
|
+
});
|
|
1701
|
+
};
|
|
1702
|
+
const prepareSelectedHelp = (name) => (command, messages, ctx, admission) => {
|
|
1703
|
+
const original = messages[0];
|
|
1704
|
+
if (command.name !== name ||
|
|
1705
|
+
messages.length !== 1 ||
|
|
1706
|
+
!original ||
|
|
1707
|
+
!canPairTelegramUserFromCommandMessage(original) ||
|
|
1708
|
+
original.from?.id === undefined ||
|
|
1709
|
+
original.from.is_bot)
|
|
1710
|
+
return undefined;
|
|
1711
|
+
const captured = { ...deps }, userId = original.from.id;
|
|
1712
|
+
if (captured.getAllowedUserId() !== userId)
|
|
1713
|
+
return undefined;
|
|
1714
|
+
const message = {
|
|
1715
|
+
...copySelectedReplyAnchor(original),
|
|
1716
|
+
from: { ...original.from },
|
|
1717
|
+
};
|
|
1718
|
+
const assertRecipientCurrent = admission.assertRecipientCurrent, reportCompleted = admission.reportCompleted;
|
|
1719
|
+
const assertAdmissionCurrent = createSelectedSemanticGuard(admission, () => captured.assertExecutionCurrent?.(original), false);
|
|
1720
|
+
const assertSemanticCurrent = () => {
|
|
1721
|
+
assertAdmissionCurrent();
|
|
1722
|
+
if (captured.getAllowedUserId() !== userId)
|
|
1723
|
+
throw new Error(`Selected ${name} operator authority is unavailable.`);
|
|
1724
|
+
};
|
|
1725
|
+
return issueSelectedOnce(assertSemanticCurrent, () => handleTelegramHelpCommand(name, message, ctx, {
|
|
1726
|
+
...captured,
|
|
1727
|
+
persistAllowedUserId: async () => {
|
|
1728
|
+
throw new Error(`Selected ${name} cannot acquire pairing authority.`);
|
|
1729
|
+
},
|
|
1730
|
+
}, { assertSemanticCurrent, assertRecipientCurrent, reportCompleted }));
|
|
1731
|
+
};
|
|
1732
|
+
const prepareSelectedConfirmation = (name) => (command, messages, ctx, admission) => {
|
|
1733
|
+
const original = messages[0], captured = { ...deps };
|
|
1734
|
+
if (command.name !== name ||
|
|
1735
|
+
messages.length !== 1 ||
|
|
1736
|
+
!original ||
|
|
1737
|
+
!captured.sendInteractiveMessage)
|
|
1738
|
+
return undefined;
|
|
1739
|
+
const target = getTelegramCommandMessageTarget(original);
|
|
1740
|
+
const assertRecipientCurrent = admission.assertRecipientCurrent, reportCompleted = admission.reportCompleted;
|
|
1741
|
+
const openConfirmation = name === "new"
|
|
1742
|
+
? openTelegramNewConfirmation
|
|
1743
|
+
: openTelegramCompactConfirmation;
|
|
1744
|
+
const assertSemanticCurrent = createSelectedSemanticGuard(admission, () => captured.assertExecutionCurrent?.(original), true);
|
|
1745
|
+
return issueSelectedOnce(assertSemanticCurrent, async () => {
|
|
1746
|
+
if (!reportCompleted())
|
|
1747
|
+
return false;
|
|
1748
|
+
assertRecipientCurrent();
|
|
1749
|
+
scheduleTelegramCommandEffect(ctx, name, "confirmation-render", captured, () => openConfirmation(target, { sendInteractiveMessage: captured.sendInteractiveMessage }, assertRecipientCurrent), assertRecipientCurrent);
|
|
1750
|
+
return true;
|
|
1751
|
+
});
|
|
1752
|
+
};
|
|
1753
|
+
return Object.assign(handle, {
|
|
1754
|
+
/** Command-only producer execution; Routing must supply the captured plan and released exact-source authority. */
|
|
1755
|
+
prepareSelectedExtensionCommand(prepared, messages, ctx, admission) {
|
|
1756
|
+
const original = messages[0], captured = { ...deps }, plan = prepared.plan;
|
|
1757
|
+
if (messages.length !== 1 ||
|
|
1758
|
+
!original ||
|
|
1759
|
+
plan.kind !== "command-only" ||
|
|
1760
|
+
typeof captured.sendTextReply !== "function" ||
|
|
1761
|
+
typeof admission.reportCompleted !== "function")
|
|
1762
|
+
return undefined;
|
|
1763
|
+
const execute = plan.execute, assertRegistrationCurrent = prepared.assertRegistrationCurrent;
|
|
1764
|
+
const assertSourceCurrent = admission.assertSourceCurrent, recipientCurrent = admission.assertRecipientCurrent;
|
|
1765
|
+
const reportCompleted = admission.reportCompleted;
|
|
1766
|
+
// Only a body-free fixed recipient/anchor enters detached delivery, never the original's admission symbols.
|
|
1767
|
+
const message = copySelectedReplyAnchor(original);
|
|
1768
|
+
const assertRecipientCurrent = () => {
|
|
1769
|
+
if (captured.isContextActive?.(ctx) === false)
|
|
1770
|
+
throw new Error("Selected extension context changed.");
|
|
1771
|
+
assertRegistrationCurrent();
|
|
1772
|
+
recipientCurrent();
|
|
1773
|
+
assertRegistrationCurrent();
|
|
1774
|
+
if (captured.isContextActive?.(ctx) === false)
|
|
1775
|
+
throw new Error("Selected extension context changed.");
|
|
1776
|
+
};
|
|
1777
|
+
const assertCurrent = () => {
|
|
1778
|
+
assertSourceCurrent();
|
|
1779
|
+
captured.assertExecutionCurrent?.(original);
|
|
1780
|
+
assertRecipientCurrent();
|
|
1781
|
+
assertSourceCurrent();
|
|
1782
|
+
captured.assertExecutionCurrent?.(original);
|
|
1783
|
+
};
|
|
1784
|
+
try {
|
|
1785
|
+
assertCurrent();
|
|
1786
|
+
}
|
|
1787
|
+
catch {
|
|
1788
|
+
return undefined;
|
|
1789
|
+
}
|
|
1790
|
+
let issued = false, reportAttempted = false;
|
|
1791
|
+
const execution = Object.freeze({
|
|
1792
|
+
assertCurrent,
|
|
1793
|
+
reportCompleted() {
|
|
1794
|
+
if (!issued || reportAttempted)
|
|
1795
|
+
return false;
|
|
1796
|
+
// Lost/refused reports never authorize another completion attempt or producer execution.
|
|
1797
|
+
reportAttempted = true;
|
|
1798
|
+
assertCurrent();
|
|
1799
|
+
return reportCompleted();
|
|
1800
|
+
},
|
|
1801
|
+
reply(text) {
|
|
1802
|
+
assertRecipientCurrent();
|
|
1803
|
+
if (typeof text !== "string")
|
|
1804
|
+
throw new Error("Selected extension reply requires text.");
|
|
1805
|
+
scheduleTelegramCommandEffect(ctx, "extension", "selected-reply", captured, () => captured.sendTextReply(message, text, {
|
|
1806
|
+
assertAuthority: assertRecipientCurrent,
|
|
1807
|
+
}), assertRecipientCurrent);
|
|
1808
|
+
},
|
|
1809
|
+
});
|
|
1810
|
+
return async () => {
|
|
1811
|
+
if (issued)
|
|
1812
|
+
return;
|
|
1813
|
+
issued = true;
|
|
1814
|
+
assertCurrent();
|
|
1815
|
+
await execute(execution);
|
|
1816
|
+
// Return, rejection and reply delivery are deliberately not semantic completion reports.
|
|
1817
|
+
};
|
|
1818
|
+
},
|
|
1819
|
+
/** Selected help/start require the existing authenticated owner; cold pairing remains ordinary first contact. */
|
|
1820
|
+
prepareSelectedStartCommand: prepareSelectedHelp("start"),
|
|
1821
|
+
prepareSelectedHelpCommand: prepareSelectedHelp("help"),
|
|
1822
|
+
/** Selected new handles only confirmation; the later authenticated callback owns replacement admission. */
|
|
1823
|
+
prepareSelectedNewCommand: prepareSelectedConfirmation("new"),
|
|
1824
|
+
/** Selected compaction handles only confirmation; the later authenticated callback owns actual compaction. */
|
|
1825
|
+
prepareSelectedCompactCommand: prepareSelectedConfirmation("compact"),
|
|
1826
|
+
/** Bare naming completes only after the exact dialog owner confirms current publication, not delivery alone. */
|
|
1827
|
+
prepareSelectedNameDialogCommand(command, messages, ctx, admission) {
|
|
1828
|
+
const original = messages[0], captured = { ...deps };
|
|
1829
|
+
if (command.name !== "name" ||
|
|
1830
|
+
command.args.trim() ||
|
|
1831
|
+
messages.length !== 1 ||
|
|
1832
|
+
!original ||
|
|
1833
|
+
!captured.openThreadNameDialog)
|
|
1834
|
+
return undefined;
|
|
1835
|
+
const message = copySelectedReplyAnchor(original);
|
|
1836
|
+
const assertSourceCurrent = admission.assertSourceCurrent, assertRecipientCurrent = admission.assertRecipientCurrent;
|
|
1837
|
+
const reportCompleted = admission.reportCompleted;
|
|
1838
|
+
const assertSemanticCurrent = createSelectedSemanticGuard(admission, () => captured.assertExecutionCurrent?.(original), true);
|
|
1839
|
+
return issueSelectedOnce(assertSemanticCurrent, async () => {
|
|
1840
|
+
const publication = await captured.openThreadNameDialog(message, ctx, {
|
|
1841
|
+
assertSemanticCurrent,
|
|
1842
|
+
assertRecipientCurrent,
|
|
1843
|
+
});
|
|
1844
|
+
assertSemanticCurrent();
|
|
1845
|
+
const assertPublished = publication?.assertPublished;
|
|
1846
|
+
if (typeof assertPublished !== "function")
|
|
1847
|
+
throw new Error("Selected name dialog publication is unconfirmed.");
|
|
1848
|
+
assertPublished();
|
|
1849
|
+
assertSemanticCurrent();
|
|
1850
|
+
assertPublished();
|
|
1851
|
+
assertSourceCurrent();
|
|
1852
|
+
captured.assertExecutionCurrent?.(original);
|
|
1853
|
+
if (!reportCompleted())
|
|
1854
|
+
throw new Error("Selected name source completion refused.");
|
|
1855
|
+
return true;
|
|
1856
|
+
});
|
|
1857
|
+
},
|
|
1858
|
+
/** Explicit naming retains source authority until its owner result; bare dialogs use their own publication leaf. */
|
|
1859
|
+
prepareSelectedNameCommand(command, messages, ctx, admission) {
|
|
1860
|
+
const original = messages[0], threadName = command.args.trim(), captured = { ...deps };
|
|
1861
|
+
if (command.name !== "name" ||
|
|
1862
|
+
messages.length !== 1 ||
|
|
1863
|
+
!original ||
|
|
1864
|
+
!threadName ||
|
|
1865
|
+
(/^[A-Z]$/.test(threadName)
|
|
1866
|
+
? !captured.resetCurrentThreadName
|
|
1867
|
+
: !captured.renameCurrentThread))
|
|
1868
|
+
return undefined;
|
|
1869
|
+
const message = copySelectedReplyAnchor(original);
|
|
1870
|
+
const assertRecipientCurrent = admission.assertRecipientCurrent, reportCompleted = admission.reportCompleted;
|
|
1871
|
+
const assertSemanticCurrent = createSelectedSemanticGuard(admission, () => captured.assertExecutionCurrent?.(original), true);
|
|
1872
|
+
return issueSelectedOnce(assertSemanticCurrent, async () => {
|
|
1873
|
+
await handleTelegramNameCommand(message, ctx, threadName, captured, {
|
|
1874
|
+
assertSemanticCurrent,
|
|
1875
|
+
assertRecipientCurrent,
|
|
1876
|
+
reportCompleted,
|
|
1877
|
+
});
|
|
1878
|
+
return true;
|
|
1879
|
+
});
|
|
1880
|
+
},
|
|
1881
|
+
/** Genuine leader originals capture their warm queue owner before release; no hold copy or semantic-completion report. */
|
|
1882
|
+
prepareSelectedQueueCommand(command, messages, ctx, admission) {
|
|
1883
|
+
const original = messages[0], parsed = { ...command };
|
|
1884
|
+
if (messages.length !== 1 ||
|
|
1885
|
+
!original ||
|
|
1886
|
+
typeof admission.assertSourceCurrent !== "function" ||
|
|
1887
|
+
typeof admission.assertRecipientCurrent !== "function")
|
|
1888
|
+
return undefined;
|
|
1889
|
+
const raw = Reflect.get(original, "text") ?? Reflect.get(original, "caption"), operator = deps.getAllowedUserId();
|
|
1890
|
+
const actual = typeof raw === "string" ? parseTelegramCommand(raw) : undefined;
|
|
1891
|
+
if (!isDeepStrictEqual(actual, parsed) ||
|
|
1892
|
+
typeof operator !== "number" ||
|
|
1893
|
+
original.chat.type !== "private" ||
|
|
1894
|
+
original.chat.id !== operator ||
|
|
1895
|
+
original.from?.id !== operator ||
|
|
1896
|
+
original.from.is_bot ||
|
|
1897
|
+
admission.target.chatId !== operator ||
|
|
1898
|
+
!Number.isSafeInteger(admission.target.threadId) ||
|
|
1899
|
+
admission.target.threadId < 1)
|
|
1900
|
+
return undefined;
|
|
1901
|
+
const plan = getHeldPlan(parsed.name);
|
|
1902
|
+
if (!plan || plan.completion !== "queue-receipt")
|
|
1903
|
+
return undefined;
|
|
1904
|
+
const source = inspectTelegramDeferredSource(original), queue = prepareTelegramDeferredQueueAdmission(original);
|
|
1905
|
+
if (!source || source.completionSha256 || !queue?.isCurrent())
|
|
1906
|
+
return undefined;
|
|
1907
|
+
const prepared = plan.prepare(ctx, undefined, parsed);
|
|
1908
|
+
if (!prepared)
|
|
1909
|
+
return undefined;
|
|
1910
|
+
const target = { ...admission.target }, originalProjection = structuredClone(copySelectedMenuCarrier(original));
|
|
1911
|
+
const message = structuredClone(originalProjection);
|
|
1912
|
+
message.message_thread_id = target.threadId;
|
|
1913
|
+
const assertSource = admission.assertSourceCurrent, assertRecipient = admission.assertRecipientCurrent;
|
|
1914
|
+
const assertCurrent = () => {
|
|
1915
|
+
assertSource.call(admission);
|
|
1916
|
+
prepared.captured.assertExecutionCurrent?.(original);
|
|
1917
|
+
assertRecipient.call(admission);
|
|
1918
|
+
if (deps.getAllowedUserId() !== operator ||
|
|
1919
|
+
!queue.isCurrent() ||
|
|
1920
|
+
!prepared.portsCurrent() ||
|
|
1921
|
+
!isDeepStrictEqual(copySelectedMenuCarrier(original), originalProjection) ||
|
|
1922
|
+
!isDeepStrictEqual(inspectTelegramDeferredSource(original), source))
|
|
1923
|
+
throw new Error("Selected queue command source/owner changed.");
|
|
1924
|
+
assertSource.call(admission);
|
|
1925
|
+
prepared.captured.assertExecutionCurrent?.(original);
|
|
1926
|
+
};
|
|
1927
|
+
const execute = issueSelectedOnce(assertCurrent, async () => {
|
|
1928
|
+
const release = acquireTelegramUpdateRouting(original);
|
|
1929
|
+
try {
|
|
1930
|
+
assertCurrent();
|
|
1931
|
+
return await prepared.run({
|
|
1932
|
+
message,
|
|
1933
|
+
carrier: original,
|
|
1934
|
+
source: { ...source },
|
|
1935
|
+
queue,
|
|
1936
|
+
command: { ...parsed },
|
|
1937
|
+
assertSemantic: assertCurrent,
|
|
1938
|
+
assertRecipient,
|
|
1939
|
+
reportCompleted() {
|
|
1940
|
+
throw new Error("Queue admission cannot report semantic source removal.");
|
|
1941
|
+
},
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
finally {
|
|
1945
|
+
release();
|
|
1946
|
+
}
|
|
1947
|
+
});
|
|
1948
|
+
return {
|
|
1949
|
+
get source() {
|
|
1950
|
+
return { ...source };
|
|
1951
|
+
},
|
|
1952
|
+
get command() {
|
|
1953
|
+
return { ...parsed };
|
|
1954
|
+
},
|
|
1955
|
+
execute,
|
|
1956
|
+
inspectCompletion() {
|
|
1957
|
+
try {
|
|
1958
|
+
assertRecipient.call(admission);
|
|
1959
|
+
if (deps.getAllowedUserId() !== operator ||
|
|
1960
|
+
!queue.isCurrent() ||
|
|
1961
|
+
!prepared.portsCurrent())
|
|
1962
|
+
return undefined;
|
|
1963
|
+
const proof = prepared.inspectQueuedCompletion?.();
|
|
1964
|
+
assertRecipient.call(admission);
|
|
1965
|
+
return deps.getAllowedUserId() === operator &&
|
|
1966
|
+
queue.isCurrent() &&
|
|
1967
|
+
prepared.portsCurrent()
|
|
1968
|
+
? proof
|
|
1969
|
+
: undefined;
|
|
1970
|
+
}
|
|
1971
|
+
catch {
|
|
1972
|
+
return undefined;
|
|
1973
|
+
}
|
|
1974
|
+
},
|
|
1975
|
+
};
|
|
1976
|
+
},
|
|
1977
|
+
/** Selected leaders reuse registry semantics; native source/recipient admission and inline transport remain role-owned. */
|
|
1978
|
+
prepareSelectedCommand: prepareSelectedPlan,
|
|
1979
|
+
/** Existing native publication boundary; unrelated/legacy receipts keep their ordinary owner. */
|
|
1980
|
+
prepareHeldQueueReceipt: ((receipt, queueOwner, context, isWorkerCurrent) => {
|
|
1981
|
+
const record = heldQueueReceipts.get(receipt.receiptId);
|
|
1982
|
+
if (record === null)
|
|
1983
|
+
throw new Error("Held queue receipt publication authority changed.");
|
|
1984
|
+
if (!record)
|
|
1985
|
+
return undefined;
|
|
1986
|
+
if (context !== record.ctx ||
|
|
1987
|
+
!isWorkerCurrent() ||
|
|
1988
|
+
!record.current() ||
|
|
1989
|
+
!isDeepStrictEqual(record.receipt, receipt))
|
|
1990
|
+
throw new Error("Held queue receipt publication authority changed.");
|
|
1991
|
+
const scope = record.queue.prepareCompletionScope(record.receipt, queueOwner, record.completionSha256);
|
|
1992
|
+
if (!scope || !isWorkerCurrent() || !record.current())
|
|
1993
|
+
throw new Error("Held queue receipt original scope is unavailable.");
|
|
1994
|
+
return [scope];
|
|
1995
|
+
}),
|
|
1996
|
+
/** Pure registry/effect availability; never reads a source or activates future recipient authority. */
|
|
1997
|
+
canPrepareHeldCommand(name, effects) {
|
|
1998
|
+
return getHeldPlan(name, effects) !== undefined;
|
|
1999
|
+
},
|
|
2000
|
+
/** Compile one registry plan from a saved original; no admission, recipient activation or update-handler replay. */
|
|
2001
|
+
prepareHeldCommand(name, readiness, ctx, admission, effects) {
|
|
2002
|
+
const plan = getHeldPlan(name, effects);
|
|
2003
|
+
if (!plan)
|
|
2004
|
+
return undefined;
|
|
2005
|
+
const value = plan.template
|
|
2006
|
+
? readiness.snapshot.update.message
|
|
2007
|
+
: undefined;
|
|
2008
|
+
const command = value &&
|
|
2009
|
+
typeof value === "object" &&
|
|
2010
|
+
typeof Reflect.get(value, "text") === "string"
|
|
2011
|
+
? parseTelegramCommand(Reflect.get(value, "text"))
|
|
2012
|
+
: undefined;
|
|
2013
|
+
const prepared = plan.prepare(ctx, effects, command);
|
|
2014
|
+
if (!prepared)
|
|
2015
|
+
return undefined;
|
|
2016
|
+
return prepareHeldSelectedCommand(name, readiness, admission, prepared.captured, prepared.portsCurrent, plan.completion, prepared.run, prepared.inspectQueuedCompletion);
|
|
2017
|
+
},
|
|
2018
|
+
/** Warm menu-only issuance; completion reporting is not a receipt or a durable removal ACK. */
|
|
2019
|
+
prepareSelectedMenuCommand(command, messages, ctx, admission) {
|
|
2020
|
+
const action = buildTelegramCommandAction(command.name);
|
|
2021
|
+
if (messages.length !== 1 ||
|
|
2022
|
+
!messages[0] ||
|
|
2023
|
+
!["status", "model", "thinking", "queue", "settings"].includes(action.kind) ||
|
|
2024
|
+
(action.kind === "settings" && !deps.openSettingsMenu))
|
|
2025
|
+
return undefined;
|
|
2026
|
+
if (action.kind === "status")
|
|
2027
|
+
return prepareSelectedPlan(command, messages, ctx, admission);
|
|
2028
|
+
const original = messages[0], name = command.name, args = command.args;
|
|
2029
|
+
if (typeof admission.assertSourceCurrent !== "function" ||
|
|
2030
|
+
typeof admission.assertRecipientCurrent !== "function" ||
|
|
2031
|
+
typeof admission.reportCompleted !== "function")
|
|
2032
|
+
return undefined;
|
|
2033
|
+
const assertSourceCurrent = admission.assertSourceCurrent, assertRecipientCurrent = admission.assertRecipientCurrent;
|
|
2034
|
+
const reportCompleted = admission.reportCompleted, assertExecutionCurrent = deps.assertExecutionCurrent;
|
|
2035
|
+
const assertIssuanceCurrent = () => {
|
|
2036
|
+
assertSourceCurrent.call(admission);
|
|
2037
|
+
assertExecutionCurrent?.call(deps, original);
|
|
2038
|
+
assertRecipientCurrent();
|
|
2039
|
+
// Recipient observations can revoke the source too; never complete from an earlier source sample.
|
|
2040
|
+
assertSourceCurrent.call(admission);
|
|
2041
|
+
assertExecutionCurrent?.call(deps, original);
|
|
2042
|
+
};
|
|
2043
|
+
// Preparation proves branch availability, not the future released recipient's active authority.
|
|
2044
|
+
// Detached rendering must not inherit the original admission/fence or a mutable target carrier.
|
|
2045
|
+
const message = copySelectedMenuCarrier(original);
|
|
2046
|
+
const menuDeps = { ...deps };
|
|
2047
|
+
const recipientOptions = { assertAuthority: assertRecipientCurrent };
|
|
2048
|
+
const recipientDeps = {
|
|
2049
|
+
...deps,
|
|
2050
|
+
assertExecutionCurrent: assertRecipientCurrent,
|
|
2051
|
+
showStatus: (value, context) => menuDeps.showStatus(value, context, recipientOptions),
|
|
2052
|
+
openModelMenu: (value, context) => menuDeps.openModelMenu(value, context, recipientOptions),
|
|
2053
|
+
openThinkingMenu: (value, context) => menuDeps.openThinkingMenu(value, context, recipientOptions),
|
|
2054
|
+
openQueueMenu: (value, context) => menuDeps.openQueueMenu(value, context, recipientOptions),
|
|
2055
|
+
openSettingsMenu: menuDeps.openSettingsMenu &&
|
|
2056
|
+
((value, context) => menuDeps.openSettingsMenu(value, context, recipientOptions)),
|
|
2057
|
+
};
|
|
2058
|
+
return issueSelectedOnce(assertIssuanceCurrent, async () => reportCompleted.call(admission) &&
|
|
2059
|
+
handleTelegramCommandRuntime(name, message, ctx, recipientDeps, args));
|
|
2060
|
+
},
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
export function createTelegramCommandOrPromptDispatcher(deps) {
|
|
2064
|
+
const dispatchPromptTemplate = async (command, messages, ctx) => {
|
|
2065
|
+
const first = messages[0];
|
|
2066
|
+
if (!first)
|
|
2067
|
+
return false;
|
|
2068
|
+
deps.assertExecutionCurrent?.(first);
|
|
2069
|
+
const expanded = command && deps.expandPromptTemplateCommand?.(command.name, command.args);
|
|
2070
|
+
if (expanded === undefined)
|
|
2071
|
+
return false;
|
|
2072
|
+
deps.assertExecutionCurrent?.(first);
|
|
2073
|
+
const replaced = deps.replaceMessageText(first, expanded);
|
|
2074
|
+
deps.assertExecutionCurrent?.(first);
|
|
2075
|
+
await deps.enqueueTurn([replaced, ...messages.slice(1)], ctx);
|
|
2076
|
+
return true;
|
|
2077
|
+
};
|
|
2078
|
+
return async (messages, ctx) => {
|
|
2079
|
+
const firstMessage = messages[0];
|
|
2080
|
+
if (!firstMessage)
|
|
2081
|
+
return;
|
|
2082
|
+
if (deps.shouldIgnoreMessages?.(messages))
|
|
2083
|
+
return;
|
|
2084
|
+
deps.assertExecutionCurrent?.(firstMessage);
|
|
2085
|
+
if (await deps.consumeThreadNameInput?.(messages, ctx)) {
|
|
2086
|
+
deps.assertExecutionCurrent?.(firstMessage);
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
const command = parseTelegramCommand(deps.extractRawText(messages));
|
|
2090
|
+
const handled = await deps.handleCommand(command?.name, firstMessage, ctx, command?.args);
|
|
2091
|
+
deps.assertExecutionCurrent?.(firstMessage);
|
|
2092
|
+
if (handled)
|
|
2093
|
+
return;
|
|
2094
|
+
if (command && deps.executeExtensionCommand) {
|
|
2095
|
+
const handledByExtension = await deps.executeExtensionCommand(command, messages[0], ctx);
|
|
2096
|
+
deps.assertExecutionCurrent?.(firstMessage);
|
|
2097
|
+
if (handledByExtension)
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
if (await dispatchPromptTemplate(command, messages, ctx))
|
|
2101
|
+
return;
|
|
2102
|
+
deps.assertExecutionCurrent?.(firstMessage);
|
|
2103
|
+
await deps.enqueueTurn(messages, ctx);
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
function scheduleTelegramCommandEffect(ctx, command, phase, deps, effect, assertExecutionCurrent) {
|
|
2107
|
+
const work = deps.beginCommandEffectWork?.();
|
|
2108
|
+
let started = false, outcome = "settled";
|
|
2109
|
+
void Promise.resolve()
|
|
2110
|
+
.then(async () => {
|
|
2111
|
+
if (deps.isContextActive?.(ctx) === false)
|
|
2112
|
+
return;
|
|
2113
|
+
assertExecutionCurrent?.();
|
|
2114
|
+
started = true;
|
|
2115
|
+
await effect();
|
|
2116
|
+
assertExecutionCurrent?.();
|
|
2117
|
+
})
|
|
2118
|
+
.catch((error) => {
|
|
2119
|
+
// A failed started effect cannot prove non-issuance; never turn its uncertainty into known idle.
|
|
2120
|
+
if (started &&
|
|
2121
|
+
!(error instanceof TelegramApiAuthorityError && !error.requestIssued))
|
|
2122
|
+
outcome = "unconfirmed";
|
|
2123
|
+
try {
|
|
2124
|
+
deps.recordRuntimeEvent?.("telegram-command", error, {
|
|
2125
|
+
command,
|
|
2126
|
+
phase,
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
catch {
|
|
2130
|
+
// Effect diagnostics cannot create an unhandled detached Promise.
|
|
2131
|
+
}
|
|
2132
|
+
})
|
|
2133
|
+
.finally(() => work?.settle(outcome));
|
|
2134
|
+
}
|
|
2135
|
+
async function handleTelegramHelpCommand(commandName, message, ctx, deps, admission) {
|
|
2136
|
+
const assertSemanticCurrent = admission?.assertSemanticCurrent ??
|
|
2137
|
+
(() => deps.assertExecutionCurrent?.(message));
|
|
2138
|
+
admission?.assertSemanticCurrent();
|
|
2139
|
+
if (message.from?.id !== undefined &&
|
|
2140
|
+
canPairTelegramUserFromCommandMessage(message)) {
|
|
2141
|
+
const allowed = await pairTelegramUserIfNeeded(message.from.id, {
|
|
2142
|
+
allowedUserId: deps.getAllowedUserId(),
|
|
2143
|
+
ctx: undefined,
|
|
2144
|
+
persistAllowedUserId: deps.persistAllowedUserId,
|
|
2145
|
+
updateStatus: () => deps.updateStatus(ctx),
|
|
2146
|
+
assertExecutionCurrent: assertSemanticCurrent,
|
|
2147
|
+
});
|
|
2148
|
+
if (!allowed)
|
|
2149
|
+
return false;
|
|
2150
|
+
}
|
|
2151
|
+
if (admission) {
|
|
2152
|
+
assertSemanticCurrent();
|
|
2153
|
+
if (!admission.reportCompleted())
|
|
2154
|
+
return false;
|
|
2155
|
+
admission.assertRecipientCurrent();
|
|
2156
|
+
}
|
|
2157
|
+
const assertEffectCurrent = admission?.assertRecipientCurrent ??
|
|
2158
|
+
(() => deps.assertExecutionCurrent?.(message));
|
|
2159
|
+
const options = admission && {
|
|
2160
|
+
assertAuthority: admission.assertRecipientCurrent,
|
|
2161
|
+
};
|
|
2162
|
+
const isContextActive = () => deps.isContextActive?.(ctx) !== false;
|
|
2163
|
+
scheduleTelegramCommandEffect(ctx, commandName, "menu-render", deps, async () => {
|
|
2164
|
+
let forumBootstrapMessage;
|
|
2165
|
+
if (commandName === "start" && deps.handleForumBootstrap) {
|
|
2166
|
+
forumBootstrapMessage = await deps.handleForumBootstrap(message, ctx, options);
|
|
2167
|
+
}
|
|
2168
|
+
if (!isContextActive())
|
|
2169
|
+
return;
|
|
2170
|
+
assertEffectCurrent();
|
|
2171
|
+
if (forumBootstrapMessage) {
|
|
2172
|
+
await deps.sendTextReply(message, forumBootstrapMessage, options);
|
|
2173
|
+
assertEffectCurrent();
|
|
2174
|
+
}
|
|
2175
|
+
if (!isContextActive())
|
|
2176
|
+
return;
|
|
2177
|
+
assertEffectCurrent();
|
|
2178
|
+
await deps.showStatus(message, ctx, options);
|
|
2179
|
+
}, assertEffectCurrent);
|
|
2180
|
+
scheduleTelegramCommandEffect(ctx, commandName, "bot-command-sync", deps, () => deps.registerBotCommands(options), assertEffectCurrent);
|
|
2181
|
+
return true;
|
|
2182
|
+
}
|
|
2183
|
+
async function handleTelegramNameCommand(message, ctx, requestedName, deps, admission) {
|
|
2184
|
+
const threadName = requestedName.trim();
|
|
2185
|
+
const reply = async (text) => {
|
|
2186
|
+
if (admission) {
|
|
2187
|
+
admission.assertSemanticCurrent();
|
|
2188
|
+
if (!admission.reportCompleted())
|
|
2189
|
+
throw new Error("Selected name source completion refused.");
|
|
2190
|
+
admission.assertRecipientCurrent();
|
|
2191
|
+
await deps.sendTextReply(message, text, {
|
|
2192
|
+
parseMode: "HTML",
|
|
2193
|
+
assertAuthority: admission.assertRecipientCurrent,
|
|
2194
|
+
});
|
|
2195
|
+
admission.assertRecipientCurrent();
|
|
2196
|
+
}
|
|
2197
|
+
else {
|
|
2198
|
+
deps.assertExecutionCurrent?.(message);
|
|
2199
|
+
await deps.sendTextReply(message, text, { parseMode: "HTML" });
|
|
2200
|
+
deps.assertExecutionCurrent?.(message);
|
|
2201
|
+
}
|
|
2202
|
+
};
|
|
2203
|
+
if (!threadName) {
|
|
2204
|
+
if (deps.openThreadNameDialog)
|
|
2205
|
+
await deps.openThreadNameDialog(message, ctx);
|
|
2206
|
+
else
|
|
2207
|
+
await reply(formatTelegramInformationHeading("🏷️", "Usage: /name Navigator"));
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
if (/^[A-Z]$/.test(threadName) && deps.resetCurrentThreadName) {
|
|
2211
|
+
admission?.assertSemanticCurrent();
|
|
2212
|
+
const target = getTelegramCommandMessageTarget(message);
|
|
2213
|
+
const result = admission
|
|
2214
|
+
? await deps.resetCurrentThreadName(target, {
|
|
2215
|
+
assertAuthority: admission.assertSemanticCurrent,
|
|
2216
|
+
})
|
|
2217
|
+
: await deps.resetCurrentThreadName(target);
|
|
2218
|
+
admission?.assertSemanticCurrent();
|
|
2219
|
+
await reply(result.ok && !result.message
|
|
2220
|
+
? formatTelegramAutomaticThreadDisplayNameRestoredHeading(result.threadName ?? threadName)
|
|
2221
|
+
: formatTelegramInformationHeading(result.ok ? "✅" : "⚠️", result.message ?? "Thread display name reset failed."));
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
admission?.assertSemanticCurrent();
|
|
2225
|
+
const validationError = deps.validateThreadName?.(threadName);
|
|
2226
|
+
admission?.assertSemanticCurrent();
|
|
2227
|
+
if (validationError) {
|
|
2228
|
+
await reply(formatTelegramInvalidInstanceName(validationError));
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
if (!deps.renameCurrentThread) {
|
|
2232
|
+
await reply(formatTelegramInformationHeading("🚫", "Thread display naming is unavailable."));
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
if (admission)
|
|
2236
|
+
admission.assertSemanticCurrent();
|
|
2237
|
+
else
|
|
2238
|
+
deps.assertExecutionCurrent?.(message);
|
|
2239
|
+
const target = getTelegramCommandMessageTarget(message);
|
|
2240
|
+
const result = admission
|
|
2241
|
+
? await deps.renameCurrentThread(target, threadName, {
|
|
2242
|
+
assertAuthority: admission.assertSemanticCurrent,
|
|
2243
|
+
})
|
|
2244
|
+
: await deps.renameCurrentThread(target, threadName);
|
|
2245
|
+
if (admission)
|
|
2246
|
+
admission.assertSemanticCurrent();
|
|
2247
|
+
else
|
|
2248
|
+
deps.assertExecutionCurrent?.(message);
|
|
2249
|
+
await reply(result.ok && !result.message
|
|
2250
|
+
? formatTelegramThreadDisplayNameSavedHeading(result.threadName ?? threadName)
|
|
2251
|
+
: formatTelegramInformationHeading(result.ok ? "✅" : "⚠️", result.message ?? "Thread display name update failed."));
|
|
2252
|
+
}
|
|
2253
|
+
async function handleTelegramCommandRuntime(commandName, message, ctx, deps, commandArgs = "") {
|
|
2254
|
+
deps.assertExecutionCurrent?.(message);
|
|
2255
|
+
const assertExecutionCurrentFor = (nextMessage) => () => deps.assertExecutionCurrent?.(nextMessage);
|
|
2256
|
+
const sendReplyFor = (nextMessage) => async (text, options) => {
|
|
2257
|
+
deps.assertExecutionCurrent?.(nextMessage);
|
|
2258
|
+
await deps.sendTextReply(nextMessage, text, options);
|
|
2259
|
+
deps.assertExecutionCurrent?.(nextMessage);
|
|
2260
|
+
};
|
|
2261
|
+
const updateStatusFor = (commandCtx) => () => deps.updateStatus(commandCtx);
|
|
2262
|
+
return executeTelegramCommandAction(buildTelegramCommandAction(commandName), message, ctx, {
|
|
2263
|
+
handleStop: async (nextMessage, commandCtx) => {
|
|
2264
|
+
await handleTelegramStopCommand({
|
|
2265
|
+
hasAbortHandler: deps.hasAbortHandler,
|
|
2266
|
+
clearPendingModelSwitch: deps.clearPendingModelSwitch,
|
|
2267
|
+
cancelNextTransitionAnnouncements: deps.cancelNextTransitionAnnouncements,
|
|
2268
|
+
clearQueuedTelegramItems: () => deps.clearQueuedTelegramItems(commandCtx),
|
|
2269
|
+
setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
|
|
2270
|
+
abortCurrentTurn: deps.abortCurrentTurn,
|
|
2271
|
+
updateStatus: updateStatusFor(commandCtx),
|
|
2272
|
+
sendTextReply: sendReplyFor(nextMessage),
|
|
2273
|
+
});
|
|
2274
|
+
},
|
|
2275
|
+
handleName: (nextMessage, commandCtx, requestedName) => handleTelegramNameCommand(nextMessage, commandCtx, requestedName, deps),
|
|
2276
|
+
handleAbort: async (nextMessage, commandCtx) => {
|
|
2277
|
+
await handleTelegramAbortCommand({
|
|
2278
|
+
hasAbortHandler: deps.hasAbortHandler,
|
|
2279
|
+
hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
|
|
2280
|
+
clearPendingModelSwitch: deps.clearPendingModelSwitch,
|
|
2281
|
+
cancelNextTransitionAnnouncements: deps.cancelNextTransitionAnnouncements,
|
|
2282
|
+
abortCurrentTurn: deps.abortCurrentTurn,
|
|
2283
|
+
setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
|
|
2284
|
+
updateStatus: updateStatusFor(commandCtx),
|
|
2285
|
+
sendTextReply: sendReplyFor(nextMessage),
|
|
2286
|
+
});
|
|
2287
|
+
},
|
|
2288
|
+
handleNext: async (nextMessage, commandCtx) => {
|
|
2289
|
+
await handleTelegramNextCommand({
|
|
2290
|
+
hasAbortHandler: deps.hasAbortHandler,
|
|
2291
|
+
isIdle: () => deps.isIdle(commandCtx),
|
|
2292
|
+
hasQueuedItems: deps.hasQueuedTelegramItems,
|
|
2293
|
+
clearPendingModelSwitch: deps.clearPendingModelSwitch,
|
|
2294
|
+
abortCurrentTurn: deps.abortCurrentTurn,
|
|
2295
|
+
dispatchNextQueuedTurn: () => deps.dispatchNextQueuedTelegramTurn(commandCtx),
|
|
2296
|
+
requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
|
|
2297
|
+
markActiveTurnNextAbortAnnouncement: deps.markActiveTurnNextAbortAnnouncement,
|
|
2298
|
+
clearFoldForDispatch: () => deps.setFoldQueuedPromptsIntoHistory(false),
|
|
2299
|
+
updateStatus: updateStatusFor(commandCtx),
|
|
2300
|
+
sendTextReply: sendReplyFor(nextMessage),
|
|
2301
|
+
getActiveTurnReply: deps.getActiveTurnReply,
|
|
2302
|
+
});
|
|
2303
|
+
},
|
|
2304
|
+
handleContinue: async (nextMessage, commandCtx) => {
|
|
2305
|
+
await handleTelegramContinueCommand(nextMessage, commandCtx, {
|
|
2306
|
+
enqueueContinueTurn: deps.enqueueContinueTurn,
|
|
2307
|
+
});
|
|
2308
|
+
},
|
|
2309
|
+
handleQueue: async (nextMessage, commandCtx) => {
|
|
2310
|
+
scheduleTelegramCommandEffect(commandCtx, "queue", "menu-render", deps, () => deps.openQueueMenu(nextMessage, commandCtx), assertExecutionCurrentFor(nextMessage));
|
|
2311
|
+
},
|
|
2312
|
+
handleNew: async (nextMessage, commandCtx) => {
|
|
2313
|
+
if (deps.sendInteractiveMessage) {
|
|
2314
|
+
await openTelegramNewConfirmation(getTelegramCommandMessageTarget(nextMessage), { sendInteractiveMessage: deps.sendInteractiveMessage });
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
await handleTelegramNewCommand({
|
|
2318
|
+
isIdle: () => deps.isIdle(commandCtx),
|
|
2319
|
+
hasPendingMessages: () => deps.hasPendingMessages(commandCtx),
|
|
2320
|
+
hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
|
|
2321
|
+
hasDispatchPending: deps.hasDispatchPending,
|
|
2322
|
+
hasQueuedTelegramItems: deps.hasQueuedTelegramItems,
|
|
2323
|
+
isCompactionInProgress: deps.isCompactionInProgress,
|
|
2324
|
+
requestNewSession: deps.requestNewSession
|
|
2325
|
+
? () => deps.requestNewSession(nextMessage)
|
|
2326
|
+
: undefined,
|
|
2327
|
+
sendTextReply: sendReplyFor(nextMessage),
|
|
2328
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
2329
|
+
});
|
|
2330
|
+
},
|
|
2331
|
+
handleCompact: async (nextMessage, commandCtx) => {
|
|
2332
|
+
if (deps.sendInteractiveMessage) {
|
|
2333
|
+
await openTelegramCompactConfirmation(getTelegramCommandMessageTarget(nextMessage), { sendInteractiveMessage: deps.sendInteractiveMessage });
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
await handleTelegramCompactCommand({
|
|
2337
|
+
isIdle: () => deps.isIdle(commandCtx),
|
|
2338
|
+
hasPendingMessages: () => deps.hasPendingMessages(commandCtx),
|
|
2339
|
+
hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
|
|
2340
|
+
hasDispatchPending: deps.hasDispatchPending,
|
|
2341
|
+
hasQueuedTelegramItems: deps.hasQueuedTelegramItems,
|
|
2342
|
+
isCompactionInProgress: deps.isCompactionInProgress,
|
|
2343
|
+
setCompactionInProgress: deps.setCompactionInProgress,
|
|
2344
|
+
updateStatus: updateStatusFor(commandCtx),
|
|
2345
|
+
dispatchNextQueuedTelegramTurn: () => deps.dispatchNextQueuedTelegramTurn(commandCtx),
|
|
2346
|
+
requestDeferredDispatchNextQueuedTelegramTurn: deps.requestDeferredDispatchNextQueuedTelegramTurn
|
|
2347
|
+
? (dispatch) => deps.requestDeferredDispatchNextQueuedTelegramTurn?.(() => dispatch())
|
|
2348
|
+
: undefined,
|
|
2349
|
+
compact: (callbacks) => deps.compact(commandCtx, callbacks),
|
|
2350
|
+
startTypingLoop: deps.startTypingLoop
|
|
2351
|
+
? () => deps.startTypingLoop?.(commandCtx, nextMessage.chat.id, {
|
|
2352
|
+
target: getTelegramCommandMessageTarget(nextMessage),
|
|
2353
|
+
})
|
|
2354
|
+
: undefined,
|
|
2355
|
+
stopTypingLoop: deps.stopTypingLoop,
|
|
2356
|
+
sendTextReply: sendReplyFor(nextMessage),
|
|
2357
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
2358
|
+
});
|
|
2359
|
+
},
|
|
2360
|
+
handleStatus: async (nextMessage, commandCtx) => {
|
|
2361
|
+
scheduleTelegramCommandEffect(commandCtx, "status", "menu-render", deps, () => deps.showStatus(nextMessage, commandCtx), assertExecutionCurrentFor(nextMessage));
|
|
2362
|
+
},
|
|
2363
|
+
handleModel: async (nextMessage, commandCtx) => {
|
|
2364
|
+
scheduleTelegramCommandEffect(commandCtx, "model", "menu-render", deps, () => handleTelegramModelCommand({
|
|
2365
|
+
ctx: commandCtx,
|
|
2366
|
+
openModelMenu: (controlCtx) => deps.openModelMenu(nextMessage, controlCtx),
|
|
2367
|
+
}), assertExecutionCurrentFor(nextMessage));
|
|
2368
|
+
},
|
|
2369
|
+
handleThinking: async (nextMessage, commandCtx) => {
|
|
2370
|
+
scheduleTelegramCommandEffect(commandCtx, "thinking", "menu-render", deps, () => deps.openThinkingMenu(nextMessage, commandCtx), assertExecutionCurrentFor(nextMessage));
|
|
2371
|
+
},
|
|
2372
|
+
handleSettings: deps.openSettingsMenu
|
|
2373
|
+
? async (nextMessage, commandCtx) => {
|
|
2374
|
+
scheduleTelegramCommandEffect(commandCtx, "settings", "menu-render", deps, () => deps.openSettingsMenu(nextMessage, commandCtx), assertExecutionCurrentFor(nextMessage));
|
|
2375
|
+
}
|
|
2376
|
+
: undefined,
|
|
2377
|
+
handleHelp: async (nextMessage, nextCommandName, commandCtx) => {
|
|
2378
|
+
await handleTelegramHelpCommand(nextCommandName, nextMessage, commandCtx, deps);
|
|
2379
|
+
},
|
|
2380
|
+
}, commandArgs);
|
|
2381
|
+
}
|
|
2382
|
+
export const TELEGRAM_INTERNAL_COMMAND_NAME = "telegram-internal";
|
|
2383
|
+
export const TELEGRAM_INTERNAL_COMMAND_DESCRIPTION = "Internal Telegram command cannot be run manually";
|
|
2384
|
+
export const TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE = "This internal Telegram command cannot be run manually.";
|
|
2385
|
+
function delayTelegramSessionAction(delayMs) {
|
|
2386
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
2387
|
+
}
|
|
2388
|
+
export async function settleTelegramSessionReplacement(deps) {
|
|
2389
|
+
const now = deps.now ?? Date.now;
|
|
2390
|
+
const sleep = deps.sleep ??
|
|
2391
|
+
((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));
|
|
2392
|
+
while (deps.isCurrent?.() !== false) {
|
|
2393
|
+
const intent = await deps.getIntent();
|
|
2394
|
+
if (!intent || intent.sourceSessionId === deps.sessionId)
|
|
2395
|
+
return "none";
|
|
2396
|
+
if (intent.profileName !== deps.profileName || intent.cwd !== deps.cwd)
|
|
2397
|
+
return "stale";
|
|
2398
|
+
if (now() >= intent.expiresAtMs)
|
|
2399
|
+
return "expired";
|
|
2400
|
+
if (!deps.hasSuccessorContinuity(intent)) {
|
|
2401
|
+
await sleep(100);
|
|
2402
|
+
continue;
|
|
2403
|
+
}
|
|
2404
|
+
if (!(await deps.clearIntent(intent)))
|
|
2405
|
+
return "failed";
|
|
2406
|
+
do {
|
|
2407
|
+
const delivered = await deps.editSuccess(intent);
|
|
2408
|
+
if (delivered.ok)
|
|
2409
|
+
return "settled";
|
|
2410
|
+
if (!delivered.retryable)
|
|
2411
|
+
return "failed";
|
|
2412
|
+
await sleep(100);
|
|
2413
|
+
} while (deps.isCurrent?.() !== false && now() < intent.expiresAtMs);
|
|
2414
|
+
return "failed";
|
|
2415
|
+
}
|
|
2416
|
+
return "stale";
|
|
2417
|
+
}
|
|
2418
|
+
function createTelegramSessionReplacementSettlementRuntime(deps) {
|
|
2419
|
+
let generation = 0;
|
|
2420
|
+
return {
|
|
2421
|
+
onSessionStart(ctx) {
|
|
2422
|
+
const currentGeneration = ++generation;
|
|
2423
|
+
const resolved = deps.resolve(ctx);
|
|
2424
|
+
if (!resolved)
|
|
2425
|
+
return;
|
|
2426
|
+
void settleTelegramSessionReplacement({
|
|
2427
|
+
...resolved,
|
|
2428
|
+
isCurrent: () => currentGeneration === generation && resolved.isCurrent?.() !== false,
|
|
2429
|
+
}).then(deps.onResult, deps.onError);
|
|
2430
|
+
},
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
export function createTelegramSessionActionAssembly(deps) {
|
|
2434
|
+
const now = deps.now ?? Date.now;
|
|
2435
|
+
const report = (error) => deps.recordRuntimeEvent?.("new-session", error);
|
|
2436
|
+
const sendTerminalResult = async (target, result) => {
|
|
2437
|
+
const text = result === "success"
|
|
2438
|
+
? "<b>🆕 New session started.</b>"
|
|
2439
|
+
: result === "cancelled"
|
|
2440
|
+
? "<b>🚫 New session cancelled.</b>"
|
|
2441
|
+
: "<b>⚠️ New session failed.</b>";
|
|
2442
|
+
const deadline = now() + 10_000;
|
|
2443
|
+
do {
|
|
2444
|
+
const delivery = await deps.sendResult(target, text);
|
|
2445
|
+
if (delivery.ok)
|
|
2446
|
+
return;
|
|
2447
|
+
if (!delivery.retryable)
|
|
2448
|
+
break;
|
|
2449
|
+
await delayTelegramSessionAction(100);
|
|
2450
|
+
} while (now() < deadline);
|
|
2451
|
+
report(new Error("Telegram new-session result delivery failed."));
|
|
2452
|
+
};
|
|
2453
|
+
const action = createTelegramSessionActionRuntime({
|
|
2454
|
+
registerCommand: deps.registerCommand,
|
|
2455
|
+
sendUserMessage: deps.sendUserMessage,
|
|
2456
|
+
notifyResult(target, result) {
|
|
2457
|
+
return sendTerminalResult(target, result);
|
|
2458
|
+
},
|
|
2459
|
+
async prepareReplacement(ctx, updateId, target) {
|
|
2460
|
+
const follower = !deps.ownsPersistence() &&
|
|
2461
|
+
typeof target.threadId === "number" &&
|
|
2462
|
+
deps.follower?.isRegisteredFor(target)
|
|
2463
|
+
? deps.follower
|
|
2464
|
+
: undefined;
|
|
2465
|
+
// Follower memory is not authority; reread the leader-published snapshot.
|
|
2466
|
+
if (follower && deps.store.refresh)
|
|
2467
|
+
await deps.store.refresh();
|
|
2468
|
+
else
|
|
2469
|
+
await deps.store.load();
|
|
2470
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
2471
|
+
const binding = typeof target.threadId === "number"
|
|
2472
|
+
? deps.store.getWorkspaceBindingByTarget(target)
|
|
2473
|
+
: undefined;
|
|
2474
|
+
if (typeof target.threadId === "number" &&
|
|
2475
|
+
(!binding || binding.cwd !== ctx.cwd || binding.sessionId !== sessionId)) {
|
|
2476
|
+
throw new Error("Telegram session replacement binding is unavailable.");
|
|
2477
|
+
}
|
|
2478
|
+
const createdAtMs = now();
|
|
2479
|
+
const intent = {
|
|
2480
|
+
continuity: binding ? "workspace-thread" : "classic-chat",
|
|
2481
|
+
cwd: binding?.cwd ?? ctx.cwd,
|
|
2482
|
+
profileName: deps.getProfileName() ?? "default",
|
|
2483
|
+
sourceSessionId: sessionId,
|
|
2484
|
+
sourceUpdateId: updateId,
|
|
2485
|
+
target: binding ? { ...binding.target } : { chatId: target.chatId },
|
|
2486
|
+
messageId: target.messageId,
|
|
2487
|
+
...(binding?.slot ? { slot: binding.slot } : {}),
|
|
2488
|
+
...((binding?.manualThreadName ?? binding?.threadName)
|
|
2489
|
+
? { threadName: binding.manualThreadName ?? binding.threadName }
|
|
2490
|
+
: {}),
|
|
2491
|
+
createdAtMs,
|
|
2492
|
+
expiresAtMs: createdAtMs + deps.handoffTtlMs,
|
|
2493
|
+
...(follower ? { sourceInstanceId: follower.instanceId } : {}),
|
|
2494
|
+
};
|
|
2495
|
+
if (!(await (follower
|
|
2496
|
+
? follower.requestSessionReplacement("publish", intent)
|
|
2497
|
+
: deps.store.commitSessionReplacementIntent(intent, deps.ownsPersistence)))) {
|
|
2498
|
+
throw new Error("Telegram session replacement intent was not persisted.");
|
|
2499
|
+
}
|
|
2500
|
+
},
|
|
2501
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
2502
|
+
});
|
|
2503
|
+
const settlement = createTelegramSessionReplacementSettlementRuntime({
|
|
2504
|
+
resolve(ctx) {
|
|
2505
|
+
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
2506
|
+
if (!sessionId)
|
|
2507
|
+
return undefined;
|
|
2508
|
+
return {
|
|
2509
|
+
async getIntent() {
|
|
2510
|
+
await deps.store.refresh?.();
|
|
2511
|
+
return deps.store.getSessionReplacementIntent();
|
|
2512
|
+
},
|
|
2513
|
+
hasSuccessorContinuity(intent) {
|
|
2514
|
+
if (intent.continuity === "classic-chat")
|
|
2515
|
+
return true;
|
|
2516
|
+
if (deps.store.getWorkspaceBindingByTarget(intent.target, sessionId)
|
|
2517
|
+
?.cwd !== intent.cwd)
|
|
2518
|
+
return false;
|
|
2519
|
+
// A follower successor claims only after its own re-registration is live.
|
|
2520
|
+
return (intent.sourceInstanceId === undefined ||
|
|
2521
|
+
deps.ownsPersistence() ||
|
|
2522
|
+
deps.follower?.isRegisteredFor(intent.target) === true);
|
|
2523
|
+
},
|
|
2524
|
+
editSuccess(intent) {
|
|
2525
|
+
return deps.sendResult(intent.target, "<b>🆕 New session started.</b>");
|
|
2526
|
+
},
|
|
2527
|
+
async clearIntent(intent) {
|
|
2528
|
+
if (intent.sourceInstanceId === undefined ||
|
|
2529
|
+
deps.ownsPersistence()) {
|
|
2530
|
+
return deps.store.removeSessionReplacementIntent(intent, deps.ownsPersistence);
|
|
2531
|
+
}
|
|
2532
|
+
return ((await deps.follower?.requestSessionReplacement("settle", intent)) ?? false);
|
|
2533
|
+
},
|
|
2534
|
+
profileName: deps.getProfileName() ?? "default",
|
|
2535
|
+
cwd: ctx.cwd,
|
|
2536
|
+
sessionId,
|
|
2537
|
+
};
|
|
2538
|
+
},
|
|
2539
|
+
onResult(result) {
|
|
2540
|
+
if (result === "expired" || result === "failed") {
|
|
2541
|
+
report(new Error(`Telegram session replacement successor settlement ${result}.`));
|
|
2542
|
+
}
|
|
2543
|
+
},
|
|
2544
|
+
onError: report,
|
|
2545
|
+
});
|
|
2546
|
+
return { action, settlement };
|
|
2547
|
+
}
|
|
2548
|
+
export function createTelegramSessionActionRuntime(deps) {
|
|
2549
|
+
let pendingUpdateId;
|
|
2550
|
+
let pendingTarget;
|
|
2551
|
+
let pendingAction;
|
|
2552
|
+
let registered = false;
|
|
2553
|
+
const reportFailure = (error) => {
|
|
2554
|
+
try {
|
|
2555
|
+
deps.recordRuntimeEvent?.("new-session", error);
|
|
2556
|
+
}
|
|
2557
|
+
catch {
|
|
2558
|
+
// Diagnostics cannot make a completed durable update retryable.
|
|
2559
|
+
}
|
|
2560
|
+
};
|
|
2561
|
+
return {
|
|
2562
|
+
register() {
|
|
2563
|
+
if (registered)
|
|
2564
|
+
return;
|
|
2565
|
+
registered = true;
|
|
2566
|
+
deps.registerCommand(TELEGRAM_INTERNAL_COMMAND_NAME, {
|
|
2567
|
+
description: TELEGRAM_INTERNAL_COMMAND_DESCRIPTION,
|
|
2568
|
+
handler: async (args, ctx) => {
|
|
2569
|
+
const action = pendingAction;
|
|
2570
|
+
if (!action || args.trim() !== action.token) {
|
|
2571
|
+
ctx.ui.notify(TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE, "warning");
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
pendingAction = undefined;
|
|
2575
|
+
switch (action.kind) {
|
|
2576
|
+
case "replace-session":
|
|
2577
|
+
try {
|
|
2578
|
+
await deps.prepareReplacement?.(ctx, action.updateId, action.target);
|
|
2579
|
+
const result = await ctx.newSession();
|
|
2580
|
+
if (result.cancelled)
|
|
2581
|
+
await deps.notifyResult(action.target, "cancelled");
|
|
2582
|
+
}
|
|
2583
|
+
catch (error) {
|
|
2584
|
+
reportFailure(error);
|
|
2585
|
+
await deps.notifyResult(action.target, "failure");
|
|
2586
|
+
}
|
|
2587
|
+
return;
|
|
2588
|
+
}
|
|
2589
|
+
},
|
|
2590
|
+
});
|
|
2591
|
+
},
|
|
2592
|
+
scheduleAfterUpdate(updateId, target) {
|
|
2593
|
+
if (pendingUpdateId !== undefined || pendingAction !== undefined)
|
|
2594
|
+
return false;
|
|
2595
|
+
pendingUpdateId = updateId;
|
|
2596
|
+
pendingTarget = { ...target };
|
|
2597
|
+
return true;
|
|
2598
|
+
},
|
|
2599
|
+
onUpdateCompleted(updateId) {
|
|
2600
|
+
if (pendingUpdateId !== updateId)
|
|
2601
|
+
return;
|
|
2602
|
+
pendingUpdateId = undefined;
|
|
2603
|
+
const target = pendingTarget;
|
|
2604
|
+
pendingTarget = undefined;
|
|
2605
|
+
if (!target)
|
|
2606
|
+
return;
|
|
2607
|
+
const token = randomUUID();
|
|
2608
|
+
pendingAction = { kind: "replace-session", token, updateId, target };
|
|
2609
|
+
void Promise.resolve()
|
|
2610
|
+
.then(() => deps.sendUserMessage(`/${TELEGRAM_INTERNAL_COMMAND_NAME} ${token}`, {
|
|
2611
|
+
expandPromptTemplates: true,
|
|
2612
|
+
}))
|
|
2613
|
+
.catch((error) => {
|
|
2614
|
+
pendingAction = undefined;
|
|
2615
|
+
reportFailure(error);
|
|
2616
|
+
});
|
|
2617
|
+
},
|
|
2618
|
+
hasPending() {
|
|
2619
|
+
return pendingUpdateId !== undefined || pendingAction !== undefined;
|
|
2620
|
+
},
|
|
2621
|
+
};
|
|
2622
|
+
}
|