@llblab/pi-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +35 -0
- package/BACKLOG.md +3 -0
- package/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +43 -0
- package/node_modules/@llblab/pi-actors/AGENTS.md +150 -0
- package/node_modules/@llblab/pi-actors/BACKLOG.md +6 -0
- package/node_modules/@llblab/pi-actors/CHANGELOG.md +865 -0
- package/node_modules/@llblab/pi-actors/README.md +231 -0
- package/node_modules/@llblab/pi-actors/banner.jpg +0 -0
- package/node_modules/@llblab/pi-actors/dist/fixtures/protocol/artifact-manifest.json +9 -0
- package/node_modules/@llblab/pi-actors/dist/fixtures/protocol/control-endpoint.json +6 -0
- package/node_modules/@llblab/pi-actors/dist/fixtures/protocol/control-record.json +9 -0
- package/node_modules/@llblab/pi-actors/dist/fixtures/protocol/recipe-summary.json +8 -0
- package/node_modules/@llblab/pi-actors/dist/fixtures/protocol/run-state.json +10 -0
- package/node_modules/@llblab/pi-actors/dist/fixtures/protocol/trace-event.json +9 -0
- package/node_modules/@llblab/pi-actors/dist/index.d.ts +7 -0
- package/node_modules/@llblab/pi-actors/dist/index.js +17 -0
- package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +146 -0
- package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +800 -0
- package/node_modules/@llblab/pi-actors/dist/lib/automatic-review-runtime.d.ts +20 -0
- package/node_modules/@llblab/pi-actors/dist/lib/automatic-review-runtime.js +103 -0
- package/node_modules/@llblab/pi-actors/dist/lib/command-templates.d.ts +85 -0
- package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +804 -0
- package/node_modules/@llblab/pi-actors/dist/lib/config.d.ts +34 -0
- package/node_modules/@llblab/pi-actors/dist/lib/config.js +216 -0
- package/node_modules/@llblab/pi-actors/dist/lib/control-projection.d.ts +20 -0
- package/node_modules/@llblab/pi-actors/dist/lib/control-projection.js +66 -0
- package/node_modules/@llblab/pi-actors/dist/lib/control.d.ts +15 -0
- package/node_modules/@llblab/pi-actors/dist/lib/control.js +97 -0
- package/node_modules/@llblab/pi-actors/dist/lib/draft-consolidation-transaction.d.ts +65 -0
- package/node_modules/@llblab/pi-actors/dist/lib/draft-consolidation-transaction.js +610 -0
- package/node_modules/@llblab/pi-actors/dist/lib/draft-consolidation.d.ts +35 -0
- package/node_modules/@llblab/pi-actors/dist/lib/draft-consolidation.js +126 -0
- package/node_modules/@llblab/pi-actors/dist/lib/draft-review.d.ts +56 -0
- package/node_modules/@llblab/pi-actors/dist/lib/draft-review.js +254 -0
- package/node_modules/@llblab/pi-actors/dist/lib/draft-sleep.d.ts +65 -0
- package/node_modules/@llblab/pi-actors/dist/lib/draft-sleep.js +467 -0
- package/node_modules/@llblab/pi-actors/dist/lib/execution-output.d.ts +25 -0
- package/node_modules/@llblab/pi-actors/dist/lib/execution-output.js +88 -0
- package/node_modules/@llblab/pi-actors/dist/lib/execution-sessions.d.ts +17 -0
- package/node_modules/@llblab/pi-actors/dist/lib/execution-sessions.js +85 -0
- package/node_modules/@llblab/pi-actors/dist/lib/execution.d.ts +99 -0
- package/node_modules/@llblab/pi-actors/dist/lib/execution.js +725 -0
- package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.d.ts +21 -0
- package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +141 -0
- package/node_modules/@llblab/pi-actors/dist/lib/file-state.d.ts +18 -0
- package/node_modules/@llblab/pi-actors/dist/lib/file-state.js +310 -0
- package/node_modules/@llblab/pi-actors/dist/lib/identity.d.ts +9 -0
- package/node_modules/@llblab/pi-actors/dist/lib/identity.js +27 -0
- package/node_modules/@llblab/pi-actors/dist/lib/inspector-actions.d.ts +16 -0
- package/node_modules/@llblab/pi-actors/dist/lib/inspector-actions.js +57 -0
- package/node_modules/@llblab/pi-actors/dist/lib/inspector-command.d.ts +7 -0
- package/node_modules/@llblab/pi-actors/dist/lib/inspector-command.js +37 -0
- package/node_modules/@llblab/pi-actors/dist/lib/inspector-overlay.d.ts +100 -0
- package/node_modules/@llblab/pi-actors/dist/lib/inspector-overlay.js +824 -0
- package/node_modules/@llblab/pi-actors/dist/lib/inspector.d.ts +20 -0
- package/node_modules/@llblab/pi-actors/dist/lib/inspector.js +136 -0
- package/node_modules/@llblab/pi-actors/dist/lib/limits.d.ts +28 -0
- package/node_modules/@llblab/pi-actors/dist/lib/limits.js +28 -0
- package/node_modules/@llblab/pi-actors/dist/lib/model-context.d.ts +56 -0
- package/node_modules/@llblab/pi-actors/dist/lib/model-context.js +220 -0
- package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +186 -0
- package/node_modules/@llblab/pi-actors/dist/lib/observability.js +951 -0
- package/node_modules/@llblab/pi-actors/dist/lib/paths.d.ts +27 -0
- package/node_modules/@llblab/pi-actors/dist/lib/paths.js +72 -0
- package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +19 -0
- package/node_modules/@llblab/pi-actors/dist/lib/pi.js +21 -0
- package/node_modules/@llblab/pi-actors/dist/lib/preflight-diagnostics.d.ts +27 -0
- package/node_modules/@llblab/pi-actors/dist/lib/preflight-diagnostics.js +86 -0
- package/node_modules/@llblab/pi-actors/dist/lib/prompts.d.ts +24 -0
- package/node_modules/@llblab/pi-actors/dist/lib/prompts.js +49 -0
- package/node_modules/@llblab/pi-actors/dist/lib/recipe-control.d.ts +7 -0
- package/node_modules/@llblab/pi-actors/dist/lib/recipe-control.js +43 -0
- package/node_modules/@llblab/pi-actors/dist/lib/recipes-context.d.ts +36 -0
- package/node_modules/@llblab/pi-actors/dist/lib/recipes-context.js +197 -0
- package/node_modules/@llblab/pi-actors/dist/lib/recipes-discovery.d.ts +77 -0
- package/node_modules/@llblab/pi-actors/dist/lib/recipes-discovery.js +783 -0
- package/node_modules/@llblab/pi-actors/dist/lib/recipes-references.d.ts +113 -0
- package/node_modules/@llblab/pi-actors/dist/lib/recipes-references.js +1108 -0
- package/node_modules/@llblab/pi-actors/dist/lib/recipes-usage.d.ts +21 -0
- package/node_modules/@llblab/pi-actors/dist/lib/recipes-usage.js +494 -0
- package/node_modules/@llblab/pi-actors/dist/lib/registry.d.ts +67 -0
- package/node_modules/@llblab/pi-actors/dist/lib/registry.js +504 -0
- package/node_modules/@llblab/pi-actors/dist/lib/review-control.d.ts +14 -0
- package/node_modules/@llblab/pi-actors/dist/lib/review-control.js +110 -0
- package/node_modules/@llblab/pi-actors/dist/lib/review-diagnostics.d.ts +11 -0
- package/node_modules/@llblab/pi-actors/dist/lib/review-diagnostics.js +148 -0
- package/node_modules/@llblab/pi-actors/dist/lib/review-projection.d.ts +14 -0
- package/node_modules/@llblab/pi-actors/dist/lib/review-projection.js +166 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-evidence-policy.d.ts +95 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-evidence-policy.js +177 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.d.ts +18 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +125 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-artifacts.d.ts +21 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-artifacts.js +68 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-control-delivery.d.ts +28 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-control-delivery.js +150 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-control.d.ts +21 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-control.js +89 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-controls.d.ts +39 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-controls.js +283 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-identity.d.ts +5 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-identity.js +11 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-index.d.ts +27 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-index.js +142 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-ownership.d.ts +7 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-ownership.js +96 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-parent-teardown.d.ts +51 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-parent-teardown.js +172 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-process.d.ts +22 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-process.js +125 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-retention.d.ts +18 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-retention.js +108 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-start.d.ts +12 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-start.js +68 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-status.d.ts +10 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-status.js +68 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-trace.d.ts +48 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runs-trace.js +491 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runtime-identity.d.ts +7 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runtime-identity.js +35 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runtime-triage.d.ts +29 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runtime-triage.js +60 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runtime.d.ts +69 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +339 -0
- package/node_modules/@llblab/pi-actors/dist/lib/schema.d.ts +60 -0
- package/node_modules/@llblab/pi-actors/dist/lib/schema.js +428 -0
- package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.d.ts +45 -0
- package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.js +204 -0
- package/node_modules/@llblab/pi-actors/dist/lib/state-readers.d.ts +21 -0
- package/node_modules/@llblab/pi-actors/dist/lib/state-readers.js +74 -0
- package/node_modules/@llblab/pi-actors/dist/lib/temp.d.ts +10 -0
- package/node_modules/@llblab/pi-actors/dist/lib/temp.js +90 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tool-review-lineage-transaction.d.ts +27 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tool-review-lineage-transaction.js +597 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tool-review-lineage.d.ts +24 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tool-review-lineage.js +98 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tool-review-scheduler.d.ts +80 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tool-review-scheduler.js +494 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tool-review-transaction.d.ts +50 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tool-review-transaction.js +362 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tool-review.d.ts +56 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tool-review.js +197 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tools-access.d.ts +19 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tools-access.js +44 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tools-inspect.d.ts +15 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tools-inspect.js +532 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tools-local.d.ts +17 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tools-local.js +191 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tools-message.d.ts +9 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tools-message.js +139 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tools-register.d.ts +18 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tools-register.js +41 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tools-response.d.ts +20 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tools-response.js +243 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tools-spawn.d.ts +15 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tools-spawn.js +133 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tools.d.ts +28 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tools.js +49 -0
- package/node_modules/@llblab/pi-actors/dist/lib/trace-projection.d.ts +22 -0
- package/node_modules/@llblab/pi-actors/dist/lib/trace-projection.js +231 -0
- package/node_modules/@llblab/pi-actors/dist/pi-actors/index.js +1 -0
- package/node_modules/@llblab/pi-actors/dist/scripts/async-runner.mjs +555 -0
- package/node_modules/@llblab/pi-actors/dist/scripts/build-dist.mjs +60 -0
- package/node_modules/@llblab/pi-actors/dist/scripts/conformance.mjs +63 -0
- package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +126 -0
- package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/command-validate.json +13 -0
- package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/jsonl-tail.json +10 -0
- package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/recipe-validate.json +11 -0
- package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/resource-locker-snapshot.json +11 -0
- package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/resource-locker.json +25 -0
- package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/run-ops-snapshot.json +15 -0
- package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/run-state-files.json +13 -0
- package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/run-summary.json +9 -0
- package/node_modules/@llblab/pi-actors/dist/skills/actors/references/diagnostics.md +44 -0
- package/node_modules/@llblab/pi-actors/dist/skills/actors/references/persistent-tools.md +76 -0
- package/node_modules/@llblab/pi-actors/dist/skills/actors/references/recipes.md +51 -0
- package/node_modules/@llblab/pi-actors/dist/skills/actors/references/runs.md +41 -0
- package/node_modules/@llblab/pi-actors/dist/skills/actors/scripts/resource-locker.mjs +425 -0
- package/node_modules/@llblab/pi-actors/dist/skills/actors/scripts/run-utils.mjs +164 -0
- package/node_modules/@llblab/pi-actors/dist/skills/actors/scripts/validate-recipe.mjs +370 -0
- package/node_modules/@llblab/pi-actors/dist/skills/artifacts/SKILL.md +33 -0
- package/node_modules/@llblab/pi-actors/dist/skills/artifacts/recipes/bundle.json +77 -0
- package/node_modules/@llblab/pi-actors/dist/skills/artifacts/recipes/file-write.json +14 -0
- package/node_modules/@llblab/pi-actors/dist/skills/artifacts/recipes/manifest.json +14 -0
- package/node_modules/@llblab/pi-actors/dist/skills/artifacts/recipes/report.json +39 -0
- package/node_modules/@llblab/pi-actors/dist/skills/artifacts/recipes/write.json +53 -0
- package/node_modules/@llblab/pi-actors/dist/skills/artifacts/scripts/artifact-utils.mjs +63 -0
- package/node_modules/@llblab/pi-actors/dist/skills/music-player/SKILL.md +57 -0
- package/node_modules/@llblab/pi-actors/dist/skills/music-player/genapps/music-player.mjs +360 -0
- package/node_modules/@llblab/pi-actors/dist/skills/music-player/recipes/playback.json +29 -0
- package/node_modules/@llblab/pi-actors/dist/skills/music-player/scripts/playback-client.mjs +143 -0
- package/node_modules/@llblab/pi-actors/dist/skills/music-player/scripts/playback.mjs +1446 -0
- package/node_modules/@llblab/pi-actors/dist/skills/project-work/SKILL.md +37 -0
- package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/changelog-head.json +11 -0
- package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/changelog-section.json +11 -0
- package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/docs-maintenance.json +66 -0
- package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/git-log.json +11 -0
- package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/git-status.json +9 -0
- package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/markdown-index.json +14 -0
- package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/package-summary.json +9 -0
- package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/release-readiness.json +115 -0
- package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/release-summary.json +74 -0
- package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/repo-health.json +75 -0
- package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/run-ops.json +56 -0
- package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/skill-summary.json +11 -0
- package/node_modules/@llblab/pi-actors/dist/skills/project-work/scripts/project-utils.mjs +89 -0
- package/node_modules/@llblab/pi-actors/dist/skills/recipe-memory/SKILL.md +36 -0
- package/node_modules/@llblab/pi-actors/dist/skills/recipe-memory/recipes/draft-review.json +14 -0
- package/node_modules/@llblab/pi-actors/dist/skills/recipe-memory/recipes/tool-review.json +14 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/SKILL.md +84 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/architect.json +82 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/checkpoint-continuation.json +51 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/development-tasking.json +67 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/lens-review.json +63 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/quorum-review.json +65 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/research-synthesis.json +81 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/review-readiness.json +59 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-artifact.json +21 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-checkpoint.json +21 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-conflict-report.json +19 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-contradiction-map.json +20 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-critic.json +22 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-evidence-map.json +20 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-followup.json +21 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-judge.json +22 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-merge.json +24 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-normalize.json +20 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-plan.json +20 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-preflight.json +18 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-prompt.json +16 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-prompts.json +39 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-quorum.json +31 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-review-coordinator.json +170 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-review.json +26 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-task-card.json +22 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-tools.json +15 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-verify.json +23 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/references/development-swarm.md +189 -0
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/references/review-swarms.md +115 -0
- package/node_modules/@llblab/pi-actors/docs/README.md +20 -0
- package/node_modules/@llblab/pi-actors/docs/actor-inspector.md +50 -0
- package/node_modules/@llblab/pi-actors/docs/async-runs.md +141 -0
- package/node_modules/@llblab/pi-actors/docs/command-templates.md +321 -0
- package/node_modules/@llblab/pi-actors/docs/recipe-library.md +89 -0
- package/node_modules/@llblab/pi-actors/docs/releasing.md +28 -0
- package/node_modules/@llblab/pi-actors/docs/template-recipes.md +175 -0
- package/node_modules/@llblab/pi-actors/docs/tool-registry.md +90 -0
- package/node_modules/@llblab/pi-actors/fixtures/protocol/artifact-manifest.json +9 -0
- package/node_modules/@llblab/pi-actors/fixtures/protocol/control-endpoint.json +6 -0
- package/node_modules/@llblab/pi-actors/fixtures/protocol/control-record.json +9 -0
- package/node_modules/@llblab/pi-actors/fixtures/protocol/recipe-summary.json +8 -0
- package/node_modules/@llblab/pi-actors/fixtures/protocol/run-state.json +10 -0
- package/node_modules/@llblab/pi-actors/fixtures/protocol/trace-event.json +9 -0
- package/node_modules/@llblab/pi-actors/index.ts +30 -0
- package/node_modules/@llblab/pi-actors/lib/async-runs.ts +1245 -0
- package/node_modules/@llblab/pi-actors/lib/automatic-review-runtime.ts +148 -0
- package/node_modules/@llblab/pi-actors/lib/command-templates.ts +1116 -0
- package/node_modules/@llblab/pi-actors/lib/config.ts +261 -0
- package/node_modules/@llblab/pi-actors/lib/control-projection.ts +105 -0
- package/node_modules/@llblab/pi-actors/lib/control.ts +117 -0
- package/node_modules/@llblab/pi-actors/lib/draft-consolidation-transaction.ts +821 -0
- package/node_modules/@llblab/pi-actors/lib/draft-consolidation.ts +181 -0
- package/node_modules/@llblab/pi-actors/lib/draft-review.ts +325 -0
- package/node_modules/@llblab/pi-actors/lib/draft-sleep.ts +575 -0
- package/node_modules/@llblab/pi-actors/lib/execution-output.ts +123 -0
- package/node_modules/@llblab/pi-actors/lib/execution-sessions.ts +111 -0
- package/node_modules/@llblab/pi-actors/lib/execution.ts +1153 -0
- package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +181 -0
- package/node_modules/@llblab/pi-actors/lib/file-state.ts +306 -0
- package/node_modules/@llblab/pi-actors/lib/identity.ts +29 -0
- package/node_modules/@llblab/pi-actors/lib/inspector-actions.ts +79 -0
- package/node_modules/@llblab/pi-actors/lib/inspector-command.ts +54 -0
- package/node_modules/@llblab/pi-actors/lib/inspector-overlay.ts +901 -0
- package/node_modules/@llblab/pi-actors/lib/inspector.ts +204 -0
- package/node_modules/@llblab/pi-actors/lib/limits.ts +29 -0
- package/node_modules/@llblab/pi-actors/lib/model-context.ts +359 -0
- package/node_modules/@llblab/pi-actors/lib/observability.ts +1337 -0
- package/node_modules/@llblab/pi-actors/lib/paths.ts +111 -0
- package/node_modules/@llblab/pi-actors/lib/pi.ts +47 -0
- package/node_modules/@llblab/pi-actors/lib/preflight-diagnostics.ts +132 -0
- package/node_modules/@llblab/pi-actors/lib/prompts.ts +69 -0
- package/node_modules/@llblab/pi-actors/lib/recipe-control.ts +52 -0
- package/node_modules/@llblab/pi-actors/lib/recipes-context.ts +268 -0
- package/node_modules/@llblab/pi-actors/lib/recipes-discovery.ts +1060 -0
- package/node_modules/@llblab/pi-actors/lib/recipes-references.ts +1598 -0
- package/node_modules/@llblab/pi-actors/lib/recipes-usage.ts +638 -0
- package/node_modules/@llblab/pi-actors/lib/registry.ts +806 -0
- package/node_modules/@llblab/pi-actors/lib/review-control.ts +136 -0
- package/node_modules/@llblab/pi-actors/lib/review-diagnostics.ts +164 -0
- package/node_modules/@llblab/pi-actors/lib/review-projection.ts +196 -0
- package/node_modules/@llblab/pi-actors/lib/run-evidence-policy.ts +242 -0
- package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +155 -0
- package/node_modules/@llblab/pi-actors/lib/runs-artifacts.ts +96 -0
- package/node_modules/@llblab/pi-actors/lib/runs-control-delivery.ts +209 -0
- package/node_modules/@llblab/pi-actors/lib/runs-control.ts +148 -0
- package/node_modules/@llblab/pi-actors/lib/runs-controls.ts +282 -0
- package/node_modules/@llblab/pi-actors/lib/runs-identity.ts +14 -0
- package/node_modules/@llblab/pi-actors/lib/runs-index.ts +192 -0
- package/node_modules/@llblab/pi-actors/lib/runs-ownership.ts +136 -0
- package/node_modules/@llblab/pi-actors/lib/runs-parent-teardown.ts +257 -0
- package/node_modules/@llblab/pi-actors/lib/runs-process.ts +174 -0
- package/node_modules/@llblab/pi-actors/lib/runs-retention.ts +140 -0
- package/node_modules/@llblab/pi-actors/lib/runs-start.ts +113 -0
- package/node_modules/@llblab/pi-actors/lib/runs-status.ts +101 -0
- package/node_modules/@llblab/pi-actors/lib/runs-trace.ts +611 -0
- package/node_modules/@llblab/pi-actors/lib/runtime-identity.ts +39 -0
- package/node_modules/@llblab/pi-actors/lib/runtime-triage.ts +106 -0
- package/node_modules/@llblab/pi-actors/lib/runtime.ts +430 -0
- package/node_modules/@llblab/pi-actors/lib/schema.ts +525 -0
- package/node_modules/@llblab/pi-actors/lib/session-evidence.ts +302 -0
- package/node_modules/@llblab/pi-actors/lib/state-readers.ts +93 -0
- package/node_modules/@llblab/pi-actors/lib/temp.ts +99 -0
- package/node_modules/@llblab/pi-actors/lib/tool-review-lineage-transaction.ts +881 -0
- package/node_modules/@llblab/pi-actors/lib/tool-review-lineage.ts +145 -0
- package/node_modules/@llblab/pi-actors/lib/tool-review-scheduler.ts +635 -0
- package/node_modules/@llblab/pi-actors/lib/tool-review-transaction.ts +563 -0
- package/node_modules/@llblab/pi-actors/lib/tool-review.ts +270 -0
- package/node_modules/@llblab/pi-actors/lib/tools-access.ts +68 -0
- package/node_modules/@llblab/pi-actors/lib/tools-inspect.ts +673 -0
- package/node_modules/@llblab/pi-actors/lib/tools-local.ts +303 -0
- package/node_modules/@llblab/pi-actors/lib/tools-message.ts +176 -0
- package/node_modules/@llblab/pi-actors/lib/tools-register.ts +63 -0
- package/node_modules/@llblab/pi-actors/lib/tools-response.ts +285 -0
- package/node_modules/@llblab/pi-actors/lib/tools-spawn.ts +212 -0
- package/node_modules/@llblab/pi-actors/lib/tools.ts +88 -0
- package/node_modules/@llblab/pi-actors/lib/trace-projection.ts +306 -0
- package/node_modules/@llblab/pi-actors/package.json +76 -0
- package/node_modules/@llblab/pi-actors/scripts/async-runner.mjs +555 -0
- package/node_modules/@llblab/pi-actors/scripts/build-dist.mjs +60 -0
- package/node_modules/@llblab/pi-actors/scripts/conformance.mjs +63 -0
- package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +126 -0
- package/node_modules/@llblab/pi-actors/skills/actors/recipes/command-validate.json +13 -0
- package/node_modules/@llblab/pi-actors/skills/actors/recipes/jsonl-tail.json +10 -0
- package/node_modules/@llblab/pi-actors/skills/actors/recipes/recipe-validate.json +11 -0
- package/node_modules/@llblab/pi-actors/skills/actors/recipes/resource-locker-snapshot.json +11 -0
- package/node_modules/@llblab/pi-actors/skills/actors/recipes/resource-locker.json +25 -0
- package/node_modules/@llblab/pi-actors/skills/actors/recipes/run-ops-snapshot.json +15 -0
- package/node_modules/@llblab/pi-actors/skills/actors/recipes/run-state-files.json +13 -0
- package/node_modules/@llblab/pi-actors/skills/actors/recipes/run-summary.json +9 -0
- package/node_modules/@llblab/pi-actors/skills/actors/references/diagnostics.md +44 -0
- package/node_modules/@llblab/pi-actors/skills/actors/references/persistent-tools.md +76 -0
- package/node_modules/@llblab/pi-actors/skills/actors/references/recipes.md +51 -0
- package/node_modules/@llblab/pi-actors/skills/actors/references/runs.md +41 -0
- package/node_modules/@llblab/pi-actors/skills/actors/scripts/resource-locker.mjs +425 -0
- package/node_modules/@llblab/pi-actors/skills/actors/scripts/run-utils.mjs +164 -0
- package/node_modules/@llblab/pi-actors/skills/actors/scripts/validate-recipe.mjs +370 -0
- package/node_modules/@llblab/pi-actors/skills/artifacts/SKILL.md +33 -0
- package/node_modules/@llblab/pi-actors/skills/artifacts/recipes/bundle.json +77 -0
- package/node_modules/@llblab/pi-actors/skills/artifacts/recipes/file-write.json +14 -0
- package/node_modules/@llblab/pi-actors/skills/artifacts/recipes/manifest.json +14 -0
- package/node_modules/@llblab/pi-actors/skills/artifacts/recipes/report.json +39 -0
- package/node_modules/@llblab/pi-actors/skills/artifacts/recipes/write.json +53 -0
- package/node_modules/@llblab/pi-actors/skills/artifacts/scripts/artifact-utils.mjs +63 -0
- package/node_modules/@llblab/pi-actors/skills/music-player/SKILL.md +57 -0
- package/node_modules/@llblab/pi-actors/skills/music-player/genapps/music-player.mjs +360 -0
- package/node_modules/@llblab/pi-actors/skills/music-player/recipes/playback.json +29 -0
- package/node_modules/@llblab/pi-actors/skills/music-player/scripts/playback-client.mjs +143 -0
- package/node_modules/@llblab/pi-actors/skills/music-player/scripts/playback.mjs +1446 -0
- package/node_modules/@llblab/pi-actors/skills/project-work/SKILL.md +37 -0
- package/node_modules/@llblab/pi-actors/skills/project-work/recipes/changelog-head.json +11 -0
- package/node_modules/@llblab/pi-actors/skills/project-work/recipes/changelog-section.json +11 -0
- package/node_modules/@llblab/pi-actors/skills/project-work/recipes/docs-maintenance.json +66 -0
- package/node_modules/@llblab/pi-actors/skills/project-work/recipes/git-log.json +11 -0
- package/node_modules/@llblab/pi-actors/skills/project-work/recipes/git-status.json +9 -0
- package/node_modules/@llblab/pi-actors/skills/project-work/recipes/markdown-index.json +14 -0
- package/node_modules/@llblab/pi-actors/skills/project-work/recipes/package-summary.json +9 -0
- package/node_modules/@llblab/pi-actors/skills/project-work/recipes/release-readiness.json +115 -0
- package/node_modules/@llblab/pi-actors/skills/project-work/recipes/release-summary.json +74 -0
- package/node_modules/@llblab/pi-actors/skills/project-work/recipes/repo-health.json +75 -0
- package/node_modules/@llblab/pi-actors/skills/project-work/recipes/run-ops.json +56 -0
- package/node_modules/@llblab/pi-actors/skills/project-work/recipes/skill-summary.json +11 -0
- package/node_modules/@llblab/pi-actors/skills/project-work/scripts/project-utils.mjs +89 -0
- package/node_modules/@llblab/pi-actors/skills/recipe-memory/SKILL.md +36 -0
- package/node_modules/@llblab/pi-actors/skills/recipe-memory/recipes/draft-review.json +14 -0
- package/node_modules/@llblab/pi-actors/skills/recipe-memory/recipes/tool-review.json +14 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/SKILL.md +84 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/architect.json +82 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/checkpoint-continuation.json +51 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/development-tasking.json +67 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/lens-review.json +63 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/quorum-review.json +65 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/research-synthesis.json +81 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/review-readiness.json +59 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-artifact.json +21 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-checkpoint.json +21 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-conflict-report.json +19 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-contradiction-map.json +20 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-critic.json +22 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-evidence-map.json +20 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-followup.json +21 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-judge.json +22 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-merge.json +24 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-normalize.json +20 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-plan.json +20 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-preflight.json +18 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-prompt.json +16 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-prompts.json +39 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-quorum.json +31 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-review-coordinator.json +170 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-review.json +26 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-task-card.json +22 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-tools.json +15 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-verify.json +23 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/references/development-swarm.md +189 -0
- package/node_modules/@llblab/pi-actors/skills/swarm/references/review-swarms.md +115 -0
- package/node_modules/@llblab/pi-codex-usage/AGENTS.md +17 -0
- package/node_modules/@llblab/pi-codex-usage/BACKLOG.md +8 -0
- package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +80 -0
- package/node_modules/@llblab/pi-codex-usage/LICENSE +22 -0
- package/node_modules/@llblab/pi-codex-usage/README.md +113 -0
- package/node_modules/@llblab/pi-codex-usage/banner.jpg +0 -0
- package/node_modules/@llblab/pi-codex-usage/index.ts +1525 -0
- package/node_modules/@llblab/pi-codex-usage/package.json +59 -0
- package/node_modules/@llblab/pi-grow-loop/AGENTS.md +56 -0
- package/node_modules/@llblab/pi-grow-loop/BACKLOG.md +3 -0
- package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +126 -0
- package/node_modules/@llblab/pi-grow-loop/README.md +216 -0
- package/node_modules/@llblab/pi-grow-loop/banner.jpg +0 -0
- package/node_modules/@llblab/pi-grow-loop/index.ts +219 -0
- package/node_modules/@llblab/pi-grow-loop/package.json +64 -0
- package/node_modules/@llblab/pi-grow-loop/skills/grow-loop/SKILL.md +131 -0
- package/node_modules/@llblab/pi-grow-loop/skills/while-true/SKILL.md +233 -0
- package/node_modules/@llblab/pi-telegram/AGENTS.md +157 -0
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +15 -0
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +886 -0
- package/node_modules/@llblab/pi-telegram/README.md +330 -0
- package/node_modules/@llblab/pi-telegram/api/activity.ts +15 -0
- package/node_modules/@llblab/pi-telegram/api/commands.ts +11 -0
- package/node_modules/@llblab/pi-telegram/api/delivery.ts +21 -0
- package/node_modules/@llblab/pi-telegram/api/inbound.ts +14 -0
- package/node_modules/@llblab/pi-telegram/api/keyboard.ts +11 -0
- package/node_modules/@llblab/pi-telegram/api/outbound.ts +11 -0
- package/node_modules/@llblab/pi-telegram/api/sections.ts +17 -0
- package/node_modules/@llblab/pi-telegram/api/status.ts +12 -0
- package/node_modules/@llblab/pi-telegram/api/updates.ts +16 -0
- package/node_modules/@llblab/pi-telegram/api/voice.ts +24 -0
- package/node_modules/@llblab/pi-telegram/docs/README.md +25 -0
- package/node_modules/@llblab/pi-telegram/docs/activity.md +304 -0
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +418 -0
- package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +48 -0
- package/node_modules/@llblab/pi-telegram/docs/command-templates.md +365 -0
- package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +176 -0
- package/node_modules/@llblab/pi-telegram/docs/delivery.md +225 -0
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +310 -0
- package/node_modules/@llblab/pi-telegram/docs/inbound.md +136 -0
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +467 -0
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +193 -0
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +599 -0
- package/node_modules/@llblab/pi-telegram/docs/sections.md +452 -0
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +256 -0
- package/node_modules/@llblab/pi-telegram/docs/updates.md +171 -0
- package/node_modules/@llblab/pi-telegram/docs/voice.md +217 -0
- package/node_modules/@llblab/pi-telegram/index.ts +1336 -0
- package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +714 -0
- package/node_modules/@llblab/pi-telegram/lib/activity.ts +782 -0
- package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +121 -0
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +1129 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +379 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +1941 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +2119 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +244 -0
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +2645 -0
- package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +954 -0
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +1854 -0
- package/node_modules/@llblab/pi-telegram/lib/config.ts +1143 -0
- package/node_modules/@llblab/pi-telegram/lib/delivery.ts +891 -0
- package/node_modules/@llblab/pi-telegram/lib/generative-app-worker.mjs +104 -0
- package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +958 -0
- package/node_modules/@llblab/pi-telegram/lib/inbound.ts +824 -0
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +3140 -0
- package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +59 -0
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +623 -0
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +1422 -0
- package/node_modules/@llblab/pi-telegram/lib/logs.ts +393 -0
- package/node_modules/@llblab/pi-telegram/lib/media.ts +921 -0
- package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +1182 -0
- package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +926 -0
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +876 -0
- package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +274 -0
- package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +185 -0
- package/node_modules/@llblab/pi-telegram/lib/menu.ts +867 -0
- package/node_modules/@llblab/pi-telegram/lib/model.ts +672 -0
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +981 -0
- package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +331 -0
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +515 -0
- package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +285 -0
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +1007 -0
- package/node_modules/@llblab/pi-telegram/lib/ownership.ts +298 -0
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +127 -0
- package/node_modules/@llblab/pi-telegram/lib/pi.ts +197 -0
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +1588 -0
- package/node_modules/@llblab/pi-telegram/lib/preview.ts +846 -0
- package/node_modules/@llblab/pi-telegram/lib/prompt-templates.ts +158 -0
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +218 -0
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +3075 -0
- package/node_modules/@llblab/pi-telegram/lib/recovery.ts +464 -0
- package/node_modules/@llblab/pi-telegram/lib/rendering.ts +1213 -0
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +892 -0
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +2889 -0
- package/node_modules/@llblab/pi-telegram/lib/runtime.ts +628 -0
- package/node_modules/@llblab/pi-telegram/lib/sections.ts +710 -0
- package/node_modules/@llblab/pi-telegram/lib/setup.ts +220 -0
- package/node_modules/@llblab/pi-telegram/lib/skills.ts +21 -0
- package/node_modules/@llblab/pi-telegram/lib/status.ts +1550 -0
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +883 -0
- package/node_modules/@llblab/pi-telegram/lib/target.ts +49 -0
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +1747 -0
- package/node_modules/@llblab/pi-telegram/lib/text-groups.ts +487 -0
- package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +1002 -0
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +3183 -0
- package/node_modules/@llblab/pi-telegram/lib/time-injection.ts +78 -0
- package/node_modules/@llblab/pi-telegram/lib/turns.ts +753 -0
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +4950 -0
- package/node_modules/@llblab/pi-telegram/lib/voice.ts +340 -0
- package/node_modules/@llblab/pi-telegram/package.json +83 -0
- package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
- package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +435 -0
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +241 -0
- package/node_modules/@llblab/pi-telegram/skills/generative-apps/SKILL.md +114 -0
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +149 -0
- package/package.json +67 -0
|
@@ -0,0 +1,3075 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram queue core contracts and pure planning helpers
|
|
3
|
+
* Zones: telegram queue, pi agent lifecycle, scheduling
|
|
4
|
+
* Owns queue item contracts, lane admission, pure queue mutations, and dispatch planning
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
import { isVoiceTurn } from "./voice.ts";
|
|
10
|
+
|
|
11
|
+
// --- Queue Items ---
|
|
12
|
+
|
|
13
|
+
export interface QueuedAttachment {
|
|
14
|
+
path: string;
|
|
15
|
+
fileName: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TelegramPromptTextContent {
|
|
19
|
+
type: "text";
|
|
20
|
+
text: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TelegramPromptImageContent {
|
|
24
|
+
type: "image";
|
|
25
|
+
data: string;
|
|
26
|
+
mimeType: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type TelegramPromptContent =
|
|
30
|
+
TelegramPromptTextContent | TelegramPromptImageContent;
|
|
31
|
+
|
|
32
|
+
export type TelegramQueueItemKind = "prompt" | "control";
|
|
33
|
+
export type TelegramQueueLane = "control" | "priority" | "default";
|
|
34
|
+
export type TelegramQueueReactionDisposition =
|
|
35
|
+
| { kind: "default" }
|
|
36
|
+
| { kind: "priority"; emoji: string }
|
|
37
|
+
| { kind: "suppressed"; emoji: string }
|
|
38
|
+
| {
|
|
39
|
+
kind: "priority-suppressed";
|
|
40
|
+
priorityEmoji: string;
|
|
41
|
+
suppressionEmoji: string;
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
kind: "reaction-transition";
|
|
45
|
+
priorityEmoji?: string | null;
|
|
46
|
+
suppressionEmoji?: string | null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export interface TelegramQueueAdmissionReceipt {
|
|
50
|
+
queueKind: TelegramQueueItemKind;
|
|
51
|
+
receiptId: string;
|
|
52
|
+
sourceUpdateIds: readonly number[];
|
|
53
|
+
journalBindingKey?: string;
|
|
54
|
+
}
|
|
55
|
+
export type TelegramQueueAdmissionMode =
|
|
56
|
+
"control-queue" | "priority-queue" | "default-queue";
|
|
57
|
+
|
|
58
|
+
export interface TelegramQueueLaneContract {
|
|
59
|
+
lane: TelegramQueueLane;
|
|
60
|
+
admissionMode: TelegramQueueAdmissionMode;
|
|
61
|
+
dispatchRank: number;
|
|
62
|
+
allowedKinds: readonly TelegramQueueItemKind[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const TELEGRAM_QUEUE_LANE_CONTRACTS: readonly TelegramQueueLaneContract[] =
|
|
66
|
+
[
|
|
67
|
+
// Control lane intentionally accepts both direct controls and resume prompts.
|
|
68
|
+
// Model-switch continuations need prompt semantics but must run before queued user work.
|
|
69
|
+
// Do not admit ordinary user prompts here without an explicit control-flow reason.
|
|
70
|
+
{
|
|
71
|
+
lane: "control",
|
|
72
|
+
admissionMode: "control-queue",
|
|
73
|
+
dispatchRank: 0,
|
|
74
|
+
allowedKinds: ["control", "prompt"],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
lane: "priority",
|
|
78
|
+
admissionMode: "priority-queue",
|
|
79
|
+
dispatchRank: 1,
|
|
80
|
+
allowedKinds: ["prompt"],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
lane: "default",
|
|
84
|
+
admissionMode: "default-queue",
|
|
85
|
+
dispatchRank: 2,
|
|
86
|
+
allowedKinds: ["prompt"],
|
|
87
|
+
},
|
|
88
|
+
] as const;
|
|
89
|
+
|
|
90
|
+
export interface TelegramQueueTarget {
|
|
91
|
+
chatId: number;
|
|
92
|
+
threadId?: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface TelegramTransportStamp {
|
|
96
|
+
profile: string;
|
|
97
|
+
generation: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface TelegramTransportStampRuntime {
|
|
101
|
+
getStamp(): TelegramTransportStamp;
|
|
102
|
+
isActive(stamp: TelegramTransportStamp | undefined): boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface TelegramQueueItemBase {
|
|
106
|
+
kind: TelegramQueueItemKind;
|
|
107
|
+
chatId: number;
|
|
108
|
+
target?: TelegramQueueTarget;
|
|
109
|
+
transportStamp?: TelegramTransportStamp;
|
|
110
|
+
replyToMessageId: number;
|
|
111
|
+
guestQueryId?: string;
|
|
112
|
+
queueOrder: number;
|
|
113
|
+
queueLane: TelegramQueueLane;
|
|
114
|
+
laneOrder: number;
|
|
115
|
+
statusSummary: string;
|
|
116
|
+
admissionReceipts?: TelegramQueueAdmissionReceipt[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface PendingTelegramTurn extends TelegramQueueItemBase {
|
|
120
|
+
kind: "prompt";
|
|
121
|
+
sourceMessageIds: number[];
|
|
122
|
+
queuedAttachments: QueuedAttachment[];
|
|
123
|
+
content: TelegramPromptContent[];
|
|
124
|
+
historyText: string;
|
|
125
|
+
priorityEmoji?: string;
|
|
126
|
+
reactionSuppressionEmoji?: string;
|
|
127
|
+
|
|
128
|
+
/** Turn should preferably be delivered as voice (mirror mode + user sent voice) */
|
|
129
|
+
voiceReplyPreferred?: boolean;
|
|
130
|
+
/** Turn must be delivered as voice (voice mode) */
|
|
131
|
+
voiceReplyRequired?: boolean;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface PendingTelegramControlItem<
|
|
135
|
+
TContext = unknown,
|
|
136
|
+
> extends TelegramQueueItemBase {
|
|
137
|
+
kind: "control";
|
|
138
|
+
controlType: "status" | "model";
|
|
139
|
+
execute: (ctx: TContext) => Promise<void>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export type TelegramQueueItem<TContext = unknown> =
|
|
143
|
+
PendingTelegramTurn | PendingTelegramControlItem<TContext>;
|
|
144
|
+
|
|
145
|
+
export const TELEGRAM_QUEUE_HANDOFF_PAYLOAD_MAX_BYTES = 8 * 1024 * 1024;
|
|
146
|
+
export const TELEGRAM_QUEUE_HANDOFF_MAX_RECEIPTS = 256;
|
|
147
|
+
|
|
148
|
+
export interface TelegramQueueHandoffBase {
|
|
149
|
+
chatId: number;
|
|
150
|
+
target?: TelegramQueueTarget;
|
|
151
|
+
transportStamp?: TelegramTransportStamp;
|
|
152
|
+
replyToMessageId: number;
|
|
153
|
+
guestQueryId?: string;
|
|
154
|
+
queueOrder: number;
|
|
155
|
+
queueLane: TelegramQueueLane;
|
|
156
|
+
laneOrder: number;
|
|
157
|
+
statusSummary: string;
|
|
158
|
+
admissionReceipts: TelegramQueueAdmissionReceipt[];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface TelegramPromptQueueHandoffPayload
|
|
162
|
+
extends TelegramQueueHandoffBase {
|
|
163
|
+
kind: "prompt";
|
|
164
|
+
sourceMessageIds: number[];
|
|
165
|
+
queuedAttachments: QueuedAttachment[];
|
|
166
|
+
content: TelegramPromptContent[];
|
|
167
|
+
historyText: string;
|
|
168
|
+
priorityEmoji?: string;
|
|
169
|
+
reactionSuppressionEmoji?: string;
|
|
170
|
+
voiceReplyPreferred?: boolean;
|
|
171
|
+
voiceReplyRequired?: boolean;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface TelegramControlQueueHandoffPayload
|
|
175
|
+
extends TelegramQueueHandoffBase {
|
|
176
|
+
kind: "control";
|
|
177
|
+
controlType: PendingTelegramControlItem<unknown>["controlType"];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export type TelegramQueueHandoffPayload =
|
|
181
|
+
| TelegramPromptQueueHandoffPayload
|
|
182
|
+
| TelegramControlQueueHandoffPayload;
|
|
183
|
+
|
|
184
|
+
export interface TelegramQueueHandoff {
|
|
185
|
+
handoffToken: string;
|
|
186
|
+
payload: TelegramQueueHandoffPayload;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface TelegramQueueHandoffStageReceipt {
|
|
190
|
+
status: "staged";
|
|
191
|
+
receiptId: string;
|
|
192
|
+
sourceUpdateIds: readonly number[];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface TelegramQueueHandoffAcceptedOwner {
|
|
196
|
+
instanceId: string;
|
|
197
|
+
processId: number;
|
|
198
|
+
processBirthId: string;
|
|
199
|
+
sessionGeneration: number;
|
|
200
|
+
acquisitionId: string;
|
|
201
|
+
acquiredAtMs: number;
|
|
202
|
+
handoffId?: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface TelegramQueueHandoffStageResult
|
|
206
|
+
extends TelegramQueueHandoffStageReceipt {
|
|
207
|
+
queueOwner: TelegramQueueHandoffAcceptedOwner;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface TelegramQueueHandoffStagingRuntime {
|
|
211
|
+
stage: (payload: TelegramQueueHandoffPayload) => TelegramQueueHandoffStageReceipt;
|
|
212
|
+
accept: (receipt: TelegramQueueAdmissionReceipt) => boolean;
|
|
213
|
+
cancel: (receipt: TelegramQueueAdmissionReceipt) => boolean;
|
|
214
|
+
hasStaged: (receipt: TelegramQueueAdmissionReceipt) => boolean;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface TelegramQueueStore<TContext = unknown> {
|
|
218
|
+
getQueuedItems: () => TelegramQueueItem<TContext>[];
|
|
219
|
+
setQueuedItems: (items: TelegramQueueItem<TContext>[]) => void;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export interface TelegramQueueStateStore<
|
|
223
|
+
TContext = unknown,
|
|
224
|
+
> extends TelegramQueueStore<TContext> {
|
|
225
|
+
hasQueuedItems: () => boolean;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface TelegramActiveTurnStore<
|
|
229
|
+
TTurn extends PendingTelegramTurn = PendingTelegramTurn,
|
|
230
|
+
> {
|
|
231
|
+
get: () => TTurn | undefined;
|
|
232
|
+
has: () => boolean;
|
|
233
|
+
set: (turn: TTurn) => void;
|
|
234
|
+
clear: () => void;
|
|
235
|
+
getChatId: () => number | undefined;
|
|
236
|
+
getTarget: () => TelegramQueueTarget | undefined;
|
|
237
|
+
getReplyToMessageId: () => number | undefined;
|
|
238
|
+
getGuestQueryId: () => string | undefined;
|
|
239
|
+
getSourceMessageIds: () => number[] | undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface TelegramDispatchGuardState {
|
|
243
|
+
compactionInProgress: boolean;
|
|
244
|
+
hasActiveTelegramTurn: boolean;
|
|
245
|
+
hasPendingTelegramDispatch: boolean;
|
|
246
|
+
isIdle: boolean;
|
|
247
|
+
hasPendingMessages: boolean;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function createTelegramQueueAdmissionReceipt(options: {
|
|
251
|
+
queueKind: TelegramQueueItemKind;
|
|
252
|
+
scope: string;
|
|
253
|
+
sourceUpdateIds: readonly number[];
|
|
254
|
+
}): TelegramQueueAdmissionReceipt | undefined {
|
|
255
|
+
if (options.sourceUpdateIds.length === 0) return undefined;
|
|
256
|
+
const scope = options.scope.trim();
|
|
257
|
+
if (!scope) {
|
|
258
|
+
throw new Error("Telegram queue admission receipt scope is required.");
|
|
259
|
+
}
|
|
260
|
+
if (options.queueKind !== "prompt" && options.queueKind !== "control") {
|
|
261
|
+
throw new Error("Telegram queue admission receipt kind is invalid.");
|
|
262
|
+
}
|
|
263
|
+
const sourceUpdateIds = [...new Set(options.sourceUpdateIds)].sort(
|
|
264
|
+
(left, right) => left - right,
|
|
265
|
+
);
|
|
266
|
+
if (
|
|
267
|
+
sourceUpdateIds.some(
|
|
268
|
+
(updateId) => !Number.isSafeInteger(updateId) || updateId < 0,
|
|
269
|
+
)
|
|
270
|
+
) {
|
|
271
|
+
throw new Error("Telegram queue admission update ids must be safe integers.");
|
|
272
|
+
}
|
|
273
|
+
const digest = createHash("sha256")
|
|
274
|
+
.update(
|
|
275
|
+
JSON.stringify({
|
|
276
|
+
scope,
|
|
277
|
+
queueKind: options.queueKind,
|
|
278
|
+
sourceUpdateIds,
|
|
279
|
+
}),
|
|
280
|
+
)
|
|
281
|
+
.digest("hex");
|
|
282
|
+
return {
|
|
283
|
+
queueKind: options.queueKind,
|
|
284
|
+
receiptId: `telegram-${options.queueKind}-v1-${digest}`,
|
|
285
|
+
sourceUpdateIds,
|
|
286
|
+
journalBindingKey: scope,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function isTelegramQueueItemDurablyAdmitted<TContext = unknown>(
|
|
291
|
+
item: TelegramQueueItem<TContext>,
|
|
292
|
+
isReceiptCommitted: (receipt: TelegramQueueAdmissionReceipt) => boolean,
|
|
293
|
+
): boolean {
|
|
294
|
+
assertTelegramQueueItemAdmissionValid(item);
|
|
295
|
+
return (item.admissionReceipts ?? []).every(isReceiptCommitted);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function getTelegramQueueLaneContract(
|
|
299
|
+
lane: TelegramQueueLane,
|
|
300
|
+
): TelegramQueueLaneContract {
|
|
301
|
+
const contract = TELEGRAM_QUEUE_LANE_CONTRACTS.find(
|
|
302
|
+
(entry) => entry.lane === lane,
|
|
303
|
+
);
|
|
304
|
+
if (!contract) throw new Error(`Unknown Telegram queue lane: ${lane}`);
|
|
305
|
+
return contract;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function getTelegramQueueItemAdmissionMode(
|
|
309
|
+
item: Pick<TelegramQueueItem, "queueLane">,
|
|
310
|
+
): TelegramQueueAdmissionMode {
|
|
311
|
+
return getTelegramQueueLaneContract(item.queueLane).admissionMode;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function isTelegramQueueItemAdmissionValid(
|
|
315
|
+
item: Pick<TelegramQueueItem, "kind" | "queueLane">,
|
|
316
|
+
): boolean {
|
|
317
|
+
return getTelegramQueueLaneContract(item.queueLane).allowedKinds.includes(
|
|
318
|
+
item.kind,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function assertTelegramQueueItemAdmissionValid(
|
|
323
|
+
item: Pick<
|
|
324
|
+
TelegramQueueItem,
|
|
325
|
+
"kind" | "queueLane" | "admissionReceipts"
|
|
326
|
+
>,
|
|
327
|
+
): void {
|
|
328
|
+
if (!isTelegramQueueItemAdmissionValid(item)) {
|
|
329
|
+
throw new Error(
|
|
330
|
+
`Invalid Telegram queue admission: ${item.kind} item cannot use ${item.queueLane} lane`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
const receiptIds = new Set<string>();
|
|
334
|
+
for (const receipt of item.admissionReceipts ?? []) {
|
|
335
|
+
const sourceUpdateIds = new Set(receipt.sourceUpdateIds);
|
|
336
|
+
if (
|
|
337
|
+
receipt.queueKind !== item.kind ||
|
|
338
|
+
!receipt.receiptId ||
|
|
339
|
+
receiptIds.has(receipt.receiptId) ||
|
|
340
|
+
receipt.sourceUpdateIds.length === 0 ||
|
|
341
|
+
(receipt.journalBindingKey !== undefined &&
|
|
342
|
+
receipt.journalBindingKey.trim().length === 0) ||
|
|
343
|
+
sourceUpdateIds.size !== receipt.sourceUpdateIds.length ||
|
|
344
|
+
receipt.sourceUpdateIds.some(
|
|
345
|
+
(updateId, index) =>
|
|
346
|
+
!Number.isSafeInteger(updateId) ||
|
|
347
|
+
updateId < 0 ||
|
|
348
|
+
(index > 0 && updateId <= receipt.sourceUpdateIds[index - 1]!),
|
|
349
|
+
)
|
|
350
|
+
) {
|
|
351
|
+
throw new Error(
|
|
352
|
+
`Invalid Telegram queue receipt for ${item.kind} item`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
receiptIds.add(receipt.receiptId);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function getTelegramQueueLaneRank(lane: TelegramQueueLane): number {
|
|
360
|
+
return getTelegramQueueLaneContract(lane).dispatchRank;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function isPendingTelegramTurn<TContext = unknown>(
|
|
364
|
+
item: TelegramQueueItem<TContext>,
|
|
365
|
+
): item is PendingTelegramTurn {
|
|
366
|
+
return item.kind === "prompt";
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function createTelegramQueueStore<TContext = unknown>(
|
|
370
|
+
initialItems: TelegramQueueItem<TContext>[] = [],
|
|
371
|
+
): TelegramQueueStateStore<TContext> {
|
|
372
|
+
let queuedItems = initialItems;
|
|
373
|
+
return {
|
|
374
|
+
getQueuedItems: () => queuedItems,
|
|
375
|
+
setQueuedItems: (items) => {
|
|
376
|
+
queuedItems = items;
|
|
377
|
+
},
|
|
378
|
+
hasQueuedItems: () => queuedItems.length > 0,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function createTelegramTransportStampRuntime(deps: {
|
|
383
|
+
getProfileName(): string | undefined;
|
|
384
|
+
getBotToken(): string | undefined;
|
|
385
|
+
}): TelegramTransportStampRuntime {
|
|
386
|
+
let profile: string | undefined;
|
|
387
|
+
let botToken: string | undefined;
|
|
388
|
+
let generation = 0;
|
|
389
|
+
const getStamp = function (): TelegramTransportStamp {
|
|
390
|
+
const nextProfile = deps.getProfileName() ?? "default";
|
|
391
|
+
const nextBotToken = deps.getBotToken();
|
|
392
|
+
if (nextProfile !== profile || nextBotToken !== botToken) {
|
|
393
|
+
profile = nextProfile;
|
|
394
|
+
botToken = nextBotToken;
|
|
395
|
+
generation += 1;
|
|
396
|
+
}
|
|
397
|
+
return { profile: nextProfile, generation: String(generation) };
|
|
398
|
+
};
|
|
399
|
+
return {
|
|
400
|
+
getStamp,
|
|
401
|
+
isActive(stamp) {
|
|
402
|
+
if (!stamp) return false;
|
|
403
|
+
const current = getStamp();
|
|
404
|
+
return (
|
|
405
|
+
stamp.profile === current.profile &&
|
|
406
|
+
stamp.generation === current.generation
|
|
407
|
+
);
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function createTelegramTransportStampedQueueStore<TContext>(
|
|
413
|
+
store: TelegramQueueStateStore<TContext>,
|
|
414
|
+
getTransportStamp: () => TelegramTransportStamp,
|
|
415
|
+
): TelegramQueueStateStore<TContext> {
|
|
416
|
+
return {
|
|
417
|
+
getQueuedItems: store.getQueuedItems,
|
|
418
|
+
hasQueuedItems: store.hasQueuedItems,
|
|
419
|
+
setQueuedItems(items) {
|
|
420
|
+
const stamp = getTransportStamp();
|
|
421
|
+
store.setQueuedItems(
|
|
422
|
+
items.map((item) =>
|
|
423
|
+
item.transportStamp ? item : { ...item, transportStamp: stamp },
|
|
424
|
+
),
|
|
425
|
+
);
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function createTelegramQueueItemCountGetter<TContext = unknown>(
|
|
431
|
+
store: Pick<TelegramQueueStore<TContext>, "getQueuedItems">,
|
|
432
|
+
): () => number {
|
|
433
|
+
return () => {
|
|
434
|
+
return store.getQueuedItems().length;
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function createTelegramActiveTurnStore<
|
|
439
|
+
TTurn extends PendingTelegramTurn = PendingTelegramTurn,
|
|
440
|
+
>(): TelegramActiveTurnStore<TTurn> {
|
|
441
|
+
let activeTurn: TTurn | undefined;
|
|
442
|
+
return {
|
|
443
|
+
get: () => activeTurn,
|
|
444
|
+
has: () => !!activeTurn,
|
|
445
|
+
set: (turn) => {
|
|
446
|
+
activeTurn = { ...turn };
|
|
447
|
+
},
|
|
448
|
+
clear: () => {
|
|
449
|
+
activeTurn = undefined;
|
|
450
|
+
},
|
|
451
|
+
getChatId: () => activeTurn?.chatId,
|
|
452
|
+
getTarget: () =>
|
|
453
|
+
activeTurn?.target ? { ...activeTurn.target } : undefined,
|
|
454
|
+
getReplyToMessageId: () => activeTurn?.replyToMessageId,
|
|
455
|
+
getGuestQueryId: () => activeTurn?.guestQueryId,
|
|
456
|
+
getSourceMessageIds: () => activeTurn?.sourceMessageIds,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// --- Queue Mutations ---
|
|
461
|
+
|
|
462
|
+
export function partitionTelegramQueueItemsForHistory<TContext = unknown>(
|
|
463
|
+
items: TelegramQueueItem<TContext>[],
|
|
464
|
+
): {
|
|
465
|
+
historyTurns: PendingTelegramTurn[];
|
|
466
|
+
remainingItems: TelegramQueueItem<TContext>[];
|
|
467
|
+
} {
|
|
468
|
+
const historyTurns: PendingTelegramTurn[] = [];
|
|
469
|
+
const remainingItems: TelegramQueueItem<TContext>[] = [];
|
|
470
|
+
for (const item of items) {
|
|
471
|
+
if (isPendingTelegramTurn(item)) {
|
|
472
|
+
historyTurns.push(item);
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
remainingItems.push(item);
|
|
476
|
+
}
|
|
477
|
+
return { historyTurns, remainingItems };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function planTelegramPromptEnqueue<TContext = unknown>(
|
|
481
|
+
items: TelegramQueueItem<TContext>[],
|
|
482
|
+
foldQueuedPromptsIntoHistory: boolean,
|
|
483
|
+
): {
|
|
484
|
+
historyTurns: PendingTelegramTurn[];
|
|
485
|
+
remainingItems: TelegramQueueItem<TContext>[];
|
|
486
|
+
} {
|
|
487
|
+
if (!foldQueuedPromptsIntoHistory) {
|
|
488
|
+
return { historyTurns: [], remainingItems: items };
|
|
489
|
+
}
|
|
490
|
+
return partitionTelegramQueueItemsForHistory(items);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export function areTelegramQueueAdmissionReceiptsEqual(
|
|
494
|
+
left: TelegramQueueAdmissionReceipt,
|
|
495
|
+
right: TelegramQueueAdmissionReceipt,
|
|
496
|
+
): boolean {
|
|
497
|
+
return (
|
|
498
|
+
left.queueKind === right.queueKind &&
|
|
499
|
+
left.receiptId === right.receiptId &&
|
|
500
|
+
left.journalBindingKey === right.journalBindingKey &&
|
|
501
|
+
left.sourceUpdateIds.length === right.sourceUpdateIds.length &&
|
|
502
|
+
left.sourceUpdateIds.every(
|
|
503
|
+
(updateId, index) => updateId === right.sourceUpdateIds[index],
|
|
504
|
+
)
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function getTelegramQueueAdmissionReceiptKey(
|
|
509
|
+
receipt: TelegramQueueAdmissionReceipt,
|
|
510
|
+
): string {
|
|
511
|
+
return JSON.stringify([
|
|
512
|
+
receipt.receiptId,
|
|
513
|
+
receipt.journalBindingKey ?? null,
|
|
514
|
+
]);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function isDuplicateTelegramQueueAdmission<TContext>(
|
|
518
|
+
items: TelegramQueueItem<TContext>[],
|
|
519
|
+
item: TelegramQueueItem<TContext>,
|
|
520
|
+
): boolean {
|
|
521
|
+
const incomingReceipts = item.admissionReceipts ?? [];
|
|
522
|
+
if (incomingReceipts.length === 0) return false;
|
|
523
|
+
const queuedReceipts = new Map<
|
|
524
|
+
string,
|
|
525
|
+
{ receipt: TelegramQueueAdmissionReceipt; count: number }
|
|
526
|
+
>();
|
|
527
|
+
for (const queuedItem of items) {
|
|
528
|
+
for (const receipt of queuedItem.admissionReceipts ?? []) {
|
|
529
|
+
const key = getTelegramQueueAdmissionReceiptKey(receipt);
|
|
530
|
+
const existing = queuedReceipts.get(key);
|
|
531
|
+
if (existing) existing.count += 1;
|
|
532
|
+
else queuedReceipts.set(key, { receipt, count: 1 });
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
let duplicateCount = 0;
|
|
536
|
+
for (const incoming of incomingReceipts) {
|
|
537
|
+
const match = queuedReceipts.get(
|
|
538
|
+
getTelegramQueueAdmissionReceiptKey(incoming),
|
|
539
|
+
);
|
|
540
|
+
if (!match) continue;
|
|
541
|
+
if (
|
|
542
|
+
match.count !== 1 ||
|
|
543
|
+
!areTelegramQueueAdmissionReceiptsEqual(match.receipt, incoming)
|
|
544
|
+
) {
|
|
545
|
+
throw new Error(
|
|
546
|
+
`Conflicting Telegram queue receipt: ${incoming.receiptId}`,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
duplicateCount += 1;
|
|
550
|
+
}
|
|
551
|
+
if (duplicateCount === 0) return false;
|
|
552
|
+
if (duplicateCount === incomingReceipts.length) return true;
|
|
553
|
+
throw new Error("Telegram queue item overlaps an existing receipt.");
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export function appendTelegramQueueItem<
|
|
557
|
+
TContext = unknown,
|
|
558
|
+
TItem extends TelegramQueueItem<TContext> = TelegramQueueItem<TContext>,
|
|
559
|
+
>(
|
|
560
|
+
items: TelegramQueueItem<TContext>[],
|
|
561
|
+
item: TItem,
|
|
562
|
+
): TelegramQueueItem<TContext>[] {
|
|
563
|
+
assertTelegramQueueItemAdmissionValid(item);
|
|
564
|
+
if (isDuplicateTelegramQueueAdmission(items, item)) return items;
|
|
565
|
+
return [...items, item];
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export function createTelegramQueueHandoff<TContext>(input: {
|
|
569
|
+
handoffToken: string;
|
|
570
|
+
item: TelegramQueueItem<TContext>;
|
|
571
|
+
}): TelegramQueueHandoff {
|
|
572
|
+
if (!input.handoffToken) {
|
|
573
|
+
throw new Error("Telegram queue handoff token is required.");
|
|
574
|
+
}
|
|
575
|
+
const handoff = {
|
|
576
|
+
handoffToken: input.handoffToken,
|
|
577
|
+
payload: createTelegramQueueHandoffPayload(input.item),
|
|
578
|
+
};
|
|
579
|
+
if (Buffer.byteLength(JSON.stringify(handoff)) > TELEGRAM_QUEUE_HANDOFF_PAYLOAD_MAX_BYTES) {
|
|
580
|
+
throw new Error("Telegram queue handoff payload exceeds its byte limit.");
|
|
581
|
+
}
|
|
582
|
+
return handoff;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export function createTelegramQueueHandoffPayload<TContext>(
|
|
586
|
+
item: TelegramQueueItem<TContext>,
|
|
587
|
+
): TelegramQueueHandoffPayload {
|
|
588
|
+
assertTelegramQueueItemAdmissionValid(item);
|
|
589
|
+
if (!item.admissionReceipts?.length) {
|
|
590
|
+
throw new Error("Telegram queue handoff requires durable admission receipts.");
|
|
591
|
+
}
|
|
592
|
+
if (item.admissionReceipts.length > TELEGRAM_QUEUE_HANDOFF_MAX_RECEIPTS) {
|
|
593
|
+
throw new Error("Telegram queue handoff has too many admission receipts.");
|
|
594
|
+
}
|
|
595
|
+
if (item.kind === "control") {
|
|
596
|
+
return structuredClone({
|
|
597
|
+
kind: item.kind,
|
|
598
|
+
controlType: item.controlType,
|
|
599
|
+
chatId: item.chatId,
|
|
600
|
+
...(item.target ? { target: item.target } : {}),
|
|
601
|
+
...(item.transportStamp ? { transportStamp: item.transportStamp } : {}),
|
|
602
|
+
replyToMessageId: item.replyToMessageId,
|
|
603
|
+
...(item.guestQueryId ? { guestQueryId: item.guestQueryId } : {}),
|
|
604
|
+
queueOrder: item.queueOrder,
|
|
605
|
+
queueLane: item.queueLane,
|
|
606
|
+
laneOrder: item.laneOrder,
|
|
607
|
+
statusSummary: item.statusSummary,
|
|
608
|
+
admissionReceipts: item.admissionReceipts,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
return structuredClone({
|
|
612
|
+
kind: item.kind,
|
|
613
|
+
chatId: item.chatId,
|
|
614
|
+
...(item.target ? { target: item.target } : {}),
|
|
615
|
+
...(item.transportStamp ? { transportStamp: item.transportStamp } : {}),
|
|
616
|
+
replyToMessageId: item.replyToMessageId,
|
|
617
|
+
...(item.guestQueryId ? { guestQueryId: item.guestQueryId } : {}),
|
|
618
|
+
queueOrder: item.queueOrder,
|
|
619
|
+
queueLane: item.queueLane,
|
|
620
|
+
laneOrder: item.laneOrder,
|
|
621
|
+
statusSummary: item.statusSummary,
|
|
622
|
+
admissionReceipts: item.admissionReceipts,
|
|
623
|
+
sourceMessageIds: item.sourceMessageIds,
|
|
624
|
+
queuedAttachments: item.queuedAttachments,
|
|
625
|
+
content: item.content,
|
|
626
|
+
historyText: item.historyText,
|
|
627
|
+
...(item.priorityEmoji ? { priorityEmoji: item.priorityEmoji } : {}),
|
|
628
|
+
...(item.reactionSuppressionEmoji
|
|
629
|
+
? { reactionSuppressionEmoji: item.reactionSuppressionEmoji }
|
|
630
|
+
: {}),
|
|
631
|
+
...(item.voiceReplyPreferred !== undefined
|
|
632
|
+
? { voiceReplyPreferred: item.voiceReplyPreferred }
|
|
633
|
+
: {}),
|
|
634
|
+
...(item.voiceReplyRequired !== undefined
|
|
635
|
+
? { voiceReplyRequired: item.voiceReplyRequired }
|
|
636
|
+
: {}),
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export function restoreTelegramQueueHandoffPayload<TContext>(
|
|
641
|
+
payload: TelegramQueueHandoffPayload,
|
|
642
|
+
createControlExecution: (
|
|
643
|
+
payload: TelegramControlQueueHandoffPayload,
|
|
644
|
+
) => PendingTelegramControlItem<TContext>["execute"],
|
|
645
|
+
): TelegramQueueItem<TContext> {
|
|
646
|
+
const item: TelegramQueueItem<TContext> =
|
|
647
|
+
payload.kind === "prompt"
|
|
648
|
+
? structuredClone(payload)
|
|
649
|
+
: {
|
|
650
|
+
...structuredClone(payload),
|
|
651
|
+
execute: createControlExecution(payload),
|
|
652
|
+
};
|
|
653
|
+
assertTelegramQueueItemAdmissionValid(item);
|
|
654
|
+
return item;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function getTelegramQueueHandoffReceipt(
|
|
658
|
+
payload: TelegramQueueHandoffPayload,
|
|
659
|
+
): TelegramQueueAdmissionReceipt {
|
|
660
|
+
const receipt = payload.admissionReceipts[0];
|
|
661
|
+
if (!receipt || payload.admissionReceipts.length !== 1) {
|
|
662
|
+
throw new Error(
|
|
663
|
+
"Telegram queue handoff requires exactly one complete receipt.",
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
return receipt;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function findTelegramQueueReceiptItem<TContext>(
|
|
670
|
+
items: readonly TelegramQueueItem<TContext>[],
|
|
671
|
+
receipt: TelegramQueueAdmissionReceipt,
|
|
672
|
+
): TelegramQueueItem<TContext> | undefined {
|
|
673
|
+
const receiptKey = getTelegramQueueAdmissionReceiptKey(receipt);
|
|
674
|
+
let match: TelegramQueueItem<TContext> | undefined;
|
|
675
|
+
let matchedReceipt: TelegramQueueAdmissionReceipt | undefined;
|
|
676
|
+
for (const item of items) {
|
|
677
|
+
for (const candidate of item.admissionReceipts ?? []) {
|
|
678
|
+
if (getTelegramQueueAdmissionReceiptKey(candidate) !== receiptKey) {
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (match) {
|
|
682
|
+
throw new Error(
|
|
683
|
+
`Conflicting Telegram queue receipt: ${receipt.receiptId}`,
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
match = item;
|
|
687
|
+
matchedReceipt = candidate;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (!match) return undefined;
|
|
691
|
+
if (
|
|
692
|
+
!matchedReceipt ||
|
|
693
|
+
!areTelegramQueueAdmissionReceiptsEqual(matchedReceipt, receipt)
|
|
694
|
+
) {
|
|
695
|
+
throw new Error(`Conflicting Telegram queue receipt: ${receipt.receiptId}`);
|
|
696
|
+
}
|
|
697
|
+
return match;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export function removeTelegramQueueItemByReceipt<TContext>(input: {
|
|
701
|
+
receipt: TelegramQueueAdmissionReceipt;
|
|
702
|
+
store: TelegramQueueStore<TContext>;
|
|
703
|
+
}): boolean {
|
|
704
|
+
const current = input.store.getQueuedItems();
|
|
705
|
+
const item = findTelegramQueueReceiptItem(current, input.receipt);
|
|
706
|
+
if (!item) return false;
|
|
707
|
+
input.store.setQueuedItems(current.filter((candidate) => candidate !== item));
|
|
708
|
+
return true;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
export function stageTelegramQueueHandoffPayload<TContext>(input: {
|
|
712
|
+
payload: TelegramQueueHandoffPayload;
|
|
713
|
+
store: TelegramQueueStore<TContext>;
|
|
714
|
+
createControlExecution: (
|
|
715
|
+
payload: TelegramControlQueueHandoffPayload,
|
|
716
|
+
) => PendingTelegramControlItem<TContext>["execute"];
|
|
717
|
+
}): TelegramQueueHandoffStageReceipt {
|
|
718
|
+
const receipt = getTelegramQueueHandoffReceipt(input.payload);
|
|
719
|
+
const item = restoreTelegramQueueHandoffPayload(
|
|
720
|
+
input.payload,
|
|
721
|
+
input.createControlExecution,
|
|
722
|
+
);
|
|
723
|
+
const current = input.store.getQueuedItems();
|
|
724
|
+
const next = appendTelegramQueueItem(current, item);
|
|
725
|
+
if (next !== current) input.store.setQueuedItems(next);
|
|
726
|
+
return {
|
|
727
|
+
status: "staged",
|
|
728
|
+
receiptId: receipt.receiptId,
|
|
729
|
+
sourceUpdateIds: [...receipt.sourceUpdateIds],
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
export function createTelegramQueueHandoffStagingRuntime<TContext>(input: {
|
|
734
|
+
liveStore: TelegramQueueStore<TContext>;
|
|
735
|
+
createControlExecution: (
|
|
736
|
+
payload: TelegramControlQueueHandoffPayload,
|
|
737
|
+
) => PendingTelegramControlItem<TContext>["execute"];
|
|
738
|
+
}): TelegramQueueHandoffStagingRuntime {
|
|
739
|
+
const stagedStore = createTelegramQueueStore<TContext>();
|
|
740
|
+
return {
|
|
741
|
+
stage(payload) {
|
|
742
|
+
const receipt = getTelegramQueueHandoffReceipt(payload);
|
|
743
|
+
const liveItem = findTelegramQueueReceiptItem(
|
|
744
|
+
input.liveStore.getQueuedItems(),
|
|
745
|
+
receipt,
|
|
746
|
+
);
|
|
747
|
+
if (liveItem) {
|
|
748
|
+
throw new Error(
|
|
749
|
+
`Telegram queue handoff receipt ${receipt.receiptId} is already live.`,
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
return stageTelegramQueueHandoffPayload({
|
|
753
|
+
payload,
|
|
754
|
+
store: stagedStore,
|
|
755
|
+
createControlExecution: input.createControlExecution,
|
|
756
|
+
});
|
|
757
|
+
},
|
|
758
|
+
accept(receipt) {
|
|
759
|
+
const stagedItems = stagedStore.getQueuedItems();
|
|
760
|
+
const stagedItem = findTelegramQueueReceiptItem(stagedItems, receipt);
|
|
761
|
+
if (!stagedItem) {
|
|
762
|
+
return Boolean(
|
|
763
|
+
findTelegramQueueReceiptItem(
|
|
764
|
+
input.liveStore.getQueuedItems(),
|
|
765
|
+
receipt,
|
|
766
|
+
),
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
const current = input.liveStore.getQueuedItems();
|
|
770
|
+
const next = appendTelegramQueueItem(current, stagedItem);
|
|
771
|
+
if (next !== current) input.liveStore.setQueuedItems(next);
|
|
772
|
+
stagedStore.setQueuedItems(
|
|
773
|
+
stagedItems.filter((candidate) => candidate !== stagedItem),
|
|
774
|
+
);
|
|
775
|
+
return true;
|
|
776
|
+
},
|
|
777
|
+
cancel(receipt) {
|
|
778
|
+
const stagedItems = stagedStore.getQueuedItems();
|
|
779
|
+
const stagedItem = findTelegramQueueReceiptItem(stagedItems, receipt);
|
|
780
|
+
if (!stagedItem) return false;
|
|
781
|
+
stagedStore.setQueuedItems(
|
|
782
|
+
stagedItems.filter((candidate) => candidate !== stagedItem),
|
|
783
|
+
);
|
|
784
|
+
return true;
|
|
785
|
+
},
|
|
786
|
+
hasStaged(receipt) {
|
|
787
|
+
return Boolean(
|
|
788
|
+
findTelegramQueueReceiptItem(stagedStore.getQueuedItems(), receipt),
|
|
789
|
+
);
|
|
790
|
+
},
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function getTelegramPromptTextSignature(item: PendingTelegramTurn): string {
|
|
795
|
+
return item.content
|
|
796
|
+
.filter(
|
|
797
|
+
(entry): entry is TelegramPromptTextContent => entry.type === "text",
|
|
798
|
+
)
|
|
799
|
+
.map((entry) => entry.text)
|
|
800
|
+
.join("\n");
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function isDuplicateTelegramPromptTurn(
|
|
804
|
+
left: PendingTelegramTurn,
|
|
805
|
+
right: PendingTelegramTurn,
|
|
806
|
+
): boolean {
|
|
807
|
+
return (
|
|
808
|
+
left.chatId === right.chatId &&
|
|
809
|
+
left.target?.threadId === right.target?.threadId &&
|
|
810
|
+
left.replyToMessageId === right.replyToMessageId &&
|
|
811
|
+
getTelegramPromptTextSignature(left) ===
|
|
812
|
+
getTelegramPromptTextSignature(right)
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
export function appendTelegramPromptTurnOnce<TContext = unknown>(
|
|
817
|
+
items: TelegramQueueItem<TContext>[],
|
|
818
|
+
turn: PendingTelegramTurn,
|
|
819
|
+
): { items: TelegramQueueItem<TContext>[]; appended: boolean } {
|
|
820
|
+
assertTelegramQueueItemAdmissionValid(turn);
|
|
821
|
+
if (isDuplicateTelegramQueueAdmission(items, turn)) {
|
|
822
|
+
return { items, appended: false };
|
|
823
|
+
}
|
|
824
|
+
const hasAdmissionReceipts = (turn.admissionReceipts?.length ?? 0) > 0;
|
|
825
|
+
const duplicate =
|
|
826
|
+
!hasAdmissionReceipts &&
|
|
827
|
+
items.some(
|
|
828
|
+
(item) =>
|
|
829
|
+
isPendingTelegramTurn(item) &&
|
|
830
|
+
(item.admissionReceipts?.length ?? 0) === 0 &&
|
|
831
|
+
isDuplicateTelegramPromptTurn(item, turn),
|
|
832
|
+
);
|
|
833
|
+
if (duplicate) return { items, appended: false };
|
|
834
|
+
return {
|
|
835
|
+
items: [...items, turn].sort(compareTelegramQueueItems),
|
|
836
|
+
appended: true,
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export function compareTelegramQueueItems<TContext = unknown>(
|
|
841
|
+
left: TelegramQueueItem<TContext>,
|
|
842
|
+
right: TelegramQueueItem<TContext>,
|
|
843
|
+
): number {
|
|
844
|
+
assertTelegramQueueItemAdmissionValid(left);
|
|
845
|
+
assertTelegramQueueItemAdmissionValid(right);
|
|
846
|
+
const laneRankDelta =
|
|
847
|
+
getTelegramQueueLaneRank(left.queueLane) -
|
|
848
|
+
getTelegramQueueLaneRank(right.queueLane);
|
|
849
|
+
if (laneRankDelta !== 0) return laneRankDelta;
|
|
850
|
+
if (left.laneOrder !== right.laneOrder) {
|
|
851
|
+
return left.laneOrder - right.laneOrder;
|
|
852
|
+
}
|
|
853
|
+
return left.queueOrder - right.queueOrder;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
export interface TelegramQueueMessageScope {
|
|
857
|
+
chatId?: number;
|
|
858
|
+
threadId?: number;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function isTelegramQueueItemInMessageScope<TContext = unknown>(
|
|
862
|
+
item: TelegramQueueItem<TContext>,
|
|
863
|
+
scope: TelegramQueueMessageScope | undefined,
|
|
864
|
+
): boolean {
|
|
865
|
+
if (!scope) return true;
|
|
866
|
+
if (typeof scope.chatId === "number" && item.chatId !== scope.chatId) {
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
if (typeof scope.threadId === "number") {
|
|
870
|
+
return item.target?.threadId === scope.threadId;
|
|
871
|
+
}
|
|
872
|
+
return true;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
export function removeTelegramQueueItemsByMessageIds<TContext = unknown>(
|
|
876
|
+
items: TelegramQueueItem<TContext>[],
|
|
877
|
+
messageIds: number[],
|
|
878
|
+
scope?: TelegramQueueMessageScope,
|
|
879
|
+
): {
|
|
880
|
+
items: TelegramQueueItem<TContext>[];
|
|
881
|
+
removedItems: TelegramQueueItem<TContext>[];
|
|
882
|
+
removedCount: number;
|
|
883
|
+
} {
|
|
884
|
+
if (messageIds.length === 0 || items.length === 0) {
|
|
885
|
+
return { items, removedItems: [], removedCount: 0 };
|
|
886
|
+
}
|
|
887
|
+
const deletedMessageIds = new Set(messageIds);
|
|
888
|
+
const nextItems: TelegramQueueItem<TContext>[] = [];
|
|
889
|
+
const removedItems: TelegramQueueItem<TContext>[] = [];
|
|
890
|
+
for (const item of items) {
|
|
891
|
+
const shouldRemove =
|
|
892
|
+
isPendingTelegramTurn(item) &&
|
|
893
|
+
isTelegramQueueItemInMessageScope(item, scope) &&
|
|
894
|
+
item.sourceMessageIds.some((messageId) =>
|
|
895
|
+
deletedMessageIds.has(messageId),
|
|
896
|
+
);
|
|
897
|
+
(shouldRemove ? removedItems : nextItems).push(item);
|
|
898
|
+
}
|
|
899
|
+
return {
|
|
900
|
+
items: nextItems,
|
|
901
|
+
removedItems,
|
|
902
|
+
removedCount: removedItems.length,
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
export function applyTelegramQueuePromptReactionDisposition<
|
|
907
|
+
TContext = unknown,
|
|
908
|
+
>(
|
|
909
|
+
items: TelegramQueueItem<TContext>[],
|
|
910
|
+
messageId: number,
|
|
911
|
+
disposition: TelegramQueueReactionDisposition,
|
|
912
|
+
destinationLaneOrder?: number,
|
|
913
|
+
scope?: TelegramQueueMessageScope,
|
|
914
|
+
): { items: TelegramQueueItem<TContext>[]; changed: boolean } {
|
|
915
|
+
let nextItems = items;
|
|
916
|
+
for (const [index, item] of items.entries()) {
|
|
917
|
+
if (
|
|
918
|
+
!isPendingTelegramTurn(item) ||
|
|
919
|
+
!isTelegramQueueItemInMessageScope(item, scope) ||
|
|
920
|
+
!item.sourceMessageIds.includes(messageId)
|
|
921
|
+
) {
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
const isPriority = disposition.kind === "reaction-transition"
|
|
925
|
+
? disposition.priorityEmoji === undefined
|
|
926
|
+
? item.queueLane === "priority"
|
|
927
|
+
: disposition.priorityEmoji !== null
|
|
928
|
+
: disposition.kind === "priority" ||
|
|
929
|
+
disposition.kind === "priority-suppressed";
|
|
930
|
+
const queueLane: TelegramQueueLane = isPriority ? "priority" : "default";
|
|
931
|
+
const laneOrder = item.queueLane === queueLane
|
|
932
|
+
? item.laneOrder
|
|
933
|
+
: destinationLaneOrder;
|
|
934
|
+
if (laneOrder === undefined) {
|
|
935
|
+
throw new Error("Telegram destination lane order is unavailable.");
|
|
936
|
+
}
|
|
937
|
+
const priorityEmoji = disposition.kind === "reaction-transition"
|
|
938
|
+
? disposition.priorityEmoji === undefined
|
|
939
|
+
? item.priorityEmoji
|
|
940
|
+
: disposition.priorityEmoji ?? undefined
|
|
941
|
+
: disposition.kind === "priority"
|
|
942
|
+
? disposition.emoji
|
|
943
|
+
: disposition.kind === "priority-suppressed"
|
|
944
|
+
? disposition.priorityEmoji
|
|
945
|
+
: undefined;
|
|
946
|
+
const reactionSuppressionEmoji =
|
|
947
|
+
disposition.kind === "reaction-transition"
|
|
948
|
+
? disposition.suppressionEmoji === undefined
|
|
949
|
+
? item.reactionSuppressionEmoji
|
|
950
|
+
: disposition.suppressionEmoji ?? undefined
|
|
951
|
+
: disposition.kind === "suppressed"
|
|
952
|
+
? disposition.emoji
|
|
953
|
+
: disposition.kind === "priority-suppressed"
|
|
954
|
+
? disposition.suppressionEmoji
|
|
955
|
+
: undefined;
|
|
956
|
+
if (
|
|
957
|
+
item.queueLane === queueLane &&
|
|
958
|
+
item.laneOrder === laneOrder &&
|
|
959
|
+
item.priorityEmoji === priorityEmoji &&
|
|
960
|
+
item.reactionSuppressionEmoji === reactionSuppressionEmoji
|
|
961
|
+
) {
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
if (nextItems === items) nextItems = [...items];
|
|
965
|
+
nextItems[index] = {
|
|
966
|
+
...item,
|
|
967
|
+
queueLane,
|
|
968
|
+
laneOrder,
|
|
969
|
+
priorityEmoji,
|
|
970
|
+
reactionSuppressionEmoji,
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
return { items: nextItems, changed: nextItems !== items };
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
export function consumeDispatchedTelegramPrompt<TContext = unknown>(
|
|
977
|
+
items: TelegramQueueItem<TContext>[],
|
|
978
|
+
hasPendingDispatch: boolean,
|
|
979
|
+
): {
|
|
980
|
+
activeTurn?: PendingTelegramTurn;
|
|
981
|
+
remainingItems: TelegramQueueItem<TContext>[];
|
|
982
|
+
} {
|
|
983
|
+
if (!hasPendingDispatch) {
|
|
984
|
+
return { activeTurn: undefined, remainingItems: items };
|
|
985
|
+
}
|
|
986
|
+
const nextItem = items[0];
|
|
987
|
+
if (!nextItem || !isPendingTelegramTurn(nextItem)) {
|
|
988
|
+
return { activeTurn: undefined, remainingItems: items };
|
|
989
|
+
}
|
|
990
|
+
return { activeTurn: nextItem, remainingItems: items.slice(1) };
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
export function formatQueuedTelegramItemsStatus<TContext = unknown>(
|
|
994
|
+
items: TelegramQueueItem<TContext>[],
|
|
995
|
+
): string {
|
|
996
|
+
return items.length === 0 ? "" : ` +${items.length}`;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
export function truncateTelegramQueueSummary(
|
|
1000
|
+
text: string,
|
|
1001
|
+
maxWords = 5,
|
|
1002
|
+
maxLength = 40,
|
|
1003
|
+
): string {
|
|
1004
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
1005
|
+
if (!normalized) return "";
|
|
1006
|
+
const words = normalized.split(" ");
|
|
1007
|
+
let summary = words.slice(0, maxWords).join(" ");
|
|
1008
|
+
if (summary.length === 0) summary = normalized;
|
|
1009
|
+
if (summary.length > maxLength) {
|
|
1010
|
+
summary = summary.slice(0, maxLength).trimEnd();
|
|
1011
|
+
}
|
|
1012
|
+
return summary.length < normalized.length || words.length > maxWords
|
|
1013
|
+
? `${summary}…`
|
|
1014
|
+
: summary;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
export function canDispatchTelegramTurnState(
|
|
1018
|
+
state: TelegramDispatchGuardState,
|
|
1019
|
+
): boolean {
|
|
1020
|
+
return (
|
|
1021
|
+
!state.compactionInProgress &&
|
|
1022
|
+
!state.hasActiveTelegramTurn &&
|
|
1023
|
+
!state.hasPendingTelegramDispatch &&
|
|
1024
|
+
state.isIdle &&
|
|
1025
|
+
!state.hasPendingMessages
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
export interface TelegramDispatchReadinessDeps<TContext> {
|
|
1030
|
+
isCompactionInProgress: () => boolean;
|
|
1031
|
+
hasActiveTurn: () => boolean;
|
|
1032
|
+
hasDispatchPending: () => boolean;
|
|
1033
|
+
isIdle: (ctx: TContext) => boolean;
|
|
1034
|
+
hasPendingMessages: (ctx: TContext) => boolean;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
export function createTelegramDispatchReadinessChecker<TContext>(
|
|
1038
|
+
deps: TelegramDispatchReadinessDeps<TContext>,
|
|
1039
|
+
): (ctx: TContext) => boolean {
|
|
1040
|
+
return (ctx) =>
|
|
1041
|
+
canDispatchTelegramTurnState({
|
|
1042
|
+
compactionInProgress: deps.isCompactionInProgress(),
|
|
1043
|
+
hasActiveTelegramTurn: deps.hasActiveTurn(),
|
|
1044
|
+
hasPendingTelegramDispatch: deps.hasDispatchPending(),
|
|
1045
|
+
isIdle: deps.isIdle(ctx),
|
|
1046
|
+
hasPendingMessages: deps.hasPendingMessages(ctx),
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
export function buildPendingTelegramControlItem<TContext = unknown>(options: {
|
|
1051
|
+
chatId: number;
|
|
1052
|
+
target?: TelegramQueueTarget;
|
|
1053
|
+
replyToMessageId: number;
|
|
1054
|
+
controlType: PendingTelegramControlItem<TContext>["controlType"];
|
|
1055
|
+
queueOrder: number;
|
|
1056
|
+
laneOrder: number;
|
|
1057
|
+
statusSummary: string;
|
|
1058
|
+
admissionReceipts?: TelegramQueueAdmissionReceipt[];
|
|
1059
|
+
execute: PendingTelegramControlItem<TContext>["execute"];
|
|
1060
|
+
}): PendingTelegramControlItem<TContext> {
|
|
1061
|
+
return {
|
|
1062
|
+
kind: "control",
|
|
1063
|
+
controlType: options.controlType,
|
|
1064
|
+
chatId: options.chatId,
|
|
1065
|
+
...(options.target ? { target: options.target } : {}),
|
|
1066
|
+
replyToMessageId: options.replyToMessageId,
|
|
1067
|
+
queueOrder: options.queueOrder,
|
|
1068
|
+
queueLane: "control",
|
|
1069
|
+
laneOrder: options.laneOrder,
|
|
1070
|
+
statusSummary: options.statusSummary,
|
|
1071
|
+
...(options.admissionReceipts?.length
|
|
1072
|
+
? { admissionReceipts: structuredClone(options.admissionReceipts) }
|
|
1073
|
+
: {}),
|
|
1074
|
+
execute: options.execute,
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
export interface TelegramControlItemBuilderDeps {
|
|
1079
|
+
allocateItemOrder: () => number;
|
|
1080
|
+
allocateControlOrder: () => number;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
export function createTelegramControlItemBuilder<TContext = unknown>(
|
|
1084
|
+
deps: TelegramControlItemBuilderDeps,
|
|
1085
|
+
): (options: {
|
|
1086
|
+
chatId: number;
|
|
1087
|
+
target?: TelegramQueueTarget;
|
|
1088
|
+
replyToMessageId: number;
|
|
1089
|
+
controlType: PendingTelegramControlItem<TContext>["controlType"];
|
|
1090
|
+
statusSummary: string;
|
|
1091
|
+
admissionReceipts?: TelegramQueueAdmissionReceipt[];
|
|
1092
|
+
execute: PendingTelegramControlItem<TContext>["execute"];
|
|
1093
|
+
}) => PendingTelegramControlItem<TContext> {
|
|
1094
|
+
return (options) =>
|
|
1095
|
+
buildPendingTelegramControlItem<TContext>({
|
|
1096
|
+
...options,
|
|
1097
|
+
queueOrder: deps.allocateItemOrder(),
|
|
1098
|
+
laneOrder: deps.allocateControlOrder(),
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// --- Dispatch Planning ---
|
|
1103
|
+
|
|
1104
|
+
export type TelegramQueueDispatchAction<TContext = unknown> =
|
|
1105
|
+
| { kind: "none"; remainingItems: TelegramQueueItem<TContext>[] }
|
|
1106
|
+
| {
|
|
1107
|
+
kind: "control";
|
|
1108
|
+
item: PendingTelegramControlItem<TContext>;
|
|
1109
|
+
remainingItems: TelegramQueueItem<TContext>[];
|
|
1110
|
+
}
|
|
1111
|
+
| {
|
|
1112
|
+
kind: "prompt";
|
|
1113
|
+
item: PendingTelegramTurn;
|
|
1114
|
+
remainingItems: TelegramQueueItem<TContext>[];
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
export function planNextTelegramQueueAction<TContext = unknown>(
|
|
1118
|
+
items: TelegramQueueItem<TContext>[],
|
|
1119
|
+
canDispatch: boolean,
|
|
1120
|
+
): TelegramQueueDispatchAction<TContext> {
|
|
1121
|
+
if (!canDispatch || items.length === 0) {
|
|
1122
|
+
return { kind: "none", remainingItems: items };
|
|
1123
|
+
}
|
|
1124
|
+
const [firstItem, ...remainingItems] = items;
|
|
1125
|
+
if (!firstItem) {
|
|
1126
|
+
return { kind: "none", remainingItems: items };
|
|
1127
|
+
}
|
|
1128
|
+
assertTelegramQueueItemAdmissionValid(firstItem);
|
|
1129
|
+
if (isPendingTelegramTurn(firstItem)) {
|
|
1130
|
+
return { kind: "prompt", item: firstItem, remainingItems: items };
|
|
1131
|
+
}
|
|
1132
|
+
return { kind: "control", item: firstItem, remainingItems };
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
export function shouldDispatchAfterTelegramAgentEnd(options: {
|
|
1136
|
+
hasTurn: boolean;
|
|
1137
|
+
stopReason?: string;
|
|
1138
|
+
foldQueuedPromptsIntoHistory: boolean;
|
|
1139
|
+
}): boolean {
|
|
1140
|
+
if (!options.hasTurn) return true;
|
|
1141
|
+
if (options.stopReason === "aborted") {
|
|
1142
|
+
return !options.foldQueuedPromptsIntoHistory;
|
|
1143
|
+
}
|
|
1144
|
+
return true;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// --- Agent Runtime ---
|
|
1148
|
+
|
|
1149
|
+
export interface TelegramAgentStartPlan<TContext = unknown> {
|
|
1150
|
+
activeTurn?: PendingTelegramTurn;
|
|
1151
|
+
remainingItems: TelegramQueueItem<TContext>[];
|
|
1152
|
+
shouldResetPendingModelSwitch: boolean;
|
|
1153
|
+
shouldResetToolExecutions: boolean;
|
|
1154
|
+
shouldClearDispatchPending: boolean;
|
|
1155
|
+
shouldClearAbortHistory: boolean;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
export interface TelegramAgentStartRuntimeDeps<
|
|
1159
|
+
TTurn extends PendingTelegramTurn,
|
|
1160
|
+
TContext = unknown,
|
|
1161
|
+
> extends TelegramRuntimeEventRecorderPort {
|
|
1162
|
+
queuedItems: TelegramQueueItem<TContext>[];
|
|
1163
|
+
hasPendingDispatch: boolean;
|
|
1164
|
+
hasActiveTurn: boolean;
|
|
1165
|
+
resetToolExecutions: () => void;
|
|
1166
|
+
resetPendingModelSwitch: () => void;
|
|
1167
|
+
setQueuedItems: (items: TelegramQueueItem<TContext>[]) => void;
|
|
1168
|
+
clearDispatchPending: () => void;
|
|
1169
|
+
setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
|
|
1170
|
+
setActiveTurn: (turn: TTurn) => void;
|
|
1171
|
+
onPromptHandedOff?: (turn: TTurn) => void;
|
|
1172
|
+
createPreviewState: () => void;
|
|
1173
|
+
startTypingLoop: () => void;
|
|
1174
|
+
updateStatus: () => void;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
export interface TelegramAgentStartHookRuntimeDeps<
|
|
1178
|
+
TTurn extends PendingTelegramTurn,
|
|
1179
|
+
TContext = unknown,
|
|
1180
|
+
> extends TelegramRuntimeEventRecorderPort {
|
|
1181
|
+
setAbortHandler: (ctx: TContext) => void;
|
|
1182
|
+
getQueuedItems: () => TelegramQueueItem<TContext>[];
|
|
1183
|
+
hasPendingDispatch: () => boolean;
|
|
1184
|
+
hasActiveTurn: () => boolean;
|
|
1185
|
+
resetToolExecutions: () => void;
|
|
1186
|
+
resetPendingModelSwitch: () => void;
|
|
1187
|
+
setQueuedItems: (items: TelegramQueueItem<TContext>[]) => void;
|
|
1188
|
+
clearDispatchPending: () => void;
|
|
1189
|
+
setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
|
|
1190
|
+
setActiveTurn: (turn: TTurn) => void;
|
|
1191
|
+
onPromptHandedOff?: (turn: TTurn, ctx: TContext) => void;
|
|
1192
|
+
createPreviewState: () => void;
|
|
1193
|
+
startTypingLoop: (ctx: TContext) => void;
|
|
1194
|
+
updateStatus: (ctx: TContext) => void;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
export type TelegramAgentStartHookEvent = unknown;
|
|
1198
|
+
|
|
1199
|
+
export interface TelegramToolExecutionRuntimeDeps {
|
|
1200
|
+
hasActiveTurn: () => boolean;
|
|
1201
|
+
getActiveToolExecutions: () => number;
|
|
1202
|
+
setActiveToolExecutions: (count: number) => void;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
export interface TelegramToolExecutionEndRuntimeDeps extends TelegramToolExecutionRuntimeDeps {
|
|
1206
|
+
triggerPendingModelSwitchAbort: () => void;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
export interface TelegramToolExecutionHookRuntimeDeps<
|
|
1210
|
+
TContext,
|
|
1211
|
+
> extends TelegramToolExecutionRuntimeDeps {
|
|
1212
|
+
triggerPendingModelSwitchAbort: (ctx: TContext) => unknown;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
export type TelegramToolExecutionHookEvent = unknown;
|
|
1216
|
+
|
|
1217
|
+
export function buildTelegramAgentStartPlan<TContext = unknown>(options: {
|
|
1218
|
+
queuedItems: TelegramQueueItem<TContext>[];
|
|
1219
|
+
hasPendingDispatch: boolean;
|
|
1220
|
+
hasActiveTurn: boolean;
|
|
1221
|
+
}): TelegramAgentStartPlan<TContext> {
|
|
1222
|
+
if (options.hasActiveTurn || !options.hasPendingDispatch) {
|
|
1223
|
+
return {
|
|
1224
|
+
activeTurn: undefined,
|
|
1225
|
+
remainingItems: options.queuedItems,
|
|
1226
|
+
shouldResetPendingModelSwitch: true,
|
|
1227
|
+
shouldResetToolExecutions: true,
|
|
1228
|
+
shouldClearDispatchPending: options.hasPendingDispatch,
|
|
1229
|
+
shouldClearAbortHistory:
|
|
1230
|
+
!options.hasActiveTurn && !options.hasPendingDispatch,
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
const nextDispatch = consumeDispatchedTelegramPrompt(
|
|
1234
|
+
options.queuedItems,
|
|
1235
|
+
options.hasPendingDispatch,
|
|
1236
|
+
);
|
|
1237
|
+
return {
|
|
1238
|
+
activeTurn: nextDispatch.activeTurn,
|
|
1239
|
+
remainingItems: nextDispatch.remainingItems,
|
|
1240
|
+
shouldResetPendingModelSwitch: true,
|
|
1241
|
+
shouldResetToolExecutions: true,
|
|
1242
|
+
shouldClearDispatchPending: options.hasPendingDispatch,
|
|
1243
|
+
shouldClearAbortHistory: false,
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
export function handleTelegramAgentStartRuntime<
|
|
1248
|
+
TTurn extends PendingTelegramTurn,
|
|
1249
|
+
TContext = unknown,
|
|
1250
|
+
>(deps: TelegramAgentStartRuntimeDeps<TTurn, TContext>): void {
|
|
1251
|
+
const startPlan = buildTelegramAgentStartPlan({
|
|
1252
|
+
queuedItems: deps.queuedItems,
|
|
1253
|
+
hasPendingDispatch: deps.hasPendingDispatch,
|
|
1254
|
+
hasActiveTurn: deps.hasActiveTurn,
|
|
1255
|
+
});
|
|
1256
|
+
if (startPlan.shouldResetToolExecutions) deps.resetToolExecutions();
|
|
1257
|
+
if (startPlan.shouldResetPendingModelSwitch) deps.resetPendingModelSwitch();
|
|
1258
|
+
if (startPlan.shouldClearAbortHistory) {
|
|
1259
|
+
deps.setFoldQueuedPromptsIntoHistory(false);
|
|
1260
|
+
}
|
|
1261
|
+
deps.setQueuedItems(startPlan.remainingItems);
|
|
1262
|
+
if (startPlan.shouldClearDispatchPending) deps.clearDispatchPending();
|
|
1263
|
+
if (startPlan.activeTurn) {
|
|
1264
|
+
const activeTurn = startPlan.activeTurn as TTurn;
|
|
1265
|
+
deps.setActiveTurn(activeTurn);
|
|
1266
|
+
try {
|
|
1267
|
+
deps.onPromptHandedOff?.(activeTurn);
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
deps.recordRuntimeEvent?.("queue", error, {
|
|
1270
|
+
phase: "prompt-handoff-receipt-settlement",
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
deps.createPreviewState();
|
|
1274
|
+
deps.startTypingLoop();
|
|
1275
|
+
}
|
|
1276
|
+
deps.updateStatus();
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
export function createTelegramAgentStartHook<
|
|
1280
|
+
TTurn extends PendingTelegramTurn,
|
|
1281
|
+
TContext = unknown,
|
|
1282
|
+
>(deps: TelegramAgentStartHookRuntimeDeps<TTurn, TContext>) {
|
|
1283
|
+
return async (
|
|
1284
|
+
_event: TelegramAgentStartHookEvent,
|
|
1285
|
+
ctx: TContext,
|
|
1286
|
+
): Promise<void> => {
|
|
1287
|
+
deps.setAbortHandler(ctx);
|
|
1288
|
+
handleTelegramAgentStartRuntime<TTurn, TContext>({
|
|
1289
|
+
queuedItems: deps.getQueuedItems(),
|
|
1290
|
+
hasPendingDispatch: deps.hasPendingDispatch(),
|
|
1291
|
+
hasActiveTurn: deps.hasActiveTurn(),
|
|
1292
|
+
resetToolExecutions: deps.resetToolExecutions,
|
|
1293
|
+
resetPendingModelSwitch: deps.resetPendingModelSwitch,
|
|
1294
|
+
setQueuedItems: deps.setQueuedItems,
|
|
1295
|
+
clearDispatchPending: deps.clearDispatchPending,
|
|
1296
|
+
setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
|
|
1297
|
+
setActiveTurn: deps.setActiveTurn,
|
|
1298
|
+
onPromptHandedOff: (turn) => deps.onPromptHandedOff?.(turn, ctx),
|
|
1299
|
+
createPreviewState: deps.createPreviewState,
|
|
1300
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1301
|
+
startTypingLoop: () => deps.startTypingLoop(ctx),
|
|
1302
|
+
updateStatus: () => deps.updateStatus(ctx),
|
|
1303
|
+
});
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
export function getNextTelegramToolExecutionCount(options: {
|
|
1308
|
+
hasActiveTurn: boolean;
|
|
1309
|
+
currentCount: number;
|
|
1310
|
+
event: "start" | "end";
|
|
1311
|
+
}): number {
|
|
1312
|
+
if (!options.hasActiveTurn) return options.currentCount;
|
|
1313
|
+
if (options.event === "start") {
|
|
1314
|
+
return options.currentCount + 1;
|
|
1315
|
+
}
|
|
1316
|
+
return Math.max(0, options.currentCount - 1);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
export function handleTelegramToolExecutionStartRuntime(
|
|
1320
|
+
deps: TelegramToolExecutionRuntimeDeps,
|
|
1321
|
+
): void {
|
|
1322
|
+
deps.setActiveToolExecutions(
|
|
1323
|
+
getNextTelegramToolExecutionCount({
|
|
1324
|
+
hasActiveTurn: deps.hasActiveTurn(),
|
|
1325
|
+
currentCount: deps.getActiveToolExecutions(),
|
|
1326
|
+
event: "start",
|
|
1327
|
+
}),
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
export function handleTelegramToolExecutionEndRuntime(
|
|
1332
|
+
deps: TelegramToolExecutionEndRuntimeDeps,
|
|
1333
|
+
): void {
|
|
1334
|
+
const hasActiveTurn = deps.hasActiveTurn();
|
|
1335
|
+
deps.setActiveToolExecutions(
|
|
1336
|
+
getNextTelegramToolExecutionCount({
|
|
1337
|
+
hasActiveTurn,
|
|
1338
|
+
currentCount: deps.getActiveToolExecutions(),
|
|
1339
|
+
event: "end",
|
|
1340
|
+
}),
|
|
1341
|
+
);
|
|
1342
|
+
if (hasActiveTurn) deps.triggerPendingModelSwitchAbort();
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
export type TelegramAgentLifecycleHooksRuntimeDeps<
|
|
1346
|
+
TTurn extends PendingTelegramTurn,
|
|
1347
|
+
TContext,
|
|
1348
|
+
TMessage,
|
|
1349
|
+
TReplyMarkup = unknown,
|
|
1350
|
+
> = TelegramAgentStartHookRuntimeDeps<TTurn, TContext> &
|
|
1351
|
+
TelegramAgentEndHookRuntimeDeps<TTurn, TContext, TMessage, TReplyMarkup> &
|
|
1352
|
+
TelegramToolExecutionHookRuntimeDeps<TContext>;
|
|
1353
|
+
|
|
1354
|
+
export function createTelegramAgentLifecycleHooks<
|
|
1355
|
+
TTurn extends PendingTelegramTurn,
|
|
1356
|
+
TContext,
|
|
1357
|
+
TMessage,
|
|
1358
|
+
TReplyMarkup = unknown,
|
|
1359
|
+
>(
|
|
1360
|
+
deps: TelegramAgentLifecycleHooksRuntimeDeps<
|
|
1361
|
+
TTurn,
|
|
1362
|
+
TContext,
|
|
1363
|
+
TMessage,
|
|
1364
|
+
TReplyMarkup
|
|
1365
|
+
>,
|
|
1366
|
+
) {
|
|
1367
|
+
const onAgentStart = createTelegramAgentStartHook<TTurn, TContext>(deps);
|
|
1368
|
+
const deliverAgentEnd = createTelegramAgentEndHook<
|
|
1369
|
+
TTurn,
|
|
1370
|
+
TContext,
|
|
1371
|
+
TMessage,
|
|
1372
|
+
TReplyMarkup
|
|
1373
|
+
>(deps);
|
|
1374
|
+
let retainedErrorEvent: TelegramAgentEndHookEvent<TMessage> | undefined;
|
|
1375
|
+
return {
|
|
1376
|
+
onAgentStart,
|
|
1377
|
+
async onAgentEnd(
|
|
1378
|
+
event: TelegramAgentEndHookEvent<TMessage>,
|
|
1379
|
+
ctx: TContext,
|
|
1380
|
+
): Promise<void> {
|
|
1381
|
+
const turn = deps.getActiveTurn();
|
|
1382
|
+
const assistant = turn ? deps.extractAssistant(event.messages) : {};
|
|
1383
|
+
if (turn && assistant.stopReason === "error") {
|
|
1384
|
+
retainedErrorEvent = event;
|
|
1385
|
+
deps.recordRuntimeEvent?.(
|
|
1386
|
+
"provider-retry",
|
|
1387
|
+
new Error("Retained Telegram turn after retryable agent error"),
|
|
1388
|
+
{ phase: "retained", hasFinalText: !!assistant.text?.trim() },
|
|
1389
|
+
);
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
if (retainedErrorEvent) {
|
|
1393
|
+
retainedErrorEvent = undefined;
|
|
1394
|
+
deps.recordRuntimeEvent?.(
|
|
1395
|
+
"provider-retry",
|
|
1396
|
+
new Error("Recovered retained Telegram turn after agent retry"),
|
|
1397
|
+
{ phase: "recovered" },
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
await deliverAgentEnd(event, ctx, assistant);
|
|
1401
|
+
},
|
|
1402
|
+
async onAgentSettled(_event: unknown, ctx: TContext): Promise<void> {
|
|
1403
|
+
const event = retainedErrorEvent;
|
|
1404
|
+
if (!event) return;
|
|
1405
|
+
retainedErrorEvent = undefined;
|
|
1406
|
+
deps.recordRuntimeEvent?.(
|
|
1407
|
+
"provider-retry",
|
|
1408
|
+
new Error("Finalized retained Telegram turn after agent settled"),
|
|
1409
|
+
{ phase: "settled-failure" },
|
|
1410
|
+
);
|
|
1411
|
+
await deliverAgentEnd(event, ctx);
|
|
1412
|
+
},
|
|
1413
|
+
clearRetainedAgentEnd(): void {
|
|
1414
|
+
retainedErrorEvent = undefined;
|
|
1415
|
+
},
|
|
1416
|
+
...createTelegramToolExecutionHooks<TContext>(deps),
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
export function createTelegramToolExecutionHooks<TContext>(
|
|
1421
|
+
deps: TelegramToolExecutionHookRuntimeDeps<TContext>,
|
|
1422
|
+
) {
|
|
1423
|
+
return {
|
|
1424
|
+
onToolExecutionStart: (): void => {
|
|
1425
|
+
handleTelegramToolExecutionStartRuntime(deps);
|
|
1426
|
+
},
|
|
1427
|
+
onToolExecutionEnd: (
|
|
1428
|
+
_event: TelegramToolExecutionHookEvent,
|
|
1429
|
+
ctx: TContext,
|
|
1430
|
+
): void => {
|
|
1431
|
+
handleTelegramToolExecutionEndRuntime({
|
|
1432
|
+
hasActiveTurn: deps.hasActiveTurn,
|
|
1433
|
+
getActiveToolExecutions: deps.getActiveToolExecutions,
|
|
1434
|
+
setActiveToolExecutions: deps.setActiveToolExecutions,
|
|
1435
|
+
triggerPendingModelSwitchAbort: () => {
|
|
1436
|
+
deps.triggerPendingModelSwitchAbort(ctx);
|
|
1437
|
+
},
|
|
1438
|
+
});
|
|
1439
|
+
},
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// --- Agent End Lifecycle ---
|
|
1444
|
+
|
|
1445
|
+
export interface TelegramAgentEndPlan {
|
|
1446
|
+
kind: "no-turn" | "aborted" | "error" | "text" | "attachments-only" | "empty";
|
|
1447
|
+
shouldClearPreview: boolean;
|
|
1448
|
+
shouldDispatchNext: boolean;
|
|
1449
|
+
shouldSendErrorMessage: boolean;
|
|
1450
|
+
shouldSendAttachmentNotice: boolean;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
export interface TelegramAgentEndAssistantResult {
|
|
1454
|
+
text?: string;
|
|
1455
|
+
stopReason?: string;
|
|
1456
|
+
errorMessage?: string;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
export interface TelegramAgentEndOutboundVoiceReply {
|
|
1460
|
+
text: string;
|
|
1461
|
+
lang?: string;
|
|
1462
|
+
rate?: string;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
export interface TelegramAgentEndOutboundReplyPlan<TReplyMarkup = unknown> {
|
|
1466
|
+
markdown: string;
|
|
1467
|
+
replyMarkup?: TReplyMarkup;
|
|
1468
|
+
voiceText?: string;
|
|
1469
|
+
voiceReplies?: TelegramAgentEndOutboundVoiceReply[];
|
|
1470
|
+
lang?: string;
|
|
1471
|
+
rate?: string;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
export interface TelegramAgentEndRuntimeDeps<
|
|
1475
|
+
TTurn extends PendingTelegramTurn,
|
|
1476
|
+
TReplyMarkup = unknown,
|
|
1477
|
+
> {
|
|
1478
|
+
turn: TTurn | undefined;
|
|
1479
|
+
assistant: TelegramAgentEndAssistantResult;
|
|
1480
|
+
foldQueuedPromptsIntoHistory: boolean;
|
|
1481
|
+
resetRuntimeState: () => void;
|
|
1482
|
+
isSessionActive?: () => boolean;
|
|
1483
|
+
isTurnTransportActive?: (turn: TTurn) => boolean;
|
|
1484
|
+
waitForTypingIdle?: () => Promise<void>;
|
|
1485
|
+
waitForActivityIdle?: () => Promise<void>;
|
|
1486
|
+
updateStatus: () => void;
|
|
1487
|
+
dispatchNextQueuedTelegramTurn: () => void;
|
|
1488
|
+
scheduleActiveTurnDelivery?: (task: () => Promise<void>) => void;
|
|
1489
|
+
clearPreview: (
|
|
1490
|
+
chatId: number,
|
|
1491
|
+
options?: { target?: TelegramQueueTarget },
|
|
1492
|
+
) => Promise<void>;
|
|
1493
|
+
setPreviewPendingText: (text: string) => void;
|
|
1494
|
+
finalizeMarkdownPreview: (
|
|
1495
|
+
chatId: number,
|
|
1496
|
+
markdown: string,
|
|
1497
|
+
replyToMessageId: number,
|
|
1498
|
+
options?: { replyMarkup?: TReplyMarkup; target?: TelegramQueueTarget },
|
|
1499
|
+
) => Promise<boolean>;
|
|
1500
|
+
sendMarkdownReply: (
|
|
1501
|
+
chatId: number,
|
|
1502
|
+
replyToMessageId: number | undefined,
|
|
1503
|
+
markdown: string,
|
|
1504
|
+
options?: { replyMarkup?: TReplyMarkup; target?: TelegramQueueTarget },
|
|
1505
|
+
) => Promise<unknown>;
|
|
1506
|
+
sendTextReply: (
|
|
1507
|
+
chatId: number,
|
|
1508
|
+
replyToMessageId: number,
|
|
1509
|
+
text: string,
|
|
1510
|
+
options?: { target?: TelegramQueueTarget },
|
|
1511
|
+
) => Promise<unknown>;
|
|
1512
|
+
sendQueuedAttachments: (turn: TTurn) => Promise<void>;
|
|
1513
|
+
sendRichAttachmentReply?: (
|
|
1514
|
+
turn: TTurn,
|
|
1515
|
+
markdown: string,
|
|
1516
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
1517
|
+
) => Promise<boolean>;
|
|
1518
|
+
answerGuestQuery?: (
|
|
1519
|
+
guestQueryId: string,
|
|
1520
|
+
text?: string,
|
|
1521
|
+
options?: { parseMode?: string },
|
|
1522
|
+
) => Promise<void>;
|
|
1523
|
+
sendGuestReply?: (guestQueryId: string, markdown: string) => Promise<void>;
|
|
1524
|
+
sendGuestAttachment?: (
|
|
1525
|
+
turn: TTurn,
|
|
1526
|
+
attachment: QueuedAttachment,
|
|
1527
|
+
caption?: string,
|
|
1528
|
+
) => Promise<void>;
|
|
1529
|
+
sendGuestVoiceReply?: (
|
|
1530
|
+
turn: TTurn,
|
|
1531
|
+
plan: TelegramAgentEndOutboundReplyPlan<TReplyMarkup>,
|
|
1532
|
+
caption?: string,
|
|
1533
|
+
) => Promise<void>;
|
|
1534
|
+
planOutboundReply?: (
|
|
1535
|
+
markdown: string,
|
|
1536
|
+
) => TelegramAgentEndOutboundReplyPlan<TReplyMarkup>;
|
|
1537
|
+
sendOutboundReplyArtifacts?: (
|
|
1538
|
+
turn: TTurn,
|
|
1539
|
+
plan: TelegramAgentEndOutboundReplyPlan,
|
|
1540
|
+
options?: { replyToPrompt?: boolean },
|
|
1541
|
+
) => Promise<void>;
|
|
1542
|
+
recordRuntimeEvent?: (
|
|
1543
|
+
category: string,
|
|
1544
|
+
error: unknown,
|
|
1545
|
+
details?: Record<string, unknown>,
|
|
1546
|
+
) => void;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
export interface TelegramAgentEndHookRuntimeDeps<
|
|
1550
|
+
TTurn extends PendingTelegramTurn,
|
|
1551
|
+
TContext,
|
|
1552
|
+
TMessage,
|
|
1553
|
+
TReplyMarkup = unknown,
|
|
1554
|
+
> {
|
|
1555
|
+
getActiveTurn: () => TTurn | undefined;
|
|
1556
|
+
loadConfig?: () => Promise<void>;
|
|
1557
|
+
extractAssistant: (
|
|
1558
|
+
messages: readonly TMessage[],
|
|
1559
|
+
) => TelegramAgentEndAssistantResult;
|
|
1560
|
+
getFoldQueuedPromptsIntoHistory: () => boolean;
|
|
1561
|
+
resetRuntimeState: () => void;
|
|
1562
|
+
isSessionActive?: (ctx: TContext) => boolean;
|
|
1563
|
+
isTurnTransportActive?: (turn: TTurn) => boolean;
|
|
1564
|
+
waitForTypingIdle?: () => Promise<void>;
|
|
1565
|
+
waitForActivityIdle?: () => Promise<void>;
|
|
1566
|
+
updateStatus: (ctx: TContext) => void;
|
|
1567
|
+
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
1568
|
+
requestDeferredDispatchNextQueuedTelegramTurn: (
|
|
1569
|
+
dispatch: (ctx: TContext) => void,
|
|
1570
|
+
) => void;
|
|
1571
|
+
scheduleActiveTurnDelivery?: TelegramAgentEndRuntimeDeps<
|
|
1572
|
+
TTurn,
|
|
1573
|
+
TReplyMarkup
|
|
1574
|
+
>["scheduleActiveTurnDelivery"];
|
|
1575
|
+
clearPreview: TelegramAgentEndRuntimeDeps<
|
|
1576
|
+
TTurn,
|
|
1577
|
+
TReplyMarkup
|
|
1578
|
+
>["clearPreview"];
|
|
1579
|
+
setPreviewPendingText: (text: string) => void;
|
|
1580
|
+
finalizeMarkdownPreview: TelegramAgentEndRuntimeDeps<
|
|
1581
|
+
TTurn,
|
|
1582
|
+
TReplyMarkup
|
|
1583
|
+
>["finalizeMarkdownPreview"];
|
|
1584
|
+
sendMarkdownReply: TelegramAgentEndRuntimeDeps<
|
|
1585
|
+
TTurn,
|
|
1586
|
+
TReplyMarkup
|
|
1587
|
+
>["sendMarkdownReply"];
|
|
1588
|
+
sendTextReply: TelegramAgentEndRuntimeDeps<TTurn>["sendTextReply"];
|
|
1589
|
+
sendQueuedAttachments: (turn: TTurn) => Promise<void>;
|
|
1590
|
+
sendRichAttachmentReply?: TelegramAgentEndRuntimeDeps<
|
|
1591
|
+
TTurn,
|
|
1592
|
+
TReplyMarkup
|
|
1593
|
+
>["sendRichAttachmentReply"];
|
|
1594
|
+
answerGuestQuery?: TelegramAgentEndRuntimeDeps<TTurn>["answerGuestQuery"];
|
|
1595
|
+
sendGuestReply?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestReply"];
|
|
1596
|
+
sendGuestAttachment?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestAttachment"];
|
|
1597
|
+
sendGuestVoiceReply?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestVoiceReply"];
|
|
1598
|
+
planOutboundReply?: TelegramAgentEndRuntimeDeps<
|
|
1599
|
+
TTurn,
|
|
1600
|
+
TReplyMarkup
|
|
1601
|
+
>["planOutboundReply"];
|
|
1602
|
+
sendOutboundReplyArtifacts?: TelegramAgentEndRuntimeDeps<TTurn>["sendOutboundReplyArtifacts"];
|
|
1603
|
+
recordRuntimeEvent?: TelegramAgentEndRuntimeDeps<TTurn>["recordRuntimeEvent"];
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
export interface TelegramAgentEndHookEvent<TMessage> {
|
|
1607
|
+
messages: readonly TMessage[];
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
export function buildTelegramAgentEndPlan(options: {
|
|
1611
|
+
hasTurn: boolean;
|
|
1612
|
+
stopReason?: string;
|
|
1613
|
+
hasFinalText: boolean;
|
|
1614
|
+
hasQueuedAttachments: boolean;
|
|
1615
|
+
foldQueuedPromptsIntoHistory: boolean;
|
|
1616
|
+
}): TelegramAgentEndPlan {
|
|
1617
|
+
const shouldDispatchNext = shouldDispatchAfterTelegramAgentEnd({
|
|
1618
|
+
hasTurn: options.hasTurn,
|
|
1619
|
+
stopReason: options.stopReason,
|
|
1620
|
+
foldQueuedPromptsIntoHistory: options.foldQueuedPromptsIntoHistory,
|
|
1621
|
+
});
|
|
1622
|
+
if (!options.hasTurn) {
|
|
1623
|
+
return {
|
|
1624
|
+
kind: "no-turn",
|
|
1625
|
+
shouldClearPreview: false,
|
|
1626
|
+
shouldDispatchNext,
|
|
1627
|
+
shouldSendErrorMessage: false,
|
|
1628
|
+
shouldSendAttachmentNotice: false,
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
if (options.stopReason === "aborted") {
|
|
1632
|
+
return {
|
|
1633
|
+
kind: "aborted",
|
|
1634
|
+
shouldClearPreview: true,
|
|
1635
|
+
shouldDispatchNext,
|
|
1636
|
+
shouldSendErrorMessage: false,
|
|
1637
|
+
shouldSendAttachmentNotice: false,
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
if (options.stopReason === "error") {
|
|
1641
|
+
return {
|
|
1642
|
+
kind: "error",
|
|
1643
|
+
shouldClearPreview: true,
|
|
1644
|
+
shouldDispatchNext,
|
|
1645
|
+
shouldSendErrorMessage: true,
|
|
1646
|
+
shouldSendAttachmentNotice: false,
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
if (options.hasFinalText) {
|
|
1650
|
+
return {
|
|
1651
|
+
kind: "text",
|
|
1652
|
+
shouldClearPreview: false,
|
|
1653
|
+
shouldDispatchNext,
|
|
1654
|
+
shouldSendErrorMessage: false,
|
|
1655
|
+
shouldSendAttachmentNotice: false,
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
if (options.hasQueuedAttachments) {
|
|
1659
|
+
return {
|
|
1660
|
+
kind: "attachments-only",
|
|
1661
|
+
shouldClearPreview: true,
|
|
1662
|
+
shouldDispatchNext,
|
|
1663
|
+
shouldSendErrorMessage: false,
|
|
1664
|
+
shouldSendAttachmentNotice: true,
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
return {
|
|
1668
|
+
kind: "empty",
|
|
1669
|
+
shouldClearPreview: true,
|
|
1670
|
+
shouldDispatchNext,
|
|
1671
|
+
shouldSendErrorMessage: false,
|
|
1672
|
+
shouldSendAttachmentNotice: false,
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
export function createTelegramAgentEndHook<
|
|
1677
|
+
TTurn extends PendingTelegramTurn,
|
|
1678
|
+
TContext,
|
|
1679
|
+
TMessage,
|
|
1680
|
+
TReplyMarkup = unknown,
|
|
1681
|
+
>(
|
|
1682
|
+
deps: TelegramAgentEndHookRuntimeDeps<
|
|
1683
|
+
TTurn,
|
|
1684
|
+
TContext,
|
|
1685
|
+
TMessage,
|
|
1686
|
+
TReplyMarkup
|
|
1687
|
+
>,
|
|
1688
|
+
) {
|
|
1689
|
+
return async (
|
|
1690
|
+
event: TelegramAgentEndHookEvent<TMessage>,
|
|
1691
|
+
ctx: TContext,
|
|
1692
|
+
assistantOverride?: TelegramAgentEndAssistantResult,
|
|
1693
|
+
): Promise<void> => {
|
|
1694
|
+
await deps.loadConfig?.();
|
|
1695
|
+
if (deps.isSessionActive && !deps.isSessionActive(ctx)) return;
|
|
1696
|
+
const turn = deps.getActiveTurn();
|
|
1697
|
+
await handleTelegramAgentEndRuntime({
|
|
1698
|
+
turn,
|
|
1699
|
+
assistant:
|
|
1700
|
+
assistantOverride ??
|
|
1701
|
+
(turn ? deps.extractAssistant(event.messages) : {}),
|
|
1702
|
+
foldQueuedPromptsIntoHistory: deps.getFoldQueuedPromptsIntoHistory(),
|
|
1703
|
+
resetRuntimeState: deps.resetRuntimeState,
|
|
1704
|
+
isSessionActive: () => deps.isSessionActive?.(ctx) ?? true,
|
|
1705
|
+
isTurnTransportActive: deps.isTurnTransportActive,
|
|
1706
|
+
waitForTypingIdle: deps.waitForTypingIdle,
|
|
1707
|
+
waitForActivityIdle: deps.waitForActivityIdle,
|
|
1708
|
+
updateStatus: () => deps.updateStatus(ctx),
|
|
1709
|
+
dispatchNextQueuedTelegramTurn: () => {
|
|
1710
|
+
deps.requestDeferredDispatchNextQueuedTelegramTurn(
|
|
1711
|
+
deps.dispatchNextQueuedTelegramTurn,
|
|
1712
|
+
);
|
|
1713
|
+
},
|
|
1714
|
+
scheduleActiveTurnDelivery: deps.scheduleActiveTurnDelivery
|
|
1715
|
+
? (task) =>
|
|
1716
|
+
deps.scheduleActiveTurnDelivery?.(async () => {
|
|
1717
|
+
if (deps.isSessionActive?.(ctx) === false) return;
|
|
1718
|
+
await task();
|
|
1719
|
+
})
|
|
1720
|
+
: undefined,
|
|
1721
|
+
clearPreview: deps.clearPreview,
|
|
1722
|
+
setPreviewPendingText: deps.setPreviewPendingText,
|
|
1723
|
+
finalizeMarkdownPreview: deps.finalizeMarkdownPreview,
|
|
1724
|
+
sendMarkdownReply: deps.sendMarkdownReply,
|
|
1725
|
+
sendTextReply: deps.sendTextReply,
|
|
1726
|
+
sendQueuedAttachments: deps.sendQueuedAttachments,
|
|
1727
|
+
sendRichAttachmentReply: deps.sendRichAttachmentReply,
|
|
1728
|
+
answerGuestQuery: deps.answerGuestQuery,
|
|
1729
|
+
sendGuestReply: deps.sendGuestReply,
|
|
1730
|
+
sendGuestAttachment: deps.sendGuestAttachment,
|
|
1731
|
+
sendGuestVoiceReply: deps.sendGuestVoiceReply,
|
|
1732
|
+
planOutboundReply: deps.planOutboundReply,
|
|
1733
|
+
sendOutboundReplyArtifacts: deps.sendOutboundReplyArtifacts,
|
|
1734
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1735
|
+
});
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
export async function handleTelegramAgentEndRuntime<
|
|
1740
|
+
TTurn extends PendingTelegramTurn,
|
|
1741
|
+
TReplyMarkup = unknown,
|
|
1742
|
+
>(deps: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>): Promise<void> {
|
|
1743
|
+
const { turn, assistant } = deps;
|
|
1744
|
+
const rawFinalText = assistant.text;
|
|
1745
|
+
let outboundReply = rawFinalText
|
|
1746
|
+
? deps.planOutboundReply?.(rawFinalText)
|
|
1747
|
+
: undefined;
|
|
1748
|
+
// Preserve the planned reply so voice-fallback can use stripped markdown + replyMarkup
|
|
1749
|
+
const plannedReply = outboundReply;
|
|
1750
|
+
|
|
1751
|
+
// Transparent voice interception: when the turn is voice-tagged and the agent
|
|
1752
|
+
// did not explicitly use <!-- telegram_voice --> markup, we automatically
|
|
1753
|
+
// convert the whole response to voice.
|
|
1754
|
+
const voiceInterceptionGuard =
|
|
1755
|
+
turn &&
|
|
1756
|
+
isVoiceTurn(turn) &&
|
|
1757
|
+
rawFinalText?.trim() &&
|
|
1758
|
+
deps.planOutboundReply &&
|
|
1759
|
+
(!outboundReply ||
|
|
1760
|
+
(!outboundReply.voiceText && !outboundReply.voiceReplies?.length));
|
|
1761
|
+
if (voiceInterceptionGuard) {
|
|
1762
|
+
const voiceText =
|
|
1763
|
+
plannedReply !== undefined
|
|
1764
|
+
? plannedReply.markdown?.trim() || ""
|
|
1765
|
+
: (rawFinalText ?? "");
|
|
1766
|
+
outboundReply = outboundReply
|
|
1767
|
+
? { ...outboundReply, voiceText, markdown: "" }
|
|
1768
|
+
: { markdown: "", voiceText };
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
const finalText = outboundReply ? outboundReply.markdown : rawFinalText;
|
|
1772
|
+
const hasOutboundArtifacts =
|
|
1773
|
+
!!outboundReply?.voiceText || !!outboundReply?.voiceReplies?.length;
|
|
1774
|
+
const replyMarkup = outboundReply?.replyMarkup;
|
|
1775
|
+
const isDeliveryActive = (): boolean =>
|
|
1776
|
+
deps.isSessionActive?.() !== false &&
|
|
1777
|
+
(!turn || deps.isTurnTransportActive?.(turn) !== false);
|
|
1778
|
+
const updateStatusIgnoringStaleContext = (): void => {
|
|
1779
|
+
try {
|
|
1780
|
+
deps.updateStatus();
|
|
1781
|
+
} catch (error) {
|
|
1782
|
+
if (!isTelegramStaleContextError(error)) throw error;
|
|
1783
|
+
}
|
|
1784
|
+
};
|
|
1785
|
+
if (!isDeliveryActive()) {
|
|
1786
|
+
deps.resetRuntimeState();
|
|
1787
|
+
updateStatusIgnoringStaleContext();
|
|
1788
|
+
deps.dispatchNextQueuedTelegramTurn();
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
deps.resetRuntimeState();
|
|
1792
|
+
await deps.waitForTypingIdle?.();
|
|
1793
|
+
if (!isDeliveryActive()) {
|
|
1794
|
+
updateStatusIgnoringStaleContext();
|
|
1795
|
+
deps.dispatchNextQueuedTelegramTurn();
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
updateStatusIgnoringStaleContext();
|
|
1799
|
+
const endPlan = buildTelegramAgentEndPlan({
|
|
1800
|
+
hasTurn: !!turn,
|
|
1801
|
+
stopReason: assistant.stopReason,
|
|
1802
|
+
hasFinalText: !!finalText || hasOutboundArtifacts,
|
|
1803
|
+
hasQueuedAttachments: (turn?.queuedAttachments.length ?? 0) > 0,
|
|
1804
|
+
foldQueuedPromptsIntoHistory: deps.foldQueuedPromptsIntoHistory,
|
|
1805
|
+
});
|
|
1806
|
+
if (!turn) {
|
|
1807
|
+
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
if (turn.guestQueryId) {
|
|
1811
|
+
if (assistant.errorMessage) {
|
|
1812
|
+
await deps.answerGuestQuery?.(
|
|
1813
|
+
turn.guestQueryId,
|
|
1814
|
+
"Telegram bridge: Pi failed while processing the request.",
|
|
1815
|
+
);
|
|
1816
|
+
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
const [guestAttachment] = turn.queuedAttachments;
|
|
1820
|
+
if (guestAttachment && deps.sendGuestAttachment) {
|
|
1821
|
+
try {
|
|
1822
|
+
await deps.sendGuestAttachment(
|
|
1823
|
+
turn,
|
|
1824
|
+
guestAttachment,
|
|
1825
|
+
finalText || undefined,
|
|
1826
|
+
);
|
|
1827
|
+
} catch (error) {
|
|
1828
|
+
deps.recordRuntimeEvent?.("delivery", error, {
|
|
1829
|
+
phase: "guest-attachment",
|
|
1830
|
+
guestQueryId: turn.guestQueryId,
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
} else if (
|
|
1834
|
+
outboundReply &&
|
|
1835
|
+
(outboundReply.voiceText || outboundReply.voiceReplies?.length) &&
|
|
1836
|
+
deps.sendGuestVoiceReply
|
|
1837
|
+
) {
|
|
1838
|
+
try {
|
|
1839
|
+
await deps.sendGuestVoiceReply(
|
|
1840
|
+
turn,
|
|
1841
|
+
outboundReply,
|
|
1842
|
+
finalText || undefined,
|
|
1843
|
+
);
|
|
1844
|
+
} catch (error) {
|
|
1845
|
+
deps.recordRuntimeEvent?.("delivery", error, {
|
|
1846
|
+
phase: "guest-voice",
|
|
1847
|
+
guestQueryId: turn.guestQueryId,
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
} else if (finalText) {
|
|
1851
|
+
if (deps.sendGuestReply) {
|
|
1852
|
+
await deps.sendGuestReply(turn.guestQueryId, finalText);
|
|
1853
|
+
} else {
|
|
1854
|
+
await deps.answerGuestQuery?.(turn.guestQueryId, finalText);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
if (!isDeliveryActive()) return;
|
|
1858
|
+
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
if (endPlan.shouldClearPreview) {
|
|
1862
|
+
await deps.clearPreview(turn.chatId, { target: turn.target });
|
|
1863
|
+
}
|
|
1864
|
+
if (!isDeliveryActive()) return;
|
|
1865
|
+
if (endPlan.shouldSendErrorMessage) {
|
|
1866
|
+
await deps.sendTextReply(
|
|
1867
|
+
turn.chatId,
|
|
1868
|
+
turn.replyToMessageId,
|
|
1869
|
+
assistant.errorMessage ||
|
|
1870
|
+
"Telegram bridge: Pi failed while processing the request.",
|
|
1871
|
+
{ target: turn.target },
|
|
1872
|
+
);
|
|
1873
|
+
if (!isDeliveryActive()) return;
|
|
1874
|
+
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
const deliverActiveTurn = async () => {
|
|
1878
|
+
await deps.waitForActivityIdle?.();
|
|
1879
|
+
if (!isDeliveryActive()) return;
|
|
1880
|
+
if (finalText) deps.setPreviewPendingText(finalText);
|
|
1881
|
+
if (!finalText && hasOutboundArtifacts)
|
|
1882
|
+
await deps.clearPreview(turn.chatId, { target: turn.target });
|
|
1883
|
+
if (!isDeliveryActive()) return;
|
|
1884
|
+
let richAttachmentDelivered = false;
|
|
1885
|
+
if (
|
|
1886
|
+
endPlan.kind === "text" &&
|
|
1887
|
+
finalText &&
|
|
1888
|
+
!hasOutboundArtifacts &&
|
|
1889
|
+
deps.sendRichAttachmentReply
|
|
1890
|
+
) {
|
|
1891
|
+
try {
|
|
1892
|
+
richAttachmentDelivered = await deps.sendRichAttachmentReply(
|
|
1893
|
+
turn,
|
|
1894
|
+
finalText,
|
|
1895
|
+
{ replyMarkup },
|
|
1896
|
+
);
|
|
1897
|
+
if (!isDeliveryActive()) return;
|
|
1898
|
+
if (richAttachmentDelivered) {
|
|
1899
|
+
await deps.clearPreview(turn.chatId, { target: turn.target });
|
|
1900
|
+
}
|
|
1901
|
+
} catch (error) {
|
|
1902
|
+
deps.recordRuntimeEvent?.("delivery", error, {
|
|
1903
|
+
phase: "rich-attachment-commit-unknown",
|
|
1904
|
+
chatId: turn.chatId,
|
|
1905
|
+
});
|
|
1906
|
+
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
if (!isDeliveryActive()) return;
|
|
1911
|
+
if (!richAttachmentDelivered && endPlan.kind === "text" && finalText) {
|
|
1912
|
+
try {
|
|
1913
|
+
const finalized = await deps.finalizeMarkdownPreview(
|
|
1914
|
+
turn.chatId,
|
|
1915
|
+
finalText,
|
|
1916
|
+
turn.replyToMessageId,
|
|
1917
|
+
{ replyMarkup, target: turn.target },
|
|
1918
|
+
);
|
|
1919
|
+
if (!isDeliveryActive()) return;
|
|
1920
|
+
if (!finalized) {
|
|
1921
|
+
await deps.clearPreview(turn.chatId, { target: turn.target });
|
|
1922
|
+
if (!isDeliveryActive()) return;
|
|
1923
|
+
await deps.sendMarkdownReply(
|
|
1924
|
+
turn.chatId,
|
|
1925
|
+
turn.replyToMessageId,
|
|
1926
|
+
finalText,
|
|
1927
|
+
{ replyMarkup, target: turn.target },
|
|
1928
|
+
);
|
|
1929
|
+
}
|
|
1930
|
+
} catch (error) {
|
|
1931
|
+
deps.recordRuntimeEvent?.("delivery", error, {
|
|
1932
|
+
phase: "final-text",
|
|
1933
|
+
chatId: turn.chatId,
|
|
1934
|
+
replyToMessageId: turn.replyToMessageId,
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
if (!isDeliveryActive()) return;
|
|
1939
|
+
if (outboundReply && deps.sendOutboundReplyArtifacts) {
|
|
1940
|
+
try {
|
|
1941
|
+
await deps.sendOutboundReplyArtifacts(turn, outboundReply, {
|
|
1942
|
+
replyToPrompt: !finalText,
|
|
1943
|
+
});
|
|
1944
|
+
if (!isDeliveryActive()) return;
|
|
1945
|
+
} catch (error) {
|
|
1946
|
+
deps.recordRuntimeEvent?.("delivery", error, {
|
|
1947
|
+
phase: "voice-artifacts",
|
|
1948
|
+
chatId: turn.chatId,
|
|
1949
|
+
});
|
|
1950
|
+
// Fallback to planned text when voice delivery fails and text wasn't already delivered
|
|
1951
|
+
if (!isDeliveryActive()) return;
|
|
1952
|
+
if (rawFinalText?.trim() && !finalText && hasOutboundArtifacts) {
|
|
1953
|
+
try {
|
|
1954
|
+
const fallbackMarkdown =
|
|
1955
|
+
plannedReply?.markdown ||
|
|
1956
|
+
outboundReply?.voiceText ||
|
|
1957
|
+
rawFinalText;
|
|
1958
|
+
await deps.sendMarkdownReply(
|
|
1959
|
+
turn.chatId,
|
|
1960
|
+
turn.replyToMessageId,
|
|
1961
|
+
fallbackMarkdown,
|
|
1962
|
+
plannedReply?.replyMarkup || turn.target
|
|
1963
|
+
? {
|
|
1964
|
+
replyMarkup: plannedReply?.replyMarkup,
|
|
1965
|
+
target: turn.target,
|
|
1966
|
+
}
|
|
1967
|
+
: undefined,
|
|
1968
|
+
);
|
|
1969
|
+
} catch (fallbackError) {
|
|
1970
|
+
deps.recordRuntimeEvent?.("delivery", fallbackError, {
|
|
1971
|
+
phase: "voice-fallback-text",
|
|
1972
|
+
chatId: turn.chatId,
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
if (!isDeliveryActive()) return;
|
|
1979
|
+
if (!richAttachmentDelivered && endPlan.shouldSendAttachmentNotice) {
|
|
1980
|
+
await deps.sendTextReply(
|
|
1981
|
+
turn.chatId,
|
|
1982
|
+
turn.replyToMessageId,
|
|
1983
|
+
"Attached requested file(s).",
|
|
1984
|
+
{ target: turn.target },
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
if (!isDeliveryActive()) return;
|
|
1988
|
+
if (!richAttachmentDelivered) await deps.sendQueuedAttachments(turn);
|
|
1989
|
+
if (!isDeliveryActive()) return;
|
|
1990
|
+
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
1991
|
+
};
|
|
1992
|
+
if (
|
|
1993
|
+
deps.scheduleActiveTurnDelivery &&
|
|
1994
|
+
(endPlan.kind === "text" || endPlan.kind === "attachments-only")
|
|
1995
|
+
) {
|
|
1996
|
+
deps.scheduleActiveTurnDelivery(deliverActiveTurn);
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
await deliverActiveTurn();
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
// --- Session Runtime ---
|
|
2003
|
+
|
|
2004
|
+
export interface TelegramSessionStartState<TModel = unknown> {
|
|
2005
|
+
currentTelegramModel: TModel | undefined;
|
|
2006
|
+
activeTelegramToolExecutions: number;
|
|
2007
|
+
pendingTelegramModelSwitch: undefined;
|
|
2008
|
+
nextQueuedTelegramItemOrder: number;
|
|
2009
|
+
nextQueuedTelegramControlOrder: number;
|
|
2010
|
+
telegramTurnDispatchPending: boolean;
|
|
2011
|
+
compactionInProgress: boolean;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
export interface TelegramSessionShutdownState<TQueueItem> {
|
|
2015
|
+
queuedTelegramItems: TQueueItem[];
|
|
2016
|
+
nextQueuedTelegramItemOrder: number;
|
|
2017
|
+
nextQueuedTelegramControlOrder: number;
|
|
2018
|
+
currentTelegramModel: undefined;
|
|
2019
|
+
activeTelegramToolExecutions: number;
|
|
2020
|
+
pendingTelegramModelSwitch: undefined;
|
|
2021
|
+
telegramTurnDispatchPending: boolean;
|
|
2022
|
+
compactionInProgress: boolean;
|
|
2023
|
+
foldQueuedPromptsIntoHistory: boolean;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
export interface TelegramSessionRuntimeCounterState {
|
|
2027
|
+
nextQueuedTelegramItemOrder?: number;
|
|
2028
|
+
nextQueuedTelegramControlOrder?: number;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
export interface TelegramSessionRuntimeFlagState {
|
|
2032
|
+
activeTelegramToolExecutions?: number;
|
|
2033
|
+
telegramTurnDispatchPending?: boolean;
|
|
2034
|
+
compactionInProgress?: boolean;
|
|
2035
|
+
foldQueuedPromptsIntoHistory?: boolean;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
export interface TelegramSessionStateApplierDeps<TQueueItem, TModel> {
|
|
2039
|
+
setQueuedItems: (items: TQueueItem[]) => void;
|
|
2040
|
+
setCurrentModel: (model: TModel | undefined) => void;
|
|
2041
|
+
setPendingModelSwitch: (selection: undefined) => void;
|
|
2042
|
+
syncCounters: (state: TelegramSessionRuntimeCounterState) => void;
|
|
2043
|
+
syncFlags: (state: TelegramSessionRuntimeFlagState) => void;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
export interface TelegramSessionStateApplier<TQueueItem, TModel> {
|
|
2047
|
+
applyStartState: (state: TelegramSessionStartState<TModel>) => void;
|
|
2048
|
+
applyShutdownState: (state: TelegramSessionShutdownState<TQueueItem>) => void;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
export interface TelegramSessionStartRuntimeDeps<TContext, TModel = unknown> {
|
|
2052
|
+
ctx: TContext;
|
|
2053
|
+
currentModel: TModel | undefined;
|
|
2054
|
+
loadConfig: () => Promise<void>;
|
|
2055
|
+
isSessionActive?: () => boolean;
|
|
2056
|
+
applyState: (state: TelegramSessionStartState<TModel>) => void;
|
|
2057
|
+
bindDeferredDispatchContext?: (ctx: TContext) => void;
|
|
2058
|
+
prepareTempDir: () => Promise<unknown>;
|
|
2059
|
+
updateStatus: () => void;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
export interface TelegramSessionShutdownRuntimeDeps<TQueueItem> {
|
|
2063
|
+
isSessionActive?: () => boolean;
|
|
2064
|
+
unbindDeferredDispatchContext?: () => void;
|
|
2065
|
+
applyState: (state: TelegramSessionShutdownState<TQueueItem>) => void;
|
|
2066
|
+
clearPendingMediaGroups: () => void;
|
|
2067
|
+
clearModelMenuState: () => void;
|
|
2068
|
+
getActiveTurnChatId: () => number | undefined;
|
|
2069
|
+
getActiveTurnTarget?: () => TelegramQueueTarget | undefined;
|
|
2070
|
+
clearPreview: (
|
|
2071
|
+
chatId: number,
|
|
2072
|
+
options?: { target?: TelegramQueueTarget },
|
|
2073
|
+
) => Promise<void>;
|
|
2074
|
+
previewShutdownTimeoutMs?: number;
|
|
2075
|
+
clearActiveTurn: () => void;
|
|
2076
|
+
clearAbort: () => void;
|
|
2077
|
+
stopPolling: () => Promise<void>;
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
export interface TelegramSessionLifecycleHookRuntimeDeps<
|
|
2081
|
+
TContext,
|
|
2082
|
+
TQueueItem,
|
|
2083
|
+
TModel = unknown,
|
|
2084
|
+
> extends TelegramRuntimeEventRecorderPort {
|
|
2085
|
+
getCurrentModel: (ctx: TContext) => TModel | undefined;
|
|
2086
|
+
loadConfig: () => Promise<void>;
|
|
2087
|
+
applySessionStartState: (state: TelegramSessionStartState<TModel>) => void;
|
|
2088
|
+
bindDeferredDispatchContext?: (ctx: TContext) => void;
|
|
2089
|
+
prepareTempDir: () => Promise<unknown>;
|
|
2090
|
+
updateStatus: (ctx: TContext) => void;
|
|
2091
|
+
isSessionActive?: (ctx: TContext) => boolean;
|
|
2092
|
+
unbindDeferredDispatchContext?: () => void;
|
|
2093
|
+
applySessionShutdownState: (
|
|
2094
|
+
state: TelegramSessionShutdownState<TQueueItem>,
|
|
2095
|
+
) => void;
|
|
2096
|
+
clearPendingMediaGroups: () => void;
|
|
2097
|
+
clearModelMenuState: () => void;
|
|
2098
|
+
getActiveTurnChatId: () => number | undefined;
|
|
2099
|
+
getActiveTurnTarget?: () => TelegramQueueTarget | undefined;
|
|
2100
|
+
clearPreview: (
|
|
2101
|
+
chatId: number,
|
|
2102
|
+
options?: { target?: TelegramQueueTarget },
|
|
2103
|
+
) => Promise<void>;
|
|
2104
|
+
previewShutdownTimeoutMs?: number;
|
|
2105
|
+
clearActiveTurn: () => void;
|
|
2106
|
+
clearAbort: () => void;
|
|
2107
|
+
stopPolling: () => Promise<void>;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
export type TelegramSessionLifecycleHookEvent = unknown;
|
|
2111
|
+
|
|
2112
|
+
export function createTelegramSessionStateApplier<TQueueItem, TModel>(
|
|
2113
|
+
deps: TelegramSessionStateApplierDeps<TQueueItem, TModel>,
|
|
2114
|
+
): TelegramSessionStateApplier<TQueueItem, TModel> {
|
|
2115
|
+
return {
|
|
2116
|
+
applyStartState: (state) => {
|
|
2117
|
+
deps.setCurrentModel(state.currentTelegramModel);
|
|
2118
|
+
deps.setPendingModelSwitch(state.pendingTelegramModelSwitch);
|
|
2119
|
+
deps.syncCounters(state);
|
|
2120
|
+
deps.syncFlags(state);
|
|
2121
|
+
},
|
|
2122
|
+
applyShutdownState: (state) => {
|
|
2123
|
+
deps.setQueuedItems(state.queuedTelegramItems);
|
|
2124
|
+
deps.syncCounters(state);
|
|
2125
|
+
deps.syncFlags(state);
|
|
2126
|
+
deps.setCurrentModel(state.currentTelegramModel);
|
|
2127
|
+
deps.setPendingModelSwitch(state.pendingTelegramModelSwitch);
|
|
2128
|
+
},
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
export interface TelegramQueueMutationRuntimeDeps<
|
|
2133
|
+
TContext,
|
|
2134
|
+
> extends TelegramQueueStore<TContext>, TelegramRuntimeEventRecorderPort {
|
|
2135
|
+
ctx: TContext;
|
|
2136
|
+
allocateLaneOrder?: () => number;
|
|
2137
|
+
onItemsDiscarded?: (
|
|
2138
|
+
items: readonly TelegramQueueItem<TContext>[],
|
|
2139
|
+
ctx: TContext,
|
|
2140
|
+
) => void;
|
|
2141
|
+
updateStatus: (ctx: TContext) => void;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
export interface TelegramQueueMutationControllerDeps<
|
|
2145
|
+
TContext,
|
|
2146
|
+
> extends TelegramQueueStore<TContext>, TelegramRuntimeEventRecorderPort {
|
|
2147
|
+
allocateLaneOrder?: () => number;
|
|
2148
|
+
onItemsDiscarded?: (
|
|
2149
|
+
items: readonly TelegramQueueItem<TContext>[],
|
|
2150
|
+
ctx: TContext,
|
|
2151
|
+
) => void;
|
|
2152
|
+
updateStatus: (ctx: TContext) => void;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
export interface TelegramQueueMutationController<TContext> {
|
|
2156
|
+
append: (item: TelegramQueueItem<TContext>, ctx: TContext) => void;
|
|
2157
|
+
reorder: (ctx: TContext) => void;
|
|
2158
|
+
clear: (ctx: TContext) => number;
|
|
2159
|
+
removeByMessageIds: (
|
|
2160
|
+
messageIds: number[],
|
|
2161
|
+
ctx: TContext,
|
|
2162
|
+
scope?: TelegramQueueMessageScope,
|
|
2163
|
+
) => number;
|
|
2164
|
+
applyReactionByMessageId: (
|
|
2165
|
+
messageId: number,
|
|
2166
|
+
disposition: TelegramQueueReactionDisposition,
|
|
2167
|
+
ctx: TContext,
|
|
2168
|
+
scope?: TelegramQueueMessageScope,
|
|
2169
|
+
) => boolean;
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
export interface TelegramControlQueueControllerDeps<TContext> {
|
|
2173
|
+
appendControlItem: (
|
|
2174
|
+
item: PendingTelegramControlItem<TContext>,
|
|
2175
|
+
ctx: TContext,
|
|
2176
|
+
) => void;
|
|
2177
|
+
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
export interface TelegramControlQueueController<TContext> {
|
|
2181
|
+
enqueue: (
|
|
2182
|
+
item: PendingTelegramControlItem<TContext>,
|
|
2183
|
+
ctx: TContext,
|
|
2184
|
+
onQueued?: (item: PendingTelegramControlItem<TContext>) => void,
|
|
2185
|
+
) => void;
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
export interface TelegramPromptEnqueueRuntimeDeps<
|
|
2189
|
+
TMessage,
|
|
2190
|
+
TContext = unknown,
|
|
2191
|
+
> extends TelegramQueueStore<TContext> {
|
|
2192
|
+
getFoldQueuedPromptsIntoHistory: () => boolean;
|
|
2193
|
+
setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
|
|
2194
|
+
createTurn: (
|
|
2195
|
+
messages: TMessage[],
|
|
2196
|
+
historyTurns: PendingTelegramTurn[],
|
|
2197
|
+
) => Promise<PendingTelegramTurn>;
|
|
2198
|
+
updateStatus: () => void;
|
|
2199
|
+
dispatchNextQueuedTelegramTurn: () => void;
|
|
2200
|
+
assertExecutionCurrent?: () => void;
|
|
2201
|
+
onQueued?: (turn: PendingTelegramTurn) => void;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
export interface TelegramPromptEnqueueControllerDeps<
|
|
2205
|
+
TMessage,
|
|
2206
|
+
TContext = unknown,
|
|
2207
|
+
> extends TelegramQueueStore<TContext> {
|
|
2208
|
+
getFoldQueuedPromptsIntoHistory: () => boolean;
|
|
2209
|
+
setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
|
|
2210
|
+
createTurn: (
|
|
2211
|
+
messages: TMessage[],
|
|
2212
|
+
historyTurns: PendingTelegramTurn[],
|
|
2213
|
+
ctx: TContext,
|
|
2214
|
+
) => Promise<PendingTelegramTurn>;
|
|
2215
|
+
updateStatus: (ctx: TContext) => void;
|
|
2216
|
+
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
2217
|
+
assertExecutionCurrent?: (messages: TMessage[]) => void;
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
export interface TelegramPromptEnqueueController<TMessage, TContext = unknown> {
|
|
2221
|
+
enqueue: (
|
|
2222
|
+
messages: TMessage[],
|
|
2223
|
+
ctx: TContext,
|
|
2224
|
+
onQueued?: (turn: PendingTelegramTurn) => void,
|
|
2225
|
+
) => Promise<PendingTelegramTurn>;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
function isTelegramStaleContextError(error: unknown): boolean {
|
|
2229
|
+
return (
|
|
2230
|
+
error instanceof Error &&
|
|
2231
|
+
(error.message.includes("stale after session") ||
|
|
2232
|
+
error.message.includes("stale ctx"))
|
|
2233
|
+
);
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
export function buildTelegramSessionStartState<TModel = unknown>(
|
|
2237
|
+
currentModel: TModel | undefined,
|
|
2238
|
+
): TelegramSessionStartState<TModel> {
|
|
2239
|
+
return {
|
|
2240
|
+
currentTelegramModel: currentModel,
|
|
2241
|
+
activeTelegramToolExecutions: 0,
|
|
2242
|
+
pendingTelegramModelSwitch: undefined,
|
|
2243
|
+
nextQueuedTelegramItemOrder: 0,
|
|
2244
|
+
nextQueuedTelegramControlOrder: 0,
|
|
2245
|
+
telegramTurnDispatchPending: false,
|
|
2246
|
+
compactionInProgress: false,
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
export function buildTelegramSessionShutdownState<
|
|
2251
|
+
TQueueItem,
|
|
2252
|
+
>(): TelegramSessionShutdownState<TQueueItem> {
|
|
2253
|
+
return {
|
|
2254
|
+
queuedTelegramItems: [],
|
|
2255
|
+
nextQueuedTelegramItemOrder: 0,
|
|
2256
|
+
nextQueuedTelegramControlOrder: 0,
|
|
2257
|
+
currentTelegramModel: undefined,
|
|
2258
|
+
activeTelegramToolExecutions: 0,
|
|
2259
|
+
pendingTelegramModelSwitch: undefined,
|
|
2260
|
+
telegramTurnDispatchPending: false,
|
|
2261
|
+
compactionInProgress: false,
|
|
2262
|
+
foldQueuedPromptsIntoHistory: false,
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
export async function startTelegramSessionRuntime<TContext, TModel = unknown>(
|
|
2267
|
+
deps: TelegramSessionStartRuntimeDeps<TContext, TModel>,
|
|
2268
|
+
): Promise<void> {
|
|
2269
|
+
await deps.loadConfig();
|
|
2270
|
+
if (deps.isSessionActive?.() === false) return;
|
|
2271
|
+
deps.applyState(buildTelegramSessionStartState(deps.currentModel));
|
|
2272
|
+
await deps.prepareTempDir();
|
|
2273
|
+
if (deps.isSessionActive?.() === false) return;
|
|
2274
|
+
try {
|
|
2275
|
+
deps.bindDeferredDispatchContext?.(deps.ctx);
|
|
2276
|
+
} catch (error) {
|
|
2277
|
+
if (!isTelegramStaleContextError(error)) throw error;
|
|
2278
|
+
}
|
|
2279
|
+
deps.updateStatus();
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
export async function shutdownTelegramSessionRuntime<TQueueItem>(
|
|
2283
|
+
deps: TelegramSessionShutdownRuntimeDeps<TQueueItem>,
|
|
2284
|
+
): Promise<void> {
|
|
2285
|
+
if (deps.isSessionActive?.() === false) return;
|
|
2286
|
+
deps.unbindDeferredDispatchContext?.();
|
|
2287
|
+
await deps.stopPolling();
|
|
2288
|
+
if (deps.isSessionActive?.() === false) return;
|
|
2289
|
+
deps.applyState(buildTelegramSessionShutdownState<TQueueItem>());
|
|
2290
|
+
deps.clearPendingMediaGroups();
|
|
2291
|
+
deps.clearModelMenuState();
|
|
2292
|
+
const activeTurnChatId = deps.getActiveTurnChatId();
|
|
2293
|
+
if (activeTurnChatId !== undefined) {
|
|
2294
|
+
const target = deps.getActiveTurnTarget?.();
|
|
2295
|
+
const previewTimeoutMs = deps.previewShutdownTimeoutMs ?? 1000;
|
|
2296
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
2297
|
+
await Promise.race([
|
|
2298
|
+
deps.clearPreview(activeTurnChatId, target ? { target } : undefined),
|
|
2299
|
+
new Promise<void>((resolve) => {
|
|
2300
|
+
timeout = setTimeout(resolve, previewTimeoutMs);
|
|
2301
|
+
}),
|
|
2302
|
+
]).finally(() => {
|
|
2303
|
+
if (timeout) clearTimeout(timeout);
|
|
2304
|
+
});
|
|
2305
|
+
if (deps.isSessionActive?.() === false) return;
|
|
2306
|
+
}
|
|
2307
|
+
deps.clearActiveTurn();
|
|
2308
|
+
deps.clearAbort();
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
export type TelegramSessionLifecycleRuntimeDeps<
|
|
2312
|
+
TContext,
|
|
2313
|
+
TQueueItem,
|
|
2314
|
+
TModel = unknown,
|
|
2315
|
+
> = Omit<
|
|
2316
|
+
TelegramSessionLifecycleHookRuntimeDeps<TContext, TQueueItem, TModel>,
|
|
2317
|
+
"applySessionStartState" | "applySessionShutdownState"
|
|
2318
|
+
> &
|
|
2319
|
+
TelegramSessionStateApplierDeps<TQueueItem, TModel>;
|
|
2320
|
+
|
|
2321
|
+
export function createTelegramSessionLifecycleRuntime<
|
|
2322
|
+
TContext,
|
|
2323
|
+
TQueueItem,
|
|
2324
|
+
TModel = unknown,
|
|
2325
|
+
>(deps: TelegramSessionLifecycleRuntimeDeps<TContext, TQueueItem, TModel>) {
|
|
2326
|
+
const stateApplier = createTelegramSessionStateApplier({
|
|
2327
|
+
setQueuedItems: deps.setQueuedItems,
|
|
2328
|
+
setCurrentModel: deps.setCurrentModel,
|
|
2329
|
+
setPendingModelSwitch: deps.setPendingModelSwitch,
|
|
2330
|
+
syncCounters: deps.syncCounters,
|
|
2331
|
+
syncFlags: deps.syncFlags,
|
|
2332
|
+
});
|
|
2333
|
+
return createTelegramSessionLifecycleHooks({
|
|
2334
|
+
getCurrentModel: deps.getCurrentModel,
|
|
2335
|
+
loadConfig: deps.loadConfig,
|
|
2336
|
+
applySessionStartState: stateApplier.applyStartState,
|
|
2337
|
+
bindDeferredDispatchContext: deps.bindDeferredDispatchContext,
|
|
2338
|
+
prepareTempDir: deps.prepareTempDir,
|
|
2339
|
+
updateStatus: deps.updateStatus,
|
|
2340
|
+
isSessionActive: deps.isSessionActive,
|
|
2341
|
+
unbindDeferredDispatchContext: deps.unbindDeferredDispatchContext,
|
|
2342
|
+
applySessionShutdownState: stateApplier.applyShutdownState,
|
|
2343
|
+
clearPendingMediaGroups: deps.clearPendingMediaGroups,
|
|
2344
|
+
clearModelMenuState: deps.clearModelMenuState,
|
|
2345
|
+
getActiveTurnChatId: deps.getActiveTurnChatId,
|
|
2346
|
+
getActiveTurnTarget: deps.getActiveTurnTarget,
|
|
2347
|
+
clearPreview: deps.clearPreview,
|
|
2348
|
+
clearActiveTurn: deps.clearActiveTurn,
|
|
2349
|
+
clearAbort: deps.clearAbort,
|
|
2350
|
+
stopPolling: deps.stopPolling,
|
|
2351
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
export function createTelegramSessionLifecycleHooks<
|
|
2356
|
+
TContext,
|
|
2357
|
+
TQueueItem,
|
|
2358
|
+
TModel = unknown,
|
|
2359
|
+
>(deps: TelegramSessionLifecycleHookRuntimeDeps<TContext, TQueueItem, TModel>) {
|
|
2360
|
+
return {
|
|
2361
|
+
onSessionStart: async (
|
|
2362
|
+
_event: TelegramSessionLifecycleHookEvent,
|
|
2363
|
+
ctx: TContext,
|
|
2364
|
+
): Promise<void> => {
|
|
2365
|
+
try {
|
|
2366
|
+
await startTelegramSessionRuntime({
|
|
2367
|
+
ctx,
|
|
2368
|
+
currentModel: deps.getCurrentModel(ctx),
|
|
2369
|
+
loadConfig: deps.loadConfig,
|
|
2370
|
+
isSessionActive: () => deps.isSessionActive?.(ctx) ?? true,
|
|
2371
|
+
applyState: deps.applySessionStartState,
|
|
2372
|
+
bindDeferredDispatchContext: deps.bindDeferredDispatchContext,
|
|
2373
|
+
prepareTempDir: deps.prepareTempDir,
|
|
2374
|
+
updateStatus: () => deps.updateStatus(ctx),
|
|
2375
|
+
});
|
|
2376
|
+
} catch (error) {
|
|
2377
|
+
deps.recordRuntimeEvent?.("session", error, { phase: "start" });
|
|
2378
|
+
throw error;
|
|
2379
|
+
}
|
|
2380
|
+
},
|
|
2381
|
+
onSessionShutdown: async (
|
|
2382
|
+
_event?: TelegramSessionLifecycleHookEvent,
|
|
2383
|
+
ctx?: TContext,
|
|
2384
|
+
): Promise<void> => {
|
|
2385
|
+
try {
|
|
2386
|
+
await shutdownTelegramSessionRuntime<TQueueItem>({
|
|
2387
|
+
isSessionActive: () =>
|
|
2388
|
+
ctx === undefined ? true : (deps.isSessionActive?.(ctx) ?? true),
|
|
2389
|
+
unbindDeferredDispatchContext: deps.unbindDeferredDispatchContext,
|
|
2390
|
+
applyState: deps.applySessionShutdownState,
|
|
2391
|
+
clearPendingMediaGroups: deps.clearPendingMediaGroups,
|
|
2392
|
+
clearModelMenuState: deps.clearModelMenuState,
|
|
2393
|
+
getActiveTurnChatId: deps.getActiveTurnChatId,
|
|
2394
|
+
getActiveTurnTarget: deps.getActiveTurnTarget,
|
|
2395
|
+
clearPreview: deps.clearPreview,
|
|
2396
|
+
previewShutdownTimeoutMs: deps.previewShutdownTimeoutMs,
|
|
2397
|
+
clearActiveTurn: deps.clearActiveTurn,
|
|
2398
|
+
clearAbort: deps.clearAbort,
|
|
2399
|
+
stopPolling: deps.stopPolling,
|
|
2400
|
+
});
|
|
2401
|
+
} catch (error) {
|
|
2402
|
+
deps.recordRuntimeEvent?.("session", error, { phase: "shutdown" });
|
|
2403
|
+
throw error;
|
|
2404
|
+
}
|
|
2405
|
+
},
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
export function createTelegramQueueMutationController<TContext>(
|
|
2410
|
+
deps: TelegramQueueMutationControllerDeps<TContext>,
|
|
2411
|
+
): TelegramQueueMutationController<TContext> {
|
|
2412
|
+
const buildRuntimeDeps = (
|
|
2413
|
+
ctx: TContext,
|
|
2414
|
+
): TelegramQueueMutationRuntimeDeps<TContext> => ({
|
|
2415
|
+
...deps,
|
|
2416
|
+
ctx,
|
|
2417
|
+
});
|
|
2418
|
+
return {
|
|
2419
|
+
append: (item, ctx) =>
|
|
2420
|
+
appendTelegramQueueItemRuntime(item, buildRuntimeDeps(ctx)),
|
|
2421
|
+
reorder: (ctx) => reorderTelegramQueueItemsRuntime(buildRuntimeDeps(ctx)),
|
|
2422
|
+
clear: (ctx) => clearTelegramQueueItemsRuntime(buildRuntimeDeps(ctx)),
|
|
2423
|
+
removeByMessageIds: (messageIds, ctx, scope) =>
|
|
2424
|
+
removeTelegramQueueItemsByMessageIdsRuntime(
|
|
2425
|
+
messageIds,
|
|
2426
|
+
buildRuntimeDeps(ctx),
|
|
2427
|
+
scope,
|
|
2428
|
+
),
|
|
2429
|
+
applyReactionByMessageId: (messageId, disposition, ctx, scope) =>
|
|
2430
|
+
applyTelegramQueuePromptReactionDispositionRuntime(
|
|
2431
|
+
messageId,
|
|
2432
|
+
disposition,
|
|
2433
|
+
buildRuntimeDeps(ctx),
|
|
2434
|
+
scope,
|
|
2435
|
+
),
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
function updateTelegramQueueStatusRuntime<TContext>(
|
|
2440
|
+
deps: TelegramQueueMutationRuntimeDeps<TContext>,
|
|
2441
|
+
): void {
|
|
2442
|
+
try {
|
|
2443
|
+
deps.updateStatus(deps.ctx);
|
|
2444
|
+
} catch (error) {
|
|
2445
|
+
if (!isTelegramStaleContextError(error)) throw error;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
function commitReorderedTelegramQueueItemsRuntime<TContext>(
|
|
2450
|
+
items: TelegramQueueItem<TContext>[],
|
|
2451
|
+
deps: TelegramQueueMutationRuntimeDeps<TContext>,
|
|
2452
|
+
): void {
|
|
2453
|
+
deps.setQueuedItems([...items].sort(compareTelegramQueueItems));
|
|
2454
|
+
updateTelegramQueueStatusRuntime(deps);
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
function appendTelegramQueueItemRuntime<TContext>(
|
|
2458
|
+
item: TelegramQueueItem<TContext>,
|
|
2459
|
+
deps: TelegramQueueMutationRuntimeDeps<TContext>,
|
|
2460
|
+
): void {
|
|
2461
|
+
const currentItems = deps.getQueuedItems();
|
|
2462
|
+
const nextItems = appendTelegramQueueItem(currentItems, item);
|
|
2463
|
+
if (nextItems === currentItems) return;
|
|
2464
|
+
commitReorderedTelegramQueueItemsRuntime(nextItems, deps);
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
export function reorderTelegramQueueItemsRuntime<TContext>(
|
|
2468
|
+
deps: TelegramQueueMutationRuntimeDeps<TContext>,
|
|
2469
|
+
): void {
|
|
2470
|
+
commitReorderedTelegramQueueItemsRuntime(deps.getQueuedItems(), deps);
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
export function clearTelegramQueueItemsRuntime<TContext>(
|
|
2474
|
+
deps: TelegramQueueMutationRuntimeDeps<TContext>,
|
|
2475
|
+
): number {
|
|
2476
|
+
const removedItems = deps.getQueuedItems();
|
|
2477
|
+
const removedCount = removedItems.length;
|
|
2478
|
+
if (removedCount === 0) return 0;
|
|
2479
|
+
deps.setQueuedItems([]);
|
|
2480
|
+
try {
|
|
2481
|
+
deps.onItemsDiscarded?.(removedItems, deps.ctx);
|
|
2482
|
+
} catch (error) {
|
|
2483
|
+
deps.recordRuntimeEvent?.("queue", error, {
|
|
2484
|
+
phase: "discard-receipt-settlement",
|
|
2485
|
+
});
|
|
2486
|
+
}
|
|
2487
|
+
updateTelegramQueueStatusRuntime(deps);
|
|
2488
|
+
return removedCount;
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
export function removeTelegramQueueItemsByMessageIdsRuntime<TContext>(
|
|
2492
|
+
messageIds: number[],
|
|
2493
|
+
deps: TelegramQueueMutationRuntimeDeps<TContext>,
|
|
2494
|
+
scope?: TelegramQueueMessageScope,
|
|
2495
|
+
): number {
|
|
2496
|
+
const { items, removedItems, removedCount } =
|
|
2497
|
+
removeTelegramQueueItemsByMessageIds(
|
|
2498
|
+
deps.getQueuedItems(),
|
|
2499
|
+
messageIds,
|
|
2500
|
+
scope,
|
|
2501
|
+
);
|
|
2502
|
+
if (removedCount === 0) return 0;
|
|
2503
|
+
deps.setQueuedItems(items);
|
|
2504
|
+
try {
|
|
2505
|
+
deps.onItemsDiscarded?.(removedItems, deps.ctx);
|
|
2506
|
+
} catch (error) {
|
|
2507
|
+
deps.recordRuntimeEvent?.("queue", error, {
|
|
2508
|
+
phase: "discard-receipt-settlement",
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
updateTelegramQueueStatusRuntime(deps);
|
|
2512
|
+
return removedCount;
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
export function applyTelegramQueuePromptReactionDispositionRuntime<TContext>(
|
|
2516
|
+
messageId: number,
|
|
2517
|
+
disposition: TelegramQueueReactionDisposition,
|
|
2518
|
+
deps: TelegramQueueMutationRuntimeDeps<TContext>,
|
|
2519
|
+
scope?: TelegramQueueMessageScope,
|
|
2520
|
+
): boolean {
|
|
2521
|
+
const queuedItems = deps.getQueuedItems();
|
|
2522
|
+
const changesLane = queuedItems.some((item) => {
|
|
2523
|
+
if (
|
|
2524
|
+
!isPendingTelegramTurn(item) ||
|
|
2525
|
+
!isTelegramQueueItemInMessageScope(item, scope) ||
|
|
2526
|
+
!item.sourceMessageIds.includes(messageId)
|
|
2527
|
+
) {
|
|
2528
|
+
return false;
|
|
2529
|
+
}
|
|
2530
|
+
const queueLane: TelegramQueueLane =
|
|
2531
|
+
disposition.kind === "reaction-transition"
|
|
2532
|
+
? disposition.priorityEmoji === undefined
|
|
2533
|
+
? item.queueLane
|
|
2534
|
+
: disposition.priorityEmoji === null
|
|
2535
|
+
? "default"
|
|
2536
|
+
: "priority"
|
|
2537
|
+
: disposition.kind === "priority" ||
|
|
2538
|
+
disposition.kind === "priority-suppressed"
|
|
2539
|
+
? "priority"
|
|
2540
|
+
: "default";
|
|
2541
|
+
return item.queueLane !== queueLane;
|
|
2542
|
+
});
|
|
2543
|
+
const destinationLaneOrder = changesLane
|
|
2544
|
+
? deps.allocateLaneOrder?.()
|
|
2545
|
+
: undefined;
|
|
2546
|
+
if (changesLane && destinationLaneOrder === undefined) return false;
|
|
2547
|
+
const { changed, items } = applyTelegramQueuePromptReactionDisposition(
|
|
2548
|
+
queuedItems,
|
|
2549
|
+
messageId,
|
|
2550
|
+
disposition,
|
|
2551
|
+
destinationLaneOrder,
|
|
2552
|
+
scope,
|
|
2553
|
+
);
|
|
2554
|
+
if (!changed) return false;
|
|
2555
|
+
commitReorderedTelegramQueueItemsRuntime(items, deps);
|
|
2556
|
+
return true;
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
export async function enqueueTelegramPromptTurnRuntime<
|
|
2560
|
+
TMessage,
|
|
2561
|
+
TContext = unknown,
|
|
2562
|
+
>(
|
|
2563
|
+
messages: TMessage[],
|
|
2564
|
+
deps: TelegramPromptEnqueueRuntimeDeps<TMessage, TContext>,
|
|
2565
|
+
): Promise<PendingTelegramTurn> {
|
|
2566
|
+
const enqueuePlan = planTelegramPromptEnqueue(
|
|
2567
|
+
deps.getQueuedItems(),
|
|
2568
|
+
deps.getFoldQueuedPromptsIntoHistory(),
|
|
2569
|
+
);
|
|
2570
|
+
deps.assertExecutionCurrent?.();
|
|
2571
|
+
deps.setFoldQueuedPromptsIntoHistory(false);
|
|
2572
|
+
const turn = await deps.createTurn(messages, enqueuePlan.historyTurns);
|
|
2573
|
+
deps.assertExecutionCurrent?.();
|
|
2574
|
+
deps.setQueuedItems(
|
|
2575
|
+
appendTelegramQueueItem(enqueuePlan.remainingItems, turn),
|
|
2576
|
+
);
|
|
2577
|
+
deps.onQueued?.(turn);
|
|
2578
|
+
deps.updateStatus();
|
|
2579
|
+
deps.dispatchNextQueuedTelegramTurn();
|
|
2580
|
+
return turn;
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
export function createTelegramPromptEnqueueController<
|
|
2584
|
+
TMessage,
|
|
2585
|
+
TContext = unknown,
|
|
2586
|
+
>(
|
|
2587
|
+
deps: TelegramPromptEnqueueControllerDeps<TMessage, TContext>,
|
|
2588
|
+
): TelegramPromptEnqueueController<TMessage, TContext> {
|
|
2589
|
+
return {
|
|
2590
|
+
enqueue: (messages, ctx, onQueued) =>
|
|
2591
|
+
enqueueTelegramPromptTurnRuntime(messages, {
|
|
2592
|
+
...deps,
|
|
2593
|
+
createTurn: (nextMessages, historyTurns) =>
|
|
2594
|
+
deps.createTurn(nextMessages, historyTurns, ctx),
|
|
2595
|
+
updateStatus: () => deps.updateStatus(ctx),
|
|
2596
|
+
dispatchNextQueuedTelegramTurn: () =>
|
|
2597
|
+
deps.dispatchNextQueuedTelegramTurn(ctx),
|
|
2598
|
+
assertExecutionCurrent: () =>
|
|
2599
|
+
deps.assertExecutionCurrent?.(messages),
|
|
2600
|
+
onQueued,
|
|
2601
|
+
}),
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
export function createTelegramControlQueueController<TContext>(
|
|
2606
|
+
deps: TelegramControlQueueControllerDeps<TContext>,
|
|
2607
|
+
): TelegramControlQueueController<TContext> {
|
|
2608
|
+
return {
|
|
2609
|
+
enqueue: (item, ctx, onQueued) => {
|
|
2610
|
+
deps.appendControlItem(item, ctx);
|
|
2611
|
+
onQueued?.(item);
|
|
2612
|
+
deps.dispatchNextQueuedTelegramTurn(ctx);
|
|
2613
|
+
},
|
|
2614
|
+
};
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
// --- Control Runtime ---
|
|
2618
|
+
|
|
2619
|
+
function getTelegramQueueErrorMessage(error: unknown): string {
|
|
2620
|
+
return error instanceof Error ? error.message : String(error);
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
export interface TelegramRuntimeEventRecorderPort {
|
|
2624
|
+
recordRuntimeEvent?: (
|
|
2625
|
+
category: string,
|
|
2626
|
+
error: unknown,
|
|
2627
|
+
details?: Record<string, unknown>,
|
|
2628
|
+
) => void;
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
export interface TelegramControlRuntimeDeps<
|
|
2632
|
+
TContext,
|
|
2633
|
+
> extends TelegramRuntimeEventRecorderPort {
|
|
2634
|
+
ctx: TContext;
|
|
2635
|
+
sendTextReply: (
|
|
2636
|
+
chatId: number,
|
|
2637
|
+
replyToMessageId: number,
|
|
2638
|
+
text: string,
|
|
2639
|
+
options?: { target?: TelegramQueueTarget },
|
|
2640
|
+
) => Promise<number | undefined>;
|
|
2641
|
+
onSettled: (item: PendingTelegramControlItem<TContext>) => void;
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
export async function executeTelegramControlItemRuntime<TContext>(
|
|
2645
|
+
item: PendingTelegramControlItem<TContext>,
|
|
2646
|
+
deps: TelegramControlRuntimeDeps<TContext>,
|
|
2647
|
+
): Promise<void> {
|
|
2648
|
+
try {
|
|
2649
|
+
await item.execute(deps.ctx);
|
|
2650
|
+
} catch (error) {
|
|
2651
|
+
const message = getTelegramQueueErrorMessage(error);
|
|
2652
|
+
deps.recordRuntimeEvent?.("control", error, {
|
|
2653
|
+
controlType: item.controlType,
|
|
2654
|
+
chatId: item.chatId,
|
|
2655
|
+
replyToMessageId: item.replyToMessageId,
|
|
2656
|
+
});
|
|
2657
|
+
await deps.sendTextReply(
|
|
2658
|
+
item.chatId,
|
|
2659
|
+
item.replyToMessageId,
|
|
2660
|
+
`Telegram control action failed: ${message}`,
|
|
2661
|
+
{ target: item.target },
|
|
2662
|
+
);
|
|
2663
|
+
} finally {
|
|
2664
|
+
deps.onSettled(item);
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
// --- Deferred Dispatch Runtime ---
|
|
2669
|
+
|
|
2670
|
+
export interface TelegramDeferredQueueDispatchRuntimeDeps extends TelegramRuntimeEventRecorderPort {
|
|
2671
|
+
delayMs?: number;
|
|
2672
|
+
setTimer?: (
|
|
2673
|
+
callback: () => void,
|
|
2674
|
+
ms: number,
|
|
2675
|
+
) => ReturnType<typeof setTimeout>;
|
|
2676
|
+
clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
export interface TelegramDeferredQueueDispatchRuntime<TContext = unknown> {
|
|
2680
|
+
bind: (ctx: TContext) => void;
|
|
2681
|
+
unbind: () => void;
|
|
2682
|
+
isBound: () => boolean;
|
|
2683
|
+
getGeneration: () => number;
|
|
2684
|
+
isGenerationActive: (generation: number) => boolean;
|
|
2685
|
+
request: (dispatchNextQueuedTelegramTurn: (ctx: TContext) => void) => void;
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
/**
|
|
2689
|
+
* Production debounce for deferred queue dispatch; the factory defaults to this
|
|
2690
|
+
* so the entrypoint wires ports instead of policy constants.
|
|
2691
|
+
*/
|
|
2692
|
+
export const TELEGRAM_DEFERRED_DISPATCH_DELAY_MS = 50;
|
|
2693
|
+
|
|
2694
|
+
export function createTelegramDeferredQueueDispatchRuntime<TContext = unknown>(
|
|
2695
|
+
deps: TelegramDeferredQueueDispatchRuntimeDeps = {},
|
|
2696
|
+
): TelegramDeferredQueueDispatchRuntime<TContext> {
|
|
2697
|
+
let boundContext: TContext | undefined;
|
|
2698
|
+
let generation = 0;
|
|
2699
|
+
const timers = new Set<ReturnType<typeof setTimeout>>();
|
|
2700
|
+
const delayMs = deps.delayMs ?? TELEGRAM_DEFERRED_DISPATCH_DELAY_MS;
|
|
2701
|
+
const setTimer =
|
|
2702
|
+
deps.setTimer ??
|
|
2703
|
+
((callback: () => void, ms: number): ReturnType<typeof setTimeout> =>
|
|
2704
|
+
setTimeout(callback, ms));
|
|
2705
|
+
const clearTimer =
|
|
2706
|
+
deps.clearTimer ??
|
|
2707
|
+
((timer: ReturnType<typeof setTimeout>): void => clearTimeout(timer));
|
|
2708
|
+
const clearTimers = (): void => {
|
|
2709
|
+
for (const timer of timers) clearTimer(timer);
|
|
2710
|
+
timers.clear();
|
|
2711
|
+
};
|
|
2712
|
+
return {
|
|
2713
|
+
bind: (ctx) => {
|
|
2714
|
+
boundContext = ctx;
|
|
2715
|
+
generation += 1;
|
|
2716
|
+
},
|
|
2717
|
+
unbind: () => {
|
|
2718
|
+
boundContext = undefined;
|
|
2719
|
+
generation += 1;
|
|
2720
|
+
clearTimers();
|
|
2721
|
+
},
|
|
2722
|
+
isBound: () => boundContext !== undefined,
|
|
2723
|
+
getGeneration: () => generation,
|
|
2724
|
+
isGenerationActive: (expectedGeneration) =>
|
|
2725
|
+
boundContext !== undefined && generation === expectedGeneration,
|
|
2726
|
+
request: (dispatchNextQueuedTelegramTurn) => {
|
|
2727
|
+
if (boundContext === undefined) return;
|
|
2728
|
+
const scheduledGeneration = generation;
|
|
2729
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
2730
|
+
timer = setTimer(() => {
|
|
2731
|
+
timers.delete(timer);
|
|
2732
|
+
if (generation !== scheduledGeneration || boundContext === undefined)
|
|
2733
|
+
return;
|
|
2734
|
+
try {
|
|
2735
|
+
dispatchNextQueuedTelegramTurn(boundContext);
|
|
2736
|
+
} catch (error) {
|
|
2737
|
+
try {
|
|
2738
|
+
deps.recordRuntimeEvent?.("dispatch", error, {
|
|
2739
|
+
phase: "deferred-queue-dispatch",
|
|
2740
|
+
generation: scheduledGeneration,
|
|
2741
|
+
});
|
|
2742
|
+
} catch {
|
|
2743
|
+
// Timer diagnostics cannot escape the deferred owner.
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
}, delayMs);
|
|
2747
|
+
timer.unref?.();
|
|
2748
|
+
timers.add(timer);
|
|
2749
|
+
},
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
// --- Dispatch Watchdog Runtime ---
|
|
2754
|
+
|
|
2755
|
+
export interface TelegramQueueDispatchWatchdogRuntime<TContext = unknown> {
|
|
2756
|
+
start: (ctx: TContext) => void;
|
|
2757
|
+
stop: () => void;
|
|
2758
|
+
poke: () => void;
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
export interface TelegramQueueDispatchWatchdogRuntimeDeps<
|
|
2762
|
+
TContext = unknown,
|
|
2763
|
+
> extends TelegramRuntimeEventRecorderPort {
|
|
2764
|
+
hasQueuedItems: () => boolean;
|
|
2765
|
+
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
2766
|
+
intervalMs?: number;
|
|
2767
|
+
setInterval?: (
|
|
2768
|
+
callback: () => void,
|
|
2769
|
+
ms: number,
|
|
2770
|
+
) => ReturnType<typeof setInterval>;
|
|
2771
|
+
clearInterval?: (timer: ReturnType<typeof setInterval>) => void;
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
export function createTelegramQueueDispatchWatchdogRuntime<TContext = unknown>(
|
|
2775
|
+
deps: TelegramQueueDispatchWatchdogRuntimeDeps<TContext>,
|
|
2776
|
+
): TelegramQueueDispatchWatchdogRuntime<TContext> {
|
|
2777
|
+
const intervalMs = deps.intervalMs ?? 1000;
|
|
2778
|
+
const setIntervalFn: NonNullable<
|
|
2779
|
+
TelegramQueueDispatchWatchdogRuntimeDeps<TContext>["setInterval"]
|
|
2780
|
+
> = deps.setInterval ?? ((callback, ms) => setInterval(callback, ms));
|
|
2781
|
+
const clearIntervalFn: NonNullable<
|
|
2782
|
+
TelegramQueueDispatchWatchdogRuntimeDeps<TContext>["clearInterval"]
|
|
2783
|
+
> = deps.clearInterval ?? ((timer) => clearInterval(timer));
|
|
2784
|
+
let ctx: TContext | undefined;
|
|
2785
|
+
let interval: ReturnType<typeof setInterval> | undefined;
|
|
2786
|
+
let dispatchInFlight = false;
|
|
2787
|
+
const tick = (): void => {
|
|
2788
|
+
if (ctx === undefined || dispatchInFlight || !deps.hasQueuedItems()) return;
|
|
2789
|
+
dispatchInFlight = true;
|
|
2790
|
+
try {
|
|
2791
|
+
deps.dispatchNextQueuedTelegramTurn(ctx);
|
|
2792
|
+
} catch (error) {
|
|
2793
|
+
try {
|
|
2794
|
+
deps.recordRuntimeEvent?.("dispatch", error, {
|
|
2795
|
+
phase: "queue-watchdog",
|
|
2796
|
+
});
|
|
2797
|
+
} catch {
|
|
2798
|
+
// Watchdog diagnostics cannot escape the interval owner.
|
|
2799
|
+
}
|
|
2800
|
+
} finally {
|
|
2801
|
+
dispatchInFlight = false;
|
|
2802
|
+
}
|
|
2803
|
+
};
|
|
2804
|
+
const stop = (): void => {
|
|
2805
|
+
ctx = undefined;
|
|
2806
|
+
if (!interval) return;
|
|
2807
|
+
clearIntervalFn(interval);
|
|
2808
|
+
interval = undefined;
|
|
2809
|
+
};
|
|
2810
|
+
return {
|
|
2811
|
+
start: (nextCtx) => {
|
|
2812
|
+
ctx = nextCtx;
|
|
2813
|
+
if (!interval) {
|
|
2814
|
+
const nextInterval = setIntervalFn(tick, intervalMs);
|
|
2815
|
+
interval = nextInterval;
|
|
2816
|
+
nextInterval.unref?.();
|
|
2817
|
+
}
|
|
2818
|
+
tick();
|
|
2819
|
+
},
|
|
2820
|
+
stop,
|
|
2821
|
+
poke: tick,
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
// --- Dispatch Runtime ---
|
|
2826
|
+
|
|
2827
|
+
export interface TelegramPromptDeliveryOptions {
|
|
2828
|
+
deliverAs: "followUp";
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
export interface TelegramDispatchRuntimeDeps<TContext = unknown> {
|
|
2832
|
+
executeControlItem: (
|
|
2833
|
+
item: Extract<
|
|
2834
|
+
TelegramQueueDispatchAction<TContext>,
|
|
2835
|
+
{ kind: "control" }
|
|
2836
|
+
>["item"],
|
|
2837
|
+
) => void;
|
|
2838
|
+
onPromptDispatchStart: (chatId: number) => void;
|
|
2839
|
+
sendUserMessage: (
|
|
2840
|
+
content: Extract<
|
|
2841
|
+
TelegramQueueDispatchAction,
|
|
2842
|
+
{ kind: "prompt" }
|
|
2843
|
+
>["item"]["content"],
|
|
2844
|
+
options?: TelegramPromptDeliveryOptions,
|
|
2845
|
+
) => void;
|
|
2846
|
+
onPromptDispatchFailure: (message: string) => void;
|
|
2847
|
+
onIdle: () => void;
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
export interface TelegramQueueDispatchControllerDeps<
|
|
2851
|
+
TContext = unknown,
|
|
2852
|
+
> extends TelegramRuntimeEventRecorderPort {
|
|
2853
|
+
getQueuedItems: () => TelegramQueueItem<TContext>[];
|
|
2854
|
+
setQueuedItems: (items: TelegramQueueItem<TContext>[]) => void;
|
|
2855
|
+
canDispatch: (ctx: TContext) => boolean;
|
|
2856
|
+
hasDispatchContext?: () => boolean;
|
|
2857
|
+
getDispatchGeneration?: () => number;
|
|
2858
|
+
isDispatchGenerationActive?: (generation: number) => boolean;
|
|
2859
|
+
updateStatus: (ctx: TContext, error?: string) => void;
|
|
2860
|
+
sendTextReply: TelegramControlRuntimeDeps<TContext>["sendTextReply"];
|
|
2861
|
+
onPromptDispatchStart: (ctx: TContext, chatId: number) => void;
|
|
2862
|
+
sendUserMessage: TelegramDispatchRuntimeDeps<TContext>["sendUserMessage"];
|
|
2863
|
+
onPromptDispatchFailure: (ctx: TContext, message: string) => void;
|
|
2864
|
+
isQueueItemTransportActive?: (item: TelegramQueueItem<TContext>) => boolean;
|
|
2865
|
+
hasPendingInboundQueueMutationForItem?: (
|
|
2866
|
+
item: TelegramQueueItem<TContext>,
|
|
2867
|
+
) => boolean;
|
|
2868
|
+
isQueueItemAdmissionReady?: (item: TelegramQueueItem<TContext>) => boolean;
|
|
2869
|
+
onControlSettled?: (
|
|
2870
|
+
item: PendingTelegramControlItem<TContext>,
|
|
2871
|
+
ctx: TContext,
|
|
2872
|
+
) => void;
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
export interface TelegramQueueDispatchController<TContext = unknown> {
|
|
2876
|
+
dispatchNext: (ctx: TContext) => void;
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
export function executeTelegramQueueDispatchPlan<TContext = unknown>(
|
|
2880
|
+
plan: TelegramQueueDispatchAction<TContext>,
|
|
2881
|
+
deps: TelegramDispatchRuntimeDeps<TContext>,
|
|
2882
|
+
): void {
|
|
2883
|
+
if (plan.kind === "none") {
|
|
2884
|
+
deps.onIdle();
|
|
2885
|
+
return;
|
|
2886
|
+
}
|
|
2887
|
+
if (plan.kind === "control") {
|
|
2888
|
+
deps.executeControlItem(plan.item);
|
|
2889
|
+
return;
|
|
2890
|
+
}
|
|
2891
|
+
deps.onPromptDispatchStart(plan.item.chatId);
|
|
2892
|
+
try {
|
|
2893
|
+
deps.sendUserMessage(plan.item.content);
|
|
2894
|
+
} catch (error) {
|
|
2895
|
+
const message = getTelegramQueueErrorMessage(error);
|
|
2896
|
+
deps.onPromptDispatchFailure(message);
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
export type TelegramQueueDispatchRuntimeDeps<TContext = unknown> = Omit<
|
|
2901
|
+
TelegramQueueDispatchControllerDeps<TContext>,
|
|
2902
|
+
"canDispatch"
|
|
2903
|
+
> &
|
|
2904
|
+
TelegramDispatchReadinessDeps<TContext>;
|
|
2905
|
+
|
|
2906
|
+
export function createTelegramQueueDispatchRuntime<TContext = unknown>(
|
|
2907
|
+
deps: TelegramQueueDispatchRuntimeDeps<TContext>,
|
|
2908
|
+
): TelegramQueueDispatchController<TContext> {
|
|
2909
|
+
return createTelegramQueueDispatchController({
|
|
2910
|
+
getQueuedItems: deps.getQueuedItems,
|
|
2911
|
+
setQueuedItems: deps.setQueuedItems,
|
|
2912
|
+
canDispatch: createTelegramDispatchReadinessChecker({
|
|
2913
|
+
isCompactionInProgress: deps.isCompactionInProgress,
|
|
2914
|
+
hasActiveTurn: deps.hasActiveTurn,
|
|
2915
|
+
hasDispatchPending: deps.hasDispatchPending,
|
|
2916
|
+
isIdle: deps.isIdle,
|
|
2917
|
+
hasPendingMessages: deps.hasPendingMessages,
|
|
2918
|
+
}),
|
|
2919
|
+
hasDispatchContext: deps.hasDispatchContext,
|
|
2920
|
+
getDispatchGeneration: deps.getDispatchGeneration,
|
|
2921
|
+
isDispatchGenerationActive: deps.isDispatchGenerationActive,
|
|
2922
|
+
updateStatus: deps.updateStatus,
|
|
2923
|
+
sendTextReply: deps.sendTextReply,
|
|
2924
|
+
onPromptDispatchStart: deps.onPromptDispatchStart,
|
|
2925
|
+
sendUserMessage: deps.sendUserMessage,
|
|
2926
|
+
onPromptDispatchFailure: deps.onPromptDispatchFailure,
|
|
2927
|
+
isQueueItemTransportActive: deps.isQueueItemTransportActive,
|
|
2928
|
+
hasPendingInboundQueueMutationForItem:
|
|
2929
|
+
deps.hasPendingInboundQueueMutationForItem,
|
|
2930
|
+
isQueueItemAdmissionReady: deps.isQueueItemAdmissionReady,
|
|
2931
|
+
onControlSettled: deps.onControlSettled,
|
|
2932
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
2933
|
+
});
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
2937
|
+
deps: TelegramQueueDispatchControllerDeps<TContext>,
|
|
2938
|
+
): TelegramQueueDispatchController<TContext> {
|
|
2939
|
+
let controlDispatchPending = false;
|
|
2940
|
+
const controller: TelegramQueueDispatchController<TContext> = {
|
|
2941
|
+
dispatchNext: (ctx) => {
|
|
2942
|
+
if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
|
|
2943
|
+
if (controlDispatchPending) {
|
|
2944
|
+
deps.updateStatus(ctx);
|
|
2945
|
+
return;
|
|
2946
|
+
}
|
|
2947
|
+
const queuedItems = deps.getQueuedItems();
|
|
2948
|
+
const activeItems: TelegramQueueItem<TContext>[] = [];
|
|
2949
|
+
const protectedInactiveItems: TelegramQueueItem<TContext>[] = [];
|
|
2950
|
+
const retainedItems: TelegramQueueItem<TContext>[] = [];
|
|
2951
|
+
let droppedInactiveItemCount = 0;
|
|
2952
|
+
for (const item of queuedItems) {
|
|
2953
|
+
if (
|
|
2954
|
+
!deps.isQueueItemTransportActive ||
|
|
2955
|
+
deps.isQueueItemTransportActive(item)
|
|
2956
|
+
) {
|
|
2957
|
+
activeItems.push(item);
|
|
2958
|
+
retainedItems.push(item);
|
|
2959
|
+
} else if ((item.admissionReceipts?.length ?? 0) > 0) {
|
|
2960
|
+
protectedInactiveItems.push(item);
|
|
2961
|
+
retainedItems.push(item);
|
|
2962
|
+
} else {
|
|
2963
|
+
droppedInactiveItemCount += 1;
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
if (droppedInactiveItemCount > 0) {
|
|
2967
|
+
deps.setQueuedItems(retainedItems);
|
|
2968
|
+
deps.recordRuntimeEvent?.(
|
|
2969
|
+
"dispatch",
|
|
2970
|
+
new Error(
|
|
2971
|
+
"Dropped queue work from an inactive Telegram transport generation.",
|
|
2972
|
+
),
|
|
2973
|
+
{ phase: "transport-generation" },
|
|
2974
|
+
);
|
|
2975
|
+
}
|
|
2976
|
+
const canDispatch = deps.canDispatch(ctx);
|
|
2977
|
+
let nextActiveIndex = 0;
|
|
2978
|
+
if (canDispatch) {
|
|
2979
|
+
while (nextActiveIndex < activeItems.length) {
|
|
2980
|
+
const candidate = activeItems[nextActiveIndex];
|
|
2981
|
+
if (
|
|
2982
|
+
!candidate ||
|
|
2983
|
+
candidate.kind !== "prompt" ||
|
|
2984
|
+
candidate.reactionSuppressionEmoji === undefined
|
|
2985
|
+
) {
|
|
2986
|
+
break;
|
|
2987
|
+
}
|
|
2988
|
+
if (deps.hasPendingInboundQueueMutationForItem?.(candidate)) {
|
|
2989
|
+
deps.updateStatus(ctx);
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
if (
|
|
2993
|
+
deps.isQueueItemAdmissionReady &&
|
|
2994
|
+
!deps.isQueueItemAdmissionReady(candidate)
|
|
2995
|
+
) {
|
|
2996
|
+
deps.updateStatus(ctx);
|
|
2997
|
+
return;
|
|
2998
|
+
}
|
|
2999
|
+
nextActiveIndex += 1;
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
const dispatchableItems = activeItems.slice(nextActiveIndex);
|
|
3003
|
+
const nextItem = dispatchableItems[0];
|
|
3004
|
+
if (
|
|
3005
|
+
nextItem &&
|
|
3006
|
+
deps.hasPendingInboundQueueMutationForItem?.(nextItem)
|
|
3007
|
+
) {
|
|
3008
|
+
deps.updateStatus(ctx);
|
|
3009
|
+
return;
|
|
3010
|
+
}
|
|
3011
|
+
if (
|
|
3012
|
+
nextItem &&
|
|
3013
|
+
deps.isQueueItemAdmissionReady &&
|
|
3014
|
+
!deps.isQueueItemAdmissionReady(nextItem)
|
|
3015
|
+
) {
|
|
3016
|
+
deps.updateStatus(ctx);
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
const dispatchPlan = planNextTelegramQueueAction(
|
|
3020
|
+
dispatchableItems,
|
|
3021
|
+
canDispatch,
|
|
3022
|
+
);
|
|
3023
|
+
if (nextActiveIndex > 0 || dispatchPlan.kind !== "none") {
|
|
3024
|
+
deps.setQueuedItems([
|
|
3025
|
+
...dispatchPlan.remainingItems,
|
|
3026
|
+
...protectedInactiveItems,
|
|
3027
|
+
]);
|
|
3028
|
+
}
|
|
3029
|
+
executeTelegramQueueDispatchPlan(dispatchPlan, {
|
|
3030
|
+
executeControlItem: (item) => {
|
|
3031
|
+
controlDispatchPending = true;
|
|
3032
|
+
const dispatchGeneration = deps.getDispatchGeneration?.();
|
|
3033
|
+
deps.updateStatus(ctx);
|
|
3034
|
+
void executeTelegramControlItemRuntime(item, {
|
|
3035
|
+
ctx,
|
|
3036
|
+
sendTextReply: deps.sendTextReply,
|
|
3037
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
3038
|
+
onSettled: (settledItem) => {
|
|
3039
|
+
try {
|
|
3040
|
+
deps.onControlSettled?.(settledItem, ctx);
|
|
3041
|
+
} catch (error) {
|
|
3042
|
+
deps.recordRuntimeEvent?.("control", error, {
|
|
3043
|
+
phase: "receipt-settlement",
|
|
3044
|
+
controlType: settledItem.controlType,
|
|
3045
|
+
});
|
|
3046
|
+
}
|
|
3047
|
+
controlDispatchPending = false;
|
|
3048
|
+
if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
|
|
3049
|
+
if (
|
|
3050
|
+
dispatchGeneration !== undefined &&
|
|
3051
|
+
deps.isDispatchGenerationActive &&
|
|
3052
|
+
!deps.isDispatchGenerationActive(dispatchGeneration)
|
|
3053
|
+
) {
|
|
3054
|
+
return;
|
|
3055
|
+
}
|
|
3056
|
+
deps.updateStatus(ctx);
|
|
3057
|
+
controller.dispatchNext(ctx);
|
|
3058
|
+
},
|
|
3059
|
+
});
|
|
3060
|
+
},
|
|
3061
|
+
onPromptDispatchStart: (chatId) => {
|
|
3062
|
+
deps.onPromptDispatchStart(ctx, chatId);
|
|
3063
|
+
},
|
|
3064
|
+
sendUserMessage: deps.sendUserMessage,
|
|
3065
|
+
onPromptDispatchFailure: (message) => {
|
|
3066
|
+
deps.onPromptDispatchFailure(ctx, message);
|
|
3067
|
+
},
|
|
3068
|
+
onIdle: () => {
|
|
3069
|
+
deps.updateStatus(ctx);
|
|
3070
|
+
},
|
|
3071
|
+
});
|
|
3072
|
+
},
|
|
3073
|
+
};
|
|
3074
|
+
return controller;
|
|
3075
|
+
}
|