@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,1422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram singleton lock helpers
|
|
3
|
+
* Zones: telegram ownership, filesystem, transport authority
|
|
4
|
+
* Owns extension-local owners.json access and Telegram bridge ownership semantics
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
chmodSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
lstatSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
mkdtempSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
renameSync,
|
|
16
|
+
rmSync,
|
|
17
|
+
unlinkSync,
|
|
18
|
+
writeFileSync,
|
|
19
|
+
} from "node:fs";
|
|
20
|
+
import { randomUUID } from "node:crypto";
|
|
21
|
+
import { basename, dirname, join } from "node:path";
|
|
22
|
+
import { resolveTelegramOwnersPath } from "./paths.ts";
|
|
23
|
+
|
|
24
|
+
export const TELEGRAM_LOCK_KEY = "default";
|
|
25
|
+
export const TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS = 8_000;
|
|
26
|
+
export const TELEGRAM_OWNERSHIP_CHECK_MS = 1_000;
|
|
27
|
+
export const TELEGRAM_OWNERSHIP_REFRESH_MS = 2_000;
|
|
28
|
+
const TELEGRAM_LOCK_WRITE_RETRY_ATTEMPTS = 5;
|
|
29
|
+
const TELEGRAM_LOCK_WRITE_RETRY_DELAY_MS = 25;
|
|
30
|
+
const TELEGRAM_LOCK_TRANSACTION_ATTEMPTS = 80;
|
|
31
|
+
const TELEGRAM_LOCK_TRANSACTION_RETRY_DELAY_MS = 25;
|
|
32
|
+
const TELEGRAM_LOCK_RUNTIME_GENERATION_KEY =
|
|
33
|
+
"__piTelegramLockRuntimeGeneration__";
|
|
34
|
+
|
|
35
|
+
function allocateTelegramLockRuntimeGeneration(): number {
|
|
36
|
+
const globals = globalThis as Record<string, unknown>;
|
|
37
|
+
const previous = globals[TELEGRAM_LOCK_RUNTIME_GENERATION_KEY];
|
|
38
|
+
const previousGeneration =
|
|
39
|
+
typeof previous === "number" && Number.isSafeInteger(previous)
|
|
40
|
+
? previous
|
|
41
|
+
: 0;
|
|
42
|
+
const generation = Math.max(Date.now(), previousGeneration + 1);
|
|
43
|
+
globals[TELEGRAM_LOCK_RUNTIME_GENERATION_KEY] = generation;
|
|
44
|
+
return generation;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getOwnersPath(): string {
|
|
48
|
+
return resolveTelegramOwnersPath();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the extension-local owner slot for the active Telegram profile.
|
|
53
|
+
* Default profile → default
|
|
54
|
+
* Named profile → the validated profile name
|
|
55
|
+
*/
|
|
56
|
+
export function resolveTelegramLockKey(activeProfile?: string): string {
|
|
57
|
+
return activeProfile || TELEGRAM_LOCK_KEY;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface TelegramActiveProfileGetter {
|
|
61
|
+
getActiveProfileName: () => string | undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createTelegramLockKeyResolver(
|
|
65
|
+
activeProfile: TelegramActiveProfileGetter,
|
|
66
|
+
): () => string {
|
|
67
|
+
return function getTelegramLockKey() {
|
|
68
|
+
return resolveTelegramLockKey(activeProfile.getActiveProfileName());
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface TelegramLockEntry {
|
|
73
|
+
pid: number;
|
|
74
|
+
cwd?: string;
|
|
75
|
+
instanceId?: string;
|
|
76
|
+
heartbeatMs?: number;
|
|
77
|
+
leaderEpoch?: number | string;
|
|
78
|
+
runtimeGeneration?: number;
|
|
79
|
+
busSocketPath?: string;
|
|
80
|
+
busSecret?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface TelegramLockContext {
|
|
84
|
+
cwd: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type TelegramLockState =
|
|
88
|
+
| { kind: "inactive" }
|
|
89
|
+
| { kind: "active-here"; lock: TelegramLockEntry }
|
|
90
|
+
| { kind: "active-elsewhere"; lock: TelegramLockEntry }
|
|
91
|
+
| { kind: "stale"; lock: TelegramLockEntry };
|
|
92
|
+
|
|
93
|
+
export interface TelegramLockAcquireOptions {
|
|
94
|
+
force?: boolean;
|
|
95
|
+
expectedOwner?: TelegramLockEntry;
|
|
96
|
+
election?: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type TelegramLockAcquireResult =
|
|
100
|
+
| { ok: true; lock: TelegramLockEntry; replacedStale: boolean }
|
|
101
|
+
| { ok: false; lock: TelegramLockEntry };
|
|
102
|
+
|
|
103
|
+
export interface TelegramLockRuntime<TContext extends TelegramLockContext> {
|
|
104
|
+
acquire: (
|
|
105
|
+
ctx: TContext,
|
|
106
|
+
options?: TelegramLockAcquireOptions,
|
|
107
|
+
) => TelegramLockAcquireResult;
|
|
108
|
+
release: () => TelegramLockState;
|
|
109
|
+
getState: () => TelegramLockState;
|
|
110
|
+
getStatusLabel: () => string;
|
|
111
|
+
getOwnedLeaderEpoch: () => number | string | undefined;
|
|
112
|
+
owns: (ctx?: TelegramLockContext) => boolean;
|
|
113
|
+
commitIfOwned: (commit: () => void) => boolean;
|
|
114
|
+
refresh: (ctx?: TelegramLockContext) => boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface TelegramLockOwnershipGuard<
|
|
118
|
+
TContext extends TelegramLockContext,
|
|
119
|
+
> {
|
|
120
|
+
ownsContext: (ctx: TContext) => boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface TelegramLockContextStore<
|
|
124
|
+
TContext extends TelegramLockContext,
|
|
125
|
+
> {
|
|
126
|
+
get: () => TContext | undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface TelegramLockRuntimeOptions {
|
|
130
|
+
key?: string | (() => string | undefined);
|
|
131
|
+
locksPath?: string;
|
|
132
|
+
pid?: number;
|
|
133
|
+
isProcessAlive?: (pid: number) => boolean;
|
|
134
|
+
instanceId?: string;
|
|
135
|
+
busSocketPath?: string;
|
|
136
|
+
busSecret?: string;
|
|
137
|
+
getNowMs?: () => number;
|
|
138
|
+
mintLeaderEpoch?: () => number | string;
|
|
139
|
+
runtimeGeneration?: number;
|
|
140
|
+
staleHeartbeatMs?: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function readLocks(path = getOwnersPath()): Record<string, unknown> {
|
|
144
|
+
if (!existsSync(path)) return {};
|
|
145
|
+
try {
|
|
146
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
147
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
148
|
+
? (value as Record<string, unknown>)
|
|
149
|
+
: {};
|
|
150
|
+
} catch {
|
|
151
|
+
return {};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function readLocksForTransaction(path: string): Record<string, unknown> {
|
|
156
|
+
let source: string;
|
|
157
|
+
try {
|
|
158
|
+
source = readFileSync(path, "utf8");
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if ((error as { code?: unknown })?.code === "ENOENT") return {};
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
const value: unknown = JSON.parse(source);
|
|
164
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
165
|
+
throw new Error(`Invalid Telegram owner store: ${path}`);
|
|
166
|
+
}
|
|
167
|
+
return value as Record<string, unknown>;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isRetryableLockWriteError(error: unknown): boolean {
|
|
171
|
+
const code = (error as { code?: unknown })?.code;
|
|
172
|
+
return code === "EPERM" || code === "EBUSY" || code === "EACCES";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function sleepSync(ms: number): void {
|
|
176
|
+
const buffer = new SharedArrayBuffer(4);
|
|
177
|
+
Atomics.wait(new Int32Array(buffer), 0, 0, ms);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface TelegramRenameRetryOptions {
|
|
181
|
+
rename?: typeof renameSync;
|
|
182
|
+
attempts?: number;
|
|
183
|
+
retryDelayMs?: number;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Rename one Telegram runtime artifact with bounded Windows sharing retries. */
|
|
187
|
+
export function renameTelegramPathWithRetry(
|
|
188
|
+
sourcePath: string,
|
|
189
|
+
destinationPath: string,
|
|
190
|
+
options: TelegramRenameRetryOptions = {},
|
|
191
|
+
): boolean {
|
|
192
|
+
const rename = options.rename ?? renameSync;
|
|
193
|
+
const attempts = Math.max(
|
|
194
|
+
1,
|
|
195
|
+
options.attempts ?? TELEGRAM_LOCK_WRITE_RETRY_ATTEMPTS,
|
|
196
|
+
);
|
|
197
|
+
const retryDelayMs = Math.max(
|
|
198
|
+
0,
|
|
199
|
+
options.retryDelayMs ?? TELEGRAM_LOCK_WRITE_RETRY_DELAY_MS,
|
|
200
|
+
);
|
|
201
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
202
|
+
try {
|
|
203
|
+
rename(sourcePath, destinationPath);
|
|
204
|
+
return true;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if ((error as { code?: unknown })?.code === "ENOENT") return false;
|
|
207
|
+
if (!isRetryableLockWriteError(error) || attempt === attempts - 1) {
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
sleepSync(retryDelayMs * (attempt + 1));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
interface TelegramLockTransactionOwner {
|
|
217
|
+
pid: number;
|
|
218
|
+
acquiredAtMs: number;
|
|
219
|
+
generation: string;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const TELEGRAM_TRANSACTION_OWNER_PATTERN = /^owner\.([A-Za-z0-9-]+)\.json$/u;
|
|
223
|
+
|
|
224
|
+
function getLockTransactionOwnerFile(generation: string): string {
|
|
225
|
+
return `owner.${generation}.json`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function getLockTransactionOwnerPath(path: string): string {
|
|
229
|
+
const stat = lstatSync(path);
|
|
230
|
+
if (stat.isDirectory()) {
|
|
231
|
+
const entries = readdirSync(path);
|
|
232
|
+
if (
|
|
233
|
+
entries.length === 1 &&
|
|
234
|
+
(TELEGRAM_TRANSACTION_OWNER_PATTERN.test(entries[0]) ||
|
|
235
|
+
TELEGRAM_TRANSACTION_RECLAIM_PATTERN.test(entries[0]))
|
|
236
|
+
) {
|
|
237
|
+
return join(path, entries[0]);
|
|
238
|
+
}
|
|
239
|
+
throw new Error(`Unverifiable Telegram lock transaction guard: ${path}`);
|
|
240
|
+
}
|
|
241
|
+
if (stat.isFile()) return path;
|
|
242
|
+
throw new Error(`Unsupported Telegram lock transaction guard: ${path}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function readLockTransactionOwner(
|
|
246
|
+
path: string,
|
|
247
|
+
): TelegramLockTransactionOwner | undefined {
|
|
248
|
+
try {
|
|
249
|
+
const value = JSON.parse(
|
|
250
|
+
readFileSync(getLockTransactionOwnerPath(path), "utf8"),
|
|
251
|
+
) as Record<string, unknown>;
|
|
252
|
+
if (
|
|
253
|
+
typeof value.pid !== "number" ||
|
|
254
|
+
typeof value.acquiredAtMs !== "number" ||
|
|
255
|
+
typeof value.generation !== "string"
|
|
256
|
+
) {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
const ownerMatch = TELEGRAM_TRANSACTION_OWNER_PATTERN.exec(
|
|
260
|
+
basename(getLockTransactionOwnerPath(path)),
|
|
261
|
+
);
|
|
262
|
+
if (ownerMatch && ownerMatch[1] !== value.generation) return undefined;
|
|
263
|
+
return {
|
|
264
|
+
pid: value.pid,
|
|
265
|
+
acquiredAtMs: value.acquiredAtMs,
|
|
266
|
+
generation: value.generation,
|
|
267
|
+
};
|
|
268
|
+
} catch {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function createLockTransactionContentionError(path: string): Error {
|
|
274
|
+
return Object.assign(
|
|
275
|
+
new Error(`Telegram lock transaction guard already exists: ${path}`),
|
|
276
|
+
{ code: "EEXIST" },
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function isLockTransactionContentionError(error: unknown): boolean {
|
|
281
|
+
const code = (error as { code?: unknown })?.code;
|
|
282
|
+
return (
|
|
283
|
+
code === "EEXIST" ||
|
|
284
|
+
code === "ENOTEMPTY" ||
|
|
285
|
+
code === "ENOTDIR" ||
|
|
286
|
+
code === "EISDIR" ||
|
|
287
|
+
isRetryableLockWriteError(error)
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function removeLockTransactionGuard(path: string): void {
|
|
292
|
+
rmSync(path, { recursive: true, force: true });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function createLockTransactionGuard(
|
|
296
|
+
path: string,
|
|
297
|
+
options: TelegramFileTransactionOptions = {},
|
|
298
|
+
): TelegramLockTransactionOwner {
|
|
299
|
+
const owner: TelegramLockTransactionOwner = {
|
|
300
|
+
pid: process.pid,
|
|
301
|
+
acquiredAtMs: Date.now(),
|
|
302
|
+
generation: randomUUID(),
|
|
303
|
+
};
|
|
304
|
+
const stagedPath = mkdtempSync(`${path}.staged.`);
|
|
305
|
+
try {
|
|
306
|
+
chmodSync(stagedPath, 0o700);
|
|
307
|
+
writeFileSync(
|
|
308
|
+
join(stagedPath, getLockTransactionOwnerFile(owner.generation)),
|
|
309
|
+
`${JSON.stringify(owner)}\n`,
|
|
310
|
+
{ encoding: "utf8", flag: "wx", mode: 0o600 },
|
|
311
|
+
);
|
|
312
|
+
if (existsSync(path)) throw createLockTransactionContentionError(path);
|
|
313
|
+
(options.publishRename ?? renameSync)(stagedPath, path);
|
|
314
|
+
return owner;
|
|
315
|
+
} finally {
|
|
316
|
+
try {
|
|
317
|
+
removeLockTransactionGuard(stagedPath);
|
|
318
|
+
} catch {
|
|
319
|
+
/* best effort */
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function releaseLockTransactionGuard(
|
|
325
|
+
path: string,
|
|
326
|
+
owner: TelegramLockTransactionOwner,
|
|
327
|
+
): void {
|
|
328
|
+
const current = readLockTransactionOwner(path);
|
|
329
|
+
if (!current) {
|
|
330
|
+
if (!existsSync(path)) return;
|
|
331
|
+
throw new Error(`Cannot verify Telegram lock transaction guard: ${path}`);
|
|
332
|
+
}
|
|
333
|
+
if (
|
|
334
|
+
current.pid !== owner.pid ||
|
|
335
|
+
current.generation !== owner.generation ||
|
|
336
|
+
current.acquiredAtMs !== owner.acquiredAtMs
|
|
337
|
+
) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
`Telegram lock transaction guard changed ownership: ${path}`,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
const releasedPath = `${path}.released.${randomUUID()}`;
|
|
343
|
+
if (!renameTelegramPathWithRetry(path, releasedPath)) return;
|
|
344
|
+
try {
|
|
345
|
+
removeLockTransactionGuard(releasedPath);
|
|
346
|
+
} catch {
|
|
347
|
+
/* released debris cannot retain transaction authority */
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function isAbandonedLockTransaction(path: string): boolean {
|
|
352
|
+
const owner = readLockTransactionOwner(path);
|
|
353
|
+
return owner ? !isProcessAlive(owner.pid) : false;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const TELEGRAM_TRANSACTION_RECLAIM_PATTERN =
|
|
357
|
+
/^owner\.reclaim\.(\d+)\.([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.json$/u;
|
|
358
|
+
const TELEGRAM_ACTIVE_TRANSACTION_RECLAIMS = Symbol.for(
|
|
359
|
+
"@llblab/pi-telegram/active-transaction-reclaims",
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
type TelegramTransactionGlobal = typeof globalThis & {
|
|
363
|
+
[TELEGRAM_ACTIVE_TRANSACTION_RECLAIMS]?: Set<string>;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
export interface TelegramFileTransactionOptions {
|
|
367
|
+
recoveryRename?: typeof renameSync;
|
|
368
|
+
publishRename?: typeof renameSync;
|
|
369
|
+
attempts?: number;
|
|
370
|
+
retryDelayMs?: number;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function getActiveTransactionReclaims(): Set<string> {
|
|
374
|
+
const root = globalThis as TelegramTransactionGlobal;
|
|
375
|
+
return (root[TELEGRAM_ACTIVE_TRANSACTION_RECLAIMS] ??= new Set());
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function reclaimAbandonedDirectoryGuard(
|
|
379
|
+
path: string,
|
|
380
|
+
options: TelegramFileTransactionOptions = {},
|
|
381
|
+
): boolean {
|
|
382
|
+
try {
|
|
383
|
+
if (!lstatSync(path).isDirectory()) return false;
|
|
384
|
+
} catch {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
let entries: string[];
|
|
388
|
+
try {
|
|
389
|
+
entries = readdirSync(path);
|
|
390
|
+
} catch {
|
|
391
|
+
// Another recovery candidate may remove the observed guard after lstat.
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
if (entries.length !== 1) return false;
|
|
395
|
+
const entry = entries[0];
|
|
396
|
+
let observedPid: number;
|
|
397
|
+
let observedReclaimGeneration: string | undefined;
|
|
398
|
+
if (TELEGRAM_TRANSACTION_OWNER_PATTERN.test(entry)) {
|
|
399
|
+
const owner = readLockTransactionOwner(path);
|
|
400
|
+
if (!owner) return false;
|
|
401
|
+
observedPid = owner.pid;
|
|
402
|
+
} else {
|
|
403
|
+
const match = TELEGRAM_TRANSACTION_RECLAIM_PATTERN.exec(entry);
|
|
404
|
+
if (!match) return false;
|
|
405
|
+
observedPid = Number.parseInt(match[1], 10);
|
|
406
|
+
observedReclaimGeneration = match[2];
|
|
407
|
+
}
|
|
408
|
+
const activeReclaims = getActiveTransactionReclaims();
|
|
409
|
+
if (observedPid === process.pid && observedReclaimGeneration !== undefined) {
|
|
410
|
+
if (activeReclaims.has(observedReclaimGeneration)) return false;
|
|
411
|
+
} else if (isProcessAlive(observedPid)) {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const renameRecovery = options.recoveryRename ?? renameSync;
|
|
416
|
+
const sourcePath = join(path, entry);
|
|
417
|
+
const reclaimGeneration = randomUUID();
|
|
418
|
+
const reclaimPath = join(
|
|
419
|
+
path,
|
|
420
|
+
`owner.reclaim.${process.pid}.${reclaimGeneration}.json`,
|
|
421
|
+
);
|
|
422
|
+
try {
|
|
423
|
+
// Claim inside the still-occupied guard before making its stable path free.
|
|
424
|
+
renameRecovery(sourcePath, reclaimPath);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
if ((error as { code?: unknown })?.code === "ENOENT") return false;
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const renameWithRetry = (fromPath: string, toPath: string): boolean =>
|
|
431
|
+
renameTelegramPathWithRetry(fromPath, toPath, { rename: renameRecovery });
|
|
432
|
+
|
|
433
|
+
activeReclaims.add(reclaimGeneration);
|
|
434
|
+
const stalePath = `${path}.stale.${process.pid}.${randomUUID()}`;
|
|
435
|
+
try {
|
|
436
|
+
try {
|
|
437
|
+
if (!renameWithRetry(path, stalePath)) return false;
|
|
438
|
+
} catch (renameError) {
|
|
439
|
+
try {
|
|
440
|
+
if (!renameWithRetry(reclaimPath, sourcePath)) throw renameError;
|
|
441
|
+
} catch (rollbackError) {
|
|
442
|
+
throw new AggregateError(
|
|
443
|
+
[renameError, rollbackError],
|
|
444
|
+
`Failed to reclaim or restore Telegram lock transaction guard: ${path}`,
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
throw renameError;
|
|
448
|
+
}
|
|
449
|
+
} finally {
|
|
450
|
+
activeReclaims.delete(reclaimGeneration);
|
|
451
|
+
}
|
|
452
|
+
try {
|
|
453
|
+
removeLockTransactionGuard(stalePath);
|
|
454
|
+
} catch {
|
|
455
|
+
/* stale debris cannot retain transaction authority */
|
|
456
|
+
}
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function acquireRecoverableDirectoryGuard(
|
|
461
|
+
path: string,
|
|
462
|
+
options: TelegramFileTransactionOptions = {},
|
|
463
|
+
): TelegramLockTransactionOwner | undefined {
|
|
464
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
465
|
+
try {
|
|
466
|
+
return createLockTransactionGuard(path, options);
|
|
467
|
+
} catch (error) {
|
|
468
|
+
if (!isLockTransactionContentionError(error)) throw error;
|
|
469
|
+
if (!reclaimAbandonedDirectoryGuard(path, options)) return undefined;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function removeAbandonedLegacyRecoveryGuard(
|
|
476
|
+
path: string,
|
|
477
|
+
options: TelegramFileTransactionOptions = {},
|
|
478
|
+
): boolean {
|
|
479
|
+
try {
|
|
480
|
+
if (!lstatSync(path).isFile() || !isAbandonedLockTransaction(path))
|
|
481
|
+
return false;
|
|
482
|
+
} catch {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
const migrationGuardPath = `${path}.migration`;
|
|
486
|
+
const migrationOwner = acquireRecoverableDirectoryGuard(
|
|
487
|
+
migrationGuardPath,
|
|
488
|
+
options,
|
|
489
|
+
);
|
|
490
|
+
if (!migrationOwner) return false;
|
|
491
|
+
try {
|
|
492
|
+
try {
|
|
493
|
+
if (!lstatSync(path).isFile() || !isAbandonedLockTransaction(path))
|
|
494
|
+
return false;
|
|
495
|
+
} catch {
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
const stalePath = `${path}.stale.${process.pid}.${randomUUID()}`;
|
|
499
|
+
try {
|
|
500
|
+
renameSync(path, stalePath);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
if ((error as { code?: unknown })?.code === "ENOENT") return false;
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
removeLockTransactionGuard(stalePath);
|
|
507
|
+
} catch {
|
|
508
|
+
/* stale debris cannot retain transaction authority */
|
|
509
|
+
}
|
|
510
|
+
return true;
|
|
511
|
+
} finally {
|
|
512
|
+
releaseLockTransactionGuard(migrationGuardPath, migrationOwner);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function acquireLegacyRecoveryGuard(
|
|
517
|
+
path: string,
|
|
518
|
+
options: TelegramFileTransactionOptions = {},
|
|
519
|
+
): TelegramLockTransactionOwner | undefined {
|
|
520
|
+
let owner = acquireRecoverableDirectoryGuard(path, options);
|
|
521
|
+
if (owner) return owner;
|
|
522
|
+
if (!removeAbandonedLegacyRecoveryGuard(path, options)) return undefined;
|
|
523
|
+
owner = acquireRecoverableDirectoryGuard(path, options);
|
|
524
|
+
return owner;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function createRecoveredLockTransactionGuard(
|
|
528
|
+
path: string,
|
|
529
|
+
options: TelegramFileTransactionOptions = {},
|
|
530
|
+
): TelegramLockTransactionOwner | undefined {
|
|
531
|
+
try {
|
|
532
|
+
return createLockTransactionGuard(path, options);
|
|
533
|
+
} catch (error) {
|
|
534
|
+
if (isLockTransactionContentionError(error)) return undefined;
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function recoverAbandonedLockTransaction(
|
|
540
|
+
path: string,
|
|
541
|
+
options: TelegramFileTransactionOptions = {},
|
|
542
|
+
): TelegramLockTransactionOwner | undefined {
|
|
543
|
+
if (!isAbandonedLockTransaction(path)) return undefined;
|
|
544
|
+
let isDirectory: boolean;
|
|
545
|
+
try {
|
|
546
|
+
isDirectory = lstatSync(path).isDirectory();
|
|
547
|
+
} catch {
|
|
548
|
+
return undefined;
|
|
549
|
+
}
|
|
550
|
+
if (isDirectory) {
|
|
551
|
+
if (!reclaimAbandonedDirectoryGuard(path, options)) return undefined;
|
|
552
|
+
const recoveredOwner = createRecoveredLockTransactionGuard(path, options);
|
|
553
|
+
try {
|
|
554
|
+
reclaimAbandonedDirectoryGuard(`${path}.recovery`, options);
|
|
555
|
+
return recoveredOwner;
|
|
556
|
+
} catch (error) {
|
|
557
|
+
if (recoveredOwner) {
|
|
558
|
+
try {
|
|
559
|
+
releaseLockTransactionGuard(path, recoveredOwner);
|
|
560
|
+
} catch {
|
|
561
|
+
/* preserve the recovery cleanup failure */
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
throw error;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const recoveryGuardPath = `${path}.recovery`;
|
|
569
|
+
const recoveryOwner = acquireLegacyRecoveryGuard(recoveryGuardPath, options);
|
|
570
|
+
if (!recoveryOwner) return undefined;
|
|
571
|
+
let recoveredOwner: TelegramLockTransactionOwner | undefined;
|
|
572
|
+
try {
|
|
573
|
+
if (!isAbandonedLockTransaction(path)) return undefined;
|
|
574
|
+
const stalePath = `${path}.stale.${process.pid}.${randomUUID()}`;
|
|
575
|
+
try {
|
|
576
|
+
renameSync(path, stalePath);
|
|
577
|
+
} catch (error) {
|
|
578
|
+
if ((error as { code?: unknown })?.code === "ENOENT") return undefined;
|
|
579
|
+
throw error;
|
|
580
|
+
}
|
|
581
|
+
try {
|
|
582
|
+
removeLockTransactionGuard(stalePath);
|
|
583
|
+
} catch {
|
|
584
|
+
/* stale debris cannot retain transaction authority */
|
|
585
|
+
}
|
|
586
|
+
recoveredOwner = createRecoveredLockTransactionGuard(path, options);
|
|
587
|
+
return recoveredOwner;
|
|
588
|
+
} finally {
|
|
589
|
+
try {
|
|
590
|
+
releaseLockTransactionGuard(recoveryGuardPath, recoveryOwner);
|
|
591
|
+
} catch (error) {
|
|
592
|
+
if (recoveredOwner) {
|
|
593
|
+
try {
|
|
594
|
+
releaseLockTransactionGuard(path, recoveredOwner);
|
|
595
|
+
} catch {
|
|
596
|
+
/* preserve the recovery cleanup failure */
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
throw error;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function acquireLockTransaction(
|
|
605
|
+
path: string,
|
|
606
|
+
options: TelegramFileTransactionOptions = {},
|
|
607
|
+
): TelegramLockTransactionOwner {
|
|
608
|
+
const attempts = Math.max(
|
|
609
|
+
1,
|
|
610
|
+
options.attempts ?? TELEGRAM_LOCK_TRANSACTION_ATTEMPTS,
|
|
611
|
+
);
|
|
612
|
+
const retryDelayMs = Math.max(
|
|
613
|
+
0,
|
|
614
|
+
options.retryDelayMs ?? TELEGRAM_LOCK_TRANSACTION_RETRY_DELAY_MS,
|
|
615
|
+
);
|
|
616
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
617
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
618
|
+
try {
|
|
619
|
+
return createLockTransactionGuard(path, options);
|
|
620
|
+
} catch (error) {
|
|
621
|
+
if (!isLockTransactionContentionError(error)) throw error;
|
|
622
|
+
const recoveredOwner = recoverAbandonedLockTransaction(path, options);
|
|
623
|
+
if (recoveredOwner !== undefined) return recoveredOwner;
|
|
624
|
+
if (attempt === attempts - 1) {
|
|
625
|
+
throw new Error(
|
|
626
|
+
`Timed out acquiring Telegram lock transaction: ${path}`,
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
sleepSync(retryDelayMs);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
throw new Error(`Failed to acquire Telegram lock transaction: ${path}`);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export function withTelegramFileTransaction<T>(
|
|
636
|
+
transactionPath: string,
|
|
637
|
+
operation: () => T,
|
|
638
|
+
options: TelegramFileTransactionOptions = {},
|
|
639
|
+
): T {
|
|
640
|
+
const owner = acquireLockTransaction(transactionPath, options);
|
|
641
|
+
try {
|
|
642
|
+
return operation();
|
|
643
|
+
} finally {
|
|
644
|
+
releaseLockTransactionGuard(transactionPath, owner);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function withLockTransaction<T>(
|
|
649
|
+
locksPath: string,
|
|
650
|
+
mutate: (locks: Record<string, unknown>) => {
|
|
651
|
+
result: T;
|
|
652
|
+
changed: boolean;
|
|
653
|
+
},
|
|
654
|
+
): T {
|
|
655
|
+
return withTelegramFileTransaction(`${locksPath}.transaction`, () => {
|
|
656
|
+
const locks = readLocksForTransaction(locksPath);
|
|
657
|
+
const outcome = mutate(locks);
|
|
658
|
+
if (outcome.changed) writeLocks(locksPath, locks);
|
|
659
|
+
return outcome.result;
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
export function writeLocks(path: string, locks: Record<string, unknown>): void {
|
|
664
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
665
|
+
const payload = `${JSON.stringify(locks, null, 2)}\n`;
|
|
666
|
+
let lastError: unknown;
|
|
667
|
+
for (
|
|
668
|
+
let attempt = 0;
|
|
669
|
+
attempt < TELEGRAM_LOCK_WRITE_RETRY_ATTEMPTS;
|
|
670
|
+
attempt += 1
|
|
671
|
+
) {
|
|
672
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.${attempt}.tmp`;
|
|
673
|
+
try {
|
|
674
|
+
writeFileSync(tempPath, payload, {
|
|
675
|
+
encoding: "utf8",
|
|
676
|
+
mode: 0o600,
|
|
677
|
+
});
|
|
678
|
+
renameSync(tempPath, path);
|
|
679
|
+
return;
|
|
680
|
+
} catch (error) {
|
|
681
|
+
lastError = error;
|
|
682
|
+
try {
|
|
683
|
+
unlinkSync(tempPath);
|
|
684
|
+
} catch {
|
|
685
|
+
/* best effort */
|
|
686
|
+
}
|
|
687
|
+
if (
|
|
688
|
+
!isRetryableLockWriteError(error) ||
|
|
689
|
+
attempt === TELEGRAM_LOCK_WRITE_RETRY_ATTEMPTS - 1
|
|
690
|
+
) {
|
|
691
|
+
throw error;
|
|
692
|
+
}
|
|
693
|
+
sleepSync(TELEGRAM_LOCK_WRITE_RETRY_DELAY_MS * (attempt + 1));
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
throw lastError;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
export function parseTelegramLockEntry(
|
|
700
|
+
value: unknown,
|
|
701
|
+
): TelegramLockEntry | undefined {
|
|
702
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
703
|
+
return undefined;
|
|
704
|
+
const record = value as Record<string, unknown>;
|
|
705
|
+
if (typeof record.pid !== "number") return undefined;
|
|
706
|
+
return {
|
|
707
|
+
pid: record.pid,
|
|
708
|
+
cwd: typeof record.cwd === "string" ? record.cwd : undefined,
|
|
709
|
+
instanceId:
|
|
710
|
+
typeof record.instanceId === "string" ? record.instanceId : undefined,
|
|
711
|
+
heartbeatMs:
|
|
712
|
+
typeof record.heartbeatMs === "number" ? record.heartbeatMs : undefined,
|
|
713
|
+
leaderEpoch:
|
|
714
|
+
typeof record.leaderEpoch === "number" ||
|
|
715
|
+
typeof record.leaderEpoch === "string"
|
|
716
|
+
? record.leaderEpoch
|
|
717
|
+
: undefined,
|
|
718
|
+
runtimeGeneration:
|
|
719
|
+
typeof record.runtimeGeneration === "number"
|
|
720
|
+
? record.runtimeGeneration
|
|
721
|
+
: undefined,
|
|
722
|
+
busSocketPath:
|
|
723
|
+
typeof record.busSocketPath === "string"
|
|
724
|
+
? record.busSocketPath
|
|
725
|
+
: undefined,
|
|
726
|
+
busSecret:
|
|
727
|
+
typeof record.busSecret === "string" ? record.busSecret : undefined,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
export function isProcessAlive(pid: number): boolean {
|
|
732
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
733
|
+
try {
|
|
734
|
+
process.kill(pid, 0);
|
|
735
|
+
return true;
|
|
736
|
+
} catch (error) {
|
|
737
|
+
return (error as { code?: string }).code === "EPERM";
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
export function formatTelegramLockEntry(lock: TelegramLockEntry): string {
|
|
742
|
+
return lock.cwd ? `pid ${lock.pid}, cwd ${lock.cwd}` : `pid ${lock.pid}`;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function formatTelegramFollowerRegistrationFailure(message: string): string {
|
|
746
|
+
if (/\b(?:ENOENT|ECONNREFUSED|ETIMEDOUT)\b/u.test(message)) {
|
|
747
|
+
return (
|
|
748
|
+
`live owner / unreachable bus endpoint after bounded retries (${message}); ` +
|
|
749
|
+
"wait briefly for owner recovery, then retry /telegram-connect. " +
|
|
750
|
+
"Do not force takeover while the owner remains live"
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
return message;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function getLockState(
|
|
757
|
+
lock: TelegramLockEntry | undefined,
|
|
758
|
+
pid: number,
|
|
759
|
+
isAlive: (pid: number) => boolean,
|
|
760
|
+
options: { nowMs?: number; staleHeartbeatMs?: number } = {},
|
|
761
|
+
): TelegramLockState {
|
|
762
|
+
if (!lock) return { kind: "inactive" };
|
|
763
|
+
if (lock.pid === pid) return { kind: "active-here", lock };
|
|
764
|
+
if (
|
|
765
|
+
typeof lock.heartbeatMs === "number" &&
|
|
766
|
+
typeof options.nowMs === "number" &&
|
|
767
|
+
typeof options.staleHeartbeatMs === "number" &&
|
|
768
|
+
options.nowMs - lock.heartbeatMs > options.staleHeartbeatMs
|
|
769
|
+
) {
|
|
770
|
+
return { kind: "stale", lock };
|
|
771
|
+
}
|
|
772
|
+
if (isAlive(lock.pid)) return { kind: "active-elsewhere", lock };
|
|
773
|
+
return { kind: "stale", lock };
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function ownsLockContext(
|
|
777
|
+
lock: TelegramLockEntry | undefined,
|
|
778
|
+
pid: number,
|
|
779
|
+
ctx?: TelegramLockContext,
|
|
780
|
+
): boolean {
|
|
781
|
+
if (!lock || lock.pid !== pid) return false;
|
|
782
|
+
return !lock.cwd || !ctx || lock.cwd === ctx.cwd;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function hasSameLockOwner(
|
|
786
|
+
current: TelegramLockEntry | undefined,
|
|
787
|
+
expected: TelegramLockEntry | undefined,
|
|
788
|
+
): boolean {
|
|
789
|
+
if (!current || !expected) return false;
|
|
790
|
+
return (
|
|
791
|
+
current.pid === expected.pid &&
|
|
792
|
+
current.cwd === expected.cwd &&
|
|
793
|
+
current.instanceId === expected.instanceId &&
|
|
794
|
+
current.leaderEpoch === expected.leaderEpoch &&
|
|
795
|
+
current.runtimeGeneration === expected.runtimeGeneration
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function canSupersedeSameProcessOwner(
|
|
800
|
+
current: TelegramLockEntry,
|
|
801
|
+
pid: number,
|
|
802
|
+
ctx: TelegramLockContext,
|
|
803
|
+
instanceId: string | undefined,
|
|
804
|
+
runtimeGeneration: number,
|
|
805
|
+
): boolean {
|
|
806
|
+
if (
|
|
807
|
+
current.pid !== pid ||
|
|
808
|
+
(current.cwd !== undefined && current.cwd !== ctx.cwd) ||
|
|
809
|
+
!instanceId
|
|
810
|
+
) {
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
return (
|
|
814
|
+
current.runtimeGeneration === undefined ||
|
|
815
|
+
runtimeGeneration > current.runtimeGeneration
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function createLockEntry(
|
|
820
|
+
pid: number,
|
|
821
|
+
ctx: TelegramLockContext,
|
|
822
|
+
options: {
|
|
823
|
+
instanceId?: string;
|
|
824
|
+
busSocketPath?: string;
|
|
825
|
+
busSecret?: string;
|
|
826
|
+
getNowMs?: () => number;
|
|
827
|
+
mintLeaderEpoch?: () => number | string;
|
|
828
|
+
runtimeGeneration?: number;
|
|
829
|
+
},
|
|
830
|
+
): TelegramLockEntry {
|
|
831
|
+
const lock: TelegramLockEntry = { pid, cwd: ctx.cwd };
|
|
832
|
+
if (options.instanceId) {
|
|
833
|
+
const nowMs = options.getNowMs?.();
|
|
834
|
+
lock.instanceId = options.instanceId;
|
|
835
|
+
lock.heartbeatMs = nowMs;
|
|
836
|
+
lock.leaderEpoch = options.mintLeaderEpoch?.() ?? randomUUID();
|
|
837
|
+
lock.runtimeGeneration = options.runtimeGeneration;
|
|
838
|
+
}
|
|
839
|
+
if (options.busSocketPath) lock.busSocketPath = options.busSocketPath;
|
|
840
|
+
if (options.busSecret) lock.busSecret = options.busSecret;
|
|
841
|
+
return lock;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function formatLockState(state: TelegramLockState): string {
|
|
845
|
+
switch (state.kind) {
|
|
846
|
+
case "inactive":
|
|
847
|
+
return "inactive";
|
|
848
|
+
case "active-here":
|
|
849
|
+
return "active here";
|
|
850
|
+
case "active-elsewhere":
|
|
851
|
+
return `active elsewhere (${formatTelegramLockEntry(state.lock)})`;
|
|
852
|
+
case "stale":
|
|
853
|
+
return `stale (${formatTelegramLockEntry(state.lock)})`;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
|
|
858
|
+
options: TelegramLockRuntimeOptions = {},
|
|
859
|
+
): TelegramLockRuntime<TContext> {
|
|
860
|
+
const key = options.key ?? TELEGRAM_LOCK_KEY;
|
|
861
|
+
const locksPath = options.locksPath ?? getOwnersPath();
|
|
862
|
+
const pid = options.pid ?? process.pid;
|
|
863
|
+
const isAlive = options.isProcessAlive ?? isProcessAlive;
|
|
864
|
+
const getNowMs = options.getNowMs ?? Date.now;
|
|
865
|
+
const runtimeGeneration =
|
|
866
|
+
options.runtimeGeneration ?? allocateTelegramLockRuntimeGeneration();
|
|
867
|
+
let ownedLockKey: string | undefined;
|
|
868
|
+
let ownedLock: TelegramLockEntry | undefined;
|
|
869
|
+
const stateOptions = () => ({
|
|
870
|
+
nowMs: getNowMs(),
|
|
871
|
+
staleHeartbeatMs: options.staleHeartbeatMs,
|
|
872
|
+
});
|
|
873
|
+
const resolveEffectiveKey = (): string => {
|
|
874
|
+
if (typeof key === "function") return key() || TELEGRAM_LOCK_KEY;
|
|
875
|
+
return key;
|
|
876
|
+
};
|
|
877
|
+
const readLock = () => {
|
|
878
|
+
const effectiveKey = resolveEffectiveKey();
|
|
879
|
+
return parseTelegramLockEntry(readLocks(locksPath)[effectiveKey]);
|
|
880
|
+
};
|
|
881
|
+
const adoptCompatibleOwnedLock = (
|
|
882
|
+
effectiveKey: string,
|
|
883
|
+
lock: TelegramLockEntry | undefined,
|
|
884
|
+
ctx?: TelegramLockContext,
|
|
885
|
+
): TelegramLockEntry | undefined => {
|
|
886
|
+
if (ownedLock) {
|
|
887
|
+
return ownedLockKey === effectiveKey ? ownedLock : undefined;
|
|
888
|
+
}
|
|
889
|
+
if (!ownsLockContext(lock, pid, ctx)) return undefined;
|
|
890
|
+
if (
|
|
891
|
+
(lock?.instanceId !== undefined &&
|
|
892
|
+
lock.instanceId !== options.instanceId) ||
|
|
893
|
+
(lock?.runtimeGeneration !== undefined &&
|
|
894
|
+
lock.runtimeGeneration !== runtimeGeneration)
|
|
895
|
+
) {
|
|
896
|
+
return undefined;
|
|
897
|
+
}
|
|
898
|
+
ownedLockKey = effectiveKey;
|
|
899
|
+
ownedLock = lock;
|
|
900
|
+
return ownedLock;
|
|
901
|
+
};
|
|
902
|
+
return {
|
|
903
|
+
acquire: (ctx, acquireOptions = {}) =>
|
|
904
|
+
withLockTransaction<TelegramLockAcquireResult>(locksPath, (locks) => {
|
|
905
|
+
const effectiveKey = resolveEffectiveKey();
|
|
906
|
+
const current = parseTelegramLockEntry(locks[effectiveKey]);
|
|
907
|
+
const state = getLockState(current, pid, isAlive, stateOptions());
|
|
908
|
+
const expectedOwned = adoptCompatibleOwnedLock(
|
|
909
|
+
effectiveKey,
|
|
910
|
+
current,
|
|
911
|
+
ctx,
|
|
912
|
+
);
|
|
913
|
+
if (
|
|
914
|
+
state.kind === "active-here" &&
|
|
915
|
+
hasSameLockOwner(current, expectedOwned)
|
|
916
|
+
) {
|
|
917
|
+
return {
|
|
918
|
+
result: {
|
|
919
|
+
ok: true,
|
|
920
|
+
lock: current!,
|
|
921
|
+
replacedStale: false,
|
|
922
|
+
} as const,
|
|
923
|
+
changed: false,
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
if (acquireOptions.election && current) {
|
|
927
|
+
if (
|
|
928
|
+
state.kind !== "stale" ||
|
|
929
|
+
!hasSameLockOwner(current, acquireOptions.expectedOwner)
|
|
930
|
+
) {
|
|
931
|
+
return {
|
|
932
|
+
result: { ok: false, lock: current } as const,
|
|
933
|
+
changed: false,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
const expectedReplacementMatches = hasSameLockOwner(
|
|
938
|
+
state.kind === "active-here" || state.kind === "active-elsewhere"
|
|
939
|
+
? state.lock
|
|
940
|
+
: undefined,
|
|
941
|
+
acquireOptions.expectedOwner,
|
|
942
|
+
);
|
|
943
|
+
const canReplaceCurrent =
|
|
944
|
+
state.kind === "active-elsewhere" ||
|
|
945
|
+
(state.kind === "active-here" &&
|
|
946
|
+
canSupersedeSameProcessOwner(
|
|
947
|
+
state.lock,
|
|
948
|
+
pid,
|
|
949
|
+
ctx,
|
|
950
|
+
options.instanceId,
|
|
951
|
+
runtimeGeneration,
|
|
952
|
+
));
|
|
953
|
+
if (
|
|
954
|
+
!acquireOptions.election &&
|
|
955
|
+
(state.kind === "active-here" || state.kind === "active-elsewhere") &&
|
|
956
|
+
(!acquireOptions.force ||
|
|
957
|
+
!expectedReplacementMatches ||
|
|
958
|
+
!canReplaceCurrent)
|
|
959
|
+
) {
|
|
960
|
+
return {
|
|
961
|
+
result: { ok: false, lock: state.lock } as const,
|
|
962
|
+
changed: false,
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
const lock = createLockEntry(pid, ctx, {
|
|
966
|
+
instanceId: options.instanceId,
|
|
967
|
+
busSocketPath: options.busSocketPath,
|
|
968
|
+
busSecret: options.busSecret,
|
|
969
|
+
getNowMs,
|
|
970
|
+
mintLeaderEpoch: options.mintLeaderEpoch,
|
|
971
|
+
runtimeGeneration,
|
|
972
|
+
});
|
|
973
|
+
locks[effectiveKey] = lock;
|
|
974
|
+
ownedLockKey = effectiveKey;
|
|
975
|
+
ownedLock = lock;
|
|
976
|
+
return {
|
|
977
|
+
result: {
|
|
978
|
+
ok: true,
|
|
979
|
+
lock,
|
|
980
|
+
replacedStale: state.kind === "stale",
|
|
981
|
+
} as const,
|
|
982
|
+
changed: true,
|
|
983
|
+
};
|
|
984
|
+
}),
|
|
985
|
+
release: () =>
|
|
986
|
+
withLockTransaction(locksPath, (locks) => {
|
|
987
|
+
const effectiveKey = resolveEffectiveKey();
|
|
988
|
+
const state = getLockState(
|
|
989
|
+
parseTelegramLockEntry(locks[effectiveKey]),
|
|
990
|
+
pid,
|
|
991
|
+
isAlive,
|
|
992
|
+
stateOptions(),
|
|
993
|
+
);
|
|
994
|
+
const changed =
|
|
995
|
+
ownedLockKey === effectiveKey &&
|
|
996
|
+
hasSameLockOwner(
|
|
997
|
+
parseTelegramLockEntry(locks[effectiveKey]),
|
|
998
|
+
ownedLock,
|
|
999
|
+
);
|
|
1000
|
+
if (changed) {
|
|
1001
|
+
delete locks[effectiveKey];
|
|
1002
|
+
ownedLockKey = undefined;
|
|
1003
|
+
ownedLock = undefined;
|
|
1004
|
+
}
|
|
1005
|
+
return { result: state, changed };
|
|
1006
|
+
}),
|
|
1007
|
+
getState: () => getLockState(readLock(), pid, isAlive, stateOptions()),
|
|
1008
|
+
getStatusLabel: () =>
|
|
1009
|
+
formatLockState(getLockState(readLock(), pid, isAlive, stateOptions())),
|
|
1010
|
+
getOwnedLeaderEpoch: () => {
|
|
1011
|
+
const effectiveKey = resolveEffectiveKey();
|
|
1012
|
+
const lock = parseTelegramLockEntry(readLocks(locksPath)[effectiveKey]);
|
|
1013
|
+
const exactOwner = adoptCompatibleOwnedLock(effectiveKey, lock);
|
|
1014
|
+
return hasSameLockOwner(lock, exactOwner) ? lock?.leaderEpoch : undefined;
|
|
1015
|
+
},
|
|
1016
|
+
owns: (ctx) => {
|
|
1017
|
+
const effectiveKey = resolveEffectiveKey();
|
|
1018
|
+
const lock = parseTelegramLockEntry(readLocks(locksPath)[effectiveKey]);
|
|
1019
|
+
return hasSameLockOwner(
|
|
1020
|
+
lock,
|
|
1021
|
+
adoptCompatibleOwnedLock(effectiveKey, lock, ctx),
|
|
1022
|
+
);
|
|
1023
|
+
},
|
|
1024
|
+
commitIfOwned: (commit) =>
|
|
1025
|
+
withLockTransaction(locksPath, (locks) => {
|
|
1026
|
+
const effectiveKey = resolveEffectiveKey();
|
|
1027
|
+
const lock = parseTelegramLockEntry(locks[effectiveKey]);
|
|
1028
|
+
const exactOwner =
|
|
1029
|
+
ownedLockKey === effectiveKey && hasSameLockOwner(lock, ownedLock);
|
|
1030
|
+
if (!exactOwner) {
|
|
1031
|
+
if (ownedLockKey === effectiveKey) {
|
|
1032
|
+
ownedLockKey = undefined;
|
|
1033
|
+
ownedLock = undefined;
|
|
1034
|
+
}
|
|
1035
|
+
return { result: false, changed: false };
|
|
1036
|
+
}
|
|
1037
|
+
commit();
|
|
1038
|
+
return { result: true, changed: false };
|
|
1039
|
+
}),
|
|
1040
|
+
refresh: (ctx) =>
|
|
1041
|
+
withLockTransaction(locksPath, (locks) => {
|
|
1042
|
+
const effectiveKey = resolveEffectiveKey();
|
|
1043
|
+
const lock = parseTelegramLockEntry(locks[effectiveKey]);
|
|
1044
|
+
const expectedOwner = adoptCompatibleOwnedLock(effectiveKey, lock, ctx);
|
|
1045
|
+
if (!lock || !hasSameLockOwner(lock, expectedOwner)) {
|
|
1046
|
+
if (ownedLockKey === effectiveKey) {
|
|
1047
|
+
ownedLockKey = undefined;
|
|
1048
|
+
ownedLock = undefined;
|
|
1049
|
+
}
|
|
1050
|
+
return { result: false, changed: false };
|
|
1051
|
+
}
|
|
1052
|
+
if (!options.instanceId) return { result: true, changed: false };
|
|
1053
|
+
const refreshedLock: TelegramLockEntry = {
|
|
1054
|
+
pid: lock.pid,
|
|
1055
|
+
...(lock.cwd ? { cwd: lock.cwd } : {}),
|
|
1056
|
+
instanceId: options.instanceId,
|
|
1057
|
+
heartbeatMs: getNowMs(),
|
|
1058
|
+
leaderEpoch:
|
|
1059
|
+
lock.leaderEpoch ?? options.mintLeaderEpoch?.() ?? randomUUID(),
|
|
1060
|
+
runtimeGeneration: lock.runtimeGeneration ?? runtimeGeneration,
|
|
1061
|
+
...(options.busSocketPath
|
|
1062
|
+
? { busSocketPath: options.busSocketPath }
|
|
1063
|
+
: {}),
|
|
1064
|
+
busSecret: options.busSecret ?? lock.busSecret,
|
|
1065
|
+
};
|
|
1066
|
+
locks[effectiveKey] = refreshedLock;
|
|
1067
|
+
ownedLockKey = effectiveKey;
|
|
1068
|
+
ownedLock = refreshedLock;
|
|
1069
|
+
return { result: true, changed: true };
|
|
1070
|
+
}),
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
export function createTelegramLockOwnershipGuard<
|
|
1075
|
+
TContext extends TelegramLockContext,
|
|
1076
|
+
>(lock: TelegramLockRuntime<TContext>): TelegramLockOwnershipGuard<TContext> {
|
|
1077
|
+
return {
|
|
1078
|
+
ownsContext: (ctx) => lock.owns(ctx),
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
export function createTelegramDirectDeliveryOwnershipChecker<
|
|
1083
|
+
TContext extends TelegramLockContext,
|
|
1084
|
+
>(deps: {
|
|
1085
|
+
lock: TelegramLockRuntime<TContext>;
|
|
1086
|
+
contextStore: TelegramLockContextStore<TContext>;
|
|
1087
|
+
}): () => boolean {
|
|
1088
|
+
return () => {
|
|
1089
|
+
const ctx = deps.contextStore.get();
|
|
1090
|
+
return ctx ? deps.lock.owns(ctx) : false;
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
export interface TelegramLockedPollingStartOptions {
|
|
1095
|
+
force?: boolean;
|
|
1096
|
+
forceFreshLeaderThread?: boolean;
|
|
1097
|
+
election?: { expectedOwner?: TelegramLockEntry };
|
|
1098
|
+
onAcquired?: () => Promise<void> | void;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
export type TelegramLockedPollingStartResult =
|
|
1102
|
+
| { ok: true; message?: string; canTakeover?: false }
|
|
1103
|
+
| { ok: false; message: string; canTakeover?: boolean; owner?: string };
|
|
1104
|
+
|
|
1105
|
+
export interface TelegramLockedPollingRuntime<
|
|
1106
|
+
TContext extends TelegramLockContext,
|
|
1107
|
+
> {
|
|
1108
|
+
start: (
|
|
1109
|
+
ctx: TContext,
|
|
1110
|
+
options?: TelegramLockedPollingStartOptions,
|
|
1111
|
+
) => Promise<TelegramLockedPollingStartResult>;
|
|
1112
|
+
stop: () => Promise<string>;
|
|
1113
|
+
suspend: () => Promise<void>;
|
|
1114
|
+
onSessionStart: (_event: unknown, ctx: TContext) => Promise<void>;
|
|
1115
|
+
registerFollowerWithOwner?: (
|
|
1116
|
+
ctx: TContext,
|
|
1117
|
+
owner: TelegramLockEntry,
|
|
1118
|
+
) => boolean | undefined | Promise<boolean | undefined>;
|
|
1119
|
+
stopFollowerRegistration?: () => void;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
export interface TelegramLockedPollingRuntimeDeps<
|
|
1123
|
+
TContext extends TelegramLockContext,
|
|
1124
|
+
> {
|
|
1125
|
+
lock: TelegramLockRuntime<TContext>;
|
|
1126
|
+
hasBotToken: () => boolean;
|
|
1127
|
+
canStartPolling?: (ctx: TContext) => boolean;
|
|
1128
|
+
formatStartBlockedMessage?: (ctx: TContext) => string;
|
|
1129
|
+
startPolling: (
|
|
1130
|
+
ctx: TContext,
|
|
1131
|
+
options?: TelegramLockedPollingStartOptions,
|
|
1132
|
+
) => void | Promise<void>;
|
|
1133
|
+
stopPolling: () => Promise<void>;
|
|
1134
|
+
registerFollowerWithOwner?: (
|
|
1135
|
+
ctx: TContext,
|
|
1136
|
+
owner: TelegramLockEntry,
|
|
1137
|
+
) => boolean | undefined | Promise<boolean | undefined>;
|
|
1138
|
+
stopFollowerRegistration?: () => void;
|
|
1139
|
+
onTransportAvailabilityChanged?: () => void;
|
|
1140
|
+
updateStatus: (ctx: TContext) => void;
|
|
1141
|
+
recordRuntimeEvent?: (
|
|
1142
|
+
category: string,
|
|
1143
|
+
error: unknown,
|
|
1144
|
+
details?: Record<string, unknown>,
|
|
1145
|
+
) => void;
|
|
1146
|
+
ownershipCheckMs?: number;
|
|
1147
|
+
ownershipRefreshMs?: number;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function snapshotLockContext(ctx: TelegramLockContext): TelegramLockContext {
|
|
1151
|
+
return { cwd: ctx.cwd };
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
export function createTelegramLockedPollingRuntime<
|
|
1155
|
+
TContext extends TelegramLockContext,
|
|
1156
|
+
>(
|
|
1157
|
+
deps: TelegramLockedPollingRuntimeDeps<TContext>,
|
|
1158
|
+
): TelegramLockedPollingRuntime<TContext> {
|
|
1159
|
+
let ownershipCheckInterval: ReturnType<typeof setInterval> | undefined;
|
|
1160
|
+
let ownershipRefreshInterval: ReturnType<typeof setInterval> | undefined;
|
|
1161
|
+
let ownershipStop: Promise<void> | undefined;
|
|
1162
|
+
let takeoverCandidate: TelegramLockEntry | undefined;
|
|
1163
|
+
let sessionAutoStartRun: Promise<void> | undefined;
|
|
1164
|
+
let sessionAutoStartGeneration = 0;
|
|
1165
|
+
const ownershipCheckMs =
|
|
1166
|
+
deps.ownershipCheckMs ?? TELEGRAM_OWNERSHIP_CHECK_MS;
|
|
1167
|
+
const ownershipRefreshMs =
|
|
1168
|
+
deps.ownershipRefreshMs ?? TELEGRAM_OWNERSHIP_REFRESH_MS;
|
|
1169
|
+
const stopOwnershipWatcher = () => {
|
|
1170
|
+
if (ownershipCheckInterval) clearInterval(ownershipCheckInterval);
|
|
1171
|
+
if (ownershipRefreshInterval) clearInterval(ownershipRefreshInterval);
|
|
1172
|
+
ownershipCheckInterval = undefined;
|
|
1173
|
+
ownershipRefreshInterval = undefined;
|
|
1174
|
+
};
|
|
1175
|
+
const suspendPolling = async () => {
|
|
1176
|
+
sessionAutoStartGeneration += 1;
|
|
1177
|
+
deps.stopFollowerRegistration?.();
|
|
1178
|
+
stopOwnershipWatcher();
|
|
1179
|
+
if (sessionAutoStartRun) {
|
|
1180
|
+
await sessionAutoStartRun;
|
|
1181
|
+
}
|
|
1182
|
+
if (ownershipStop) {
|
|
1183
|
+
await ownershipStop;
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
await deps.stopPolling();
|
|
1187
|
+
};
|
|
1188
|
+
const stopAfterOwnershipLoss = () => {
|
|
1189
|
+
if (ownershipStop) return;
|
|
1190
|
+
stopOwnershipWatcher();
|
|
1191
|
+
deps.onTransportAvailabilityChanged?.();
|
|
1192
|
+
ownershipStop = deps
|
|
1193
|
+
.stopPolling()
|
|
1194
|
+
.catch((error) =>
|
|
1195
|
+
deps.recordRuntimeEvent?.("lock", error, { phase: "ownership-loss" }),
|
|
1196
|
+
)
|
|
1197
|
+
.finally(() => {
|
|
1198
|
+
ownershipStop = undefined;
|
|
1199
|
+
});
|
|
1200
|
+
};
|
|
1201
|
+
const startOwnershipWatcher = (ctx: TContext) => {
|
|
1202
|
+
const owner = snapshotLockContext(ctx);
|
|
1203
|
+
stopOwnershipWatcher();
|
|
1204
|
+
ownershipCheckInterval = setInterval(() => {
|
|
1205
|
+
try {
|
|
1206
|
+
if (deps.lock.owns(owner)) return;
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
deps.recordRuntimeEvent?.("lock", error, { phase: "check" });
|
|
1209
|
+
}
|
|
1210
|
+
stopAfterOwnershipLoss();
|
|
1211
|
+
}, ownershipCheckMs);
|
|
1212
|
+
ownershipRefreshInterval = setInterval(() => {
|
|
1213
|
+
try {
|
|
1214
|
+
if (deps.lock.refresh(owner)) return;
|
|
1215
|
+
} catch (error) {
|
|
1216
|
+
deps.recordRuntimeEvent?.("lock", error, { phase: "refresh" });
|
|
1217
|
+
}
|
|
1218
|
+
stopAfterOwnershipLoss();
|
|
1219
|
+
}, ownershipRefreshMs);
|
|
1220
|
+
ownershipCheckInterval.unref?.();
|
|
1221
|
+
ownershipRefreshInterval.unref?.();
|
|
1222
|
+
};
|
|
1223
|
+
const runOwnedPollingStart = async (
|
|
1224
|
+
ctx: TContext,
|
|
1225
|
+
options: TelegramLockedPollingStartOptions,
|
|
1226
|
+
): Promise<boolean> => {
|
|
1227
|
+
startOwnershipWatcher(ctx);
|
|
1228
|
+
try {
|
|
1229
|
+
if (!deps.lock.refresh(snapshotLockContext(ctx))) {
|
|
1230
|
+
stopOwnershipWatcher();
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
await options.onAcquired?.();
|
|
1234
|
+
await deps.startPolling(ctx, options);
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
stopOwnershipWatcher();
|
|
1237
|
+
try {
|
|
1238
|
+
await deps.stopPolling();
|
|
1239
|
+
} catch (stopError) {
|
|
1240
|
+
deps.recordRuntimeEvent?.("lock", stopError, {
|
|
1241
|
+
phase: "startup-rollback",
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
deps.lock.release();
|
|
1245
|
+
deps.onTransportAvailabilityChanged?.();
|
|
1246
|
+
throw error;
|
|
1247
|
+
}
|
|
1248
|
+
if (deps.lock.owns(ctx)) return true;
|
|
1249
|
+
stopOwnershipWatcher();
|
|
1250
|
+
if (ownershipStop) await ownershipStop;
|
|
1251
|
+
await deps.stopPolling();
|
|
1252
|
+
deps.onTransportAvailabilityChanged?.();
|
|
1253
|
+
return false;
|
|
1254
|
+
};
|
|
1255
|
+
const canStartPolling = (ctx: TContext): boolean =>
|
|
1256
|
+
deps.canStartPolling?.(ctx) ?? true;
|
|
1257
|
+
const formatStartBlockedMessage = (ctx: TContext): string =>
|
|
1258
|
+
deps.formatStartBlockedMessage?.(ctx) ??
|
|
1259
|
+
"Telegram polling is unavailable in this Pi run mode.";
|
|
1260
|
+
return {
|
|
1261
|
+
start: async (ctx, options = {}) => {
|
|
1262
|
+
if (!deps.hasBotToken()) {
|
|
1263
|
+
return { ok: false, message: "Telegram bot is not configured." };
|
|
1264
|
+
}
|
|
1265
|
+
if (!canStartPolling(ctx)) {
|
|
1266
|
+
return { ok: false, message: formatStartBlockedMessage(ctx) };
|
|
1267
|
+
}
|
|
1268
|
+
let acquired = deps.lock.acquire(ctx, {
|
|
1269
|
+
force: options.force,
|
|
1270
|
+
expectedOwner:
|
|
1271
|
+
options.election?.expectedOwner ??
|
|
1272
|
+
(options.force ? takeoverCandidate : undefined),
|
|
1273
|
+
election: options.election !== undefined,
|
|
1274
|
+
});
|
|
1275
|
+
if (!acquired.ok && !options.election) {
|
|
1276
|
+
const currentState = deps.lock.getState();
|
|
1277
|
+
if (
|
|
1278
|
+
currentState.kind === "active-here" &&
|
|
1279
|
+
hasSameLockOwner(currentState.lock, acquired.lock)
|
|
1280
|
+
) {
|
|
1281
|
+
acquired = deps.lock.acquire(ctx, {
|
|
1282
|
+
force: true,
|
|
1283
|
+
expectedOwner: acquired.lock,
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
if (!acquired.ok) {
|
|
1288
|
+
takeoverCandidate = acquired.lock;
|
|
1289
|
+
if (options.election) {
|
|
1290
|
+
return {
|
|
1291
|
+
ok: false,
|
|
1292
|
+
canTakeover: false,
|
|
1293
|
+
owner: formatTelegramLockEntry(acquired.lock),
|
|
1294
|
+
message: "Telegram leadership election lost to another live owner.",
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
if (deps.registerFollowerWithOwner) {
|
|
1298
|
+
let failureMessage: string | undefined;
|
|
1299
|
+
try {
|
|
1300
|
+
const registered = await deps.registerFollowerWithOwner(
|
|
1301
|
+
ctx,
|
|
1302
|
+
acquired.lock,
|
|
1303
|
+
);
|
|
1304
|
+
if (registered) {
|
|
1305
|
+
deps.updateStatus(ctx);
|
|
1306
|
+
return { ok: true, canTakeover: false };
|
|
1307
|
+
}
|
|
1308
|
+
if (registered === false) failureMessage = "not registered";
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
failureMessage =
|
|
1311
|
+
error instanceof Error ? error.message : String(error);
|
|
1312
|
+
deps.recordRuntimeEvent?.("bus", error, {
|
|
1313
|
+
phase: "follower-register",
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
if (failureMessage) {
|
|
1317
|
+
const owner = formatTelegramLockEntry(acquired.lock);
|
|
1318
|
+
return {
|
|
1319
|
+
ok: false,
|
|
1320
|
+
canTakeover: false,
|
|
1321
|
+
owner,
|
|
1322
|
+
message: `Telegram bridge is active in another Pi instance (${owner}); follower registration failed: ${formatTelegramFollowerRegistrationFailure(failureMessage)}.`,
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
const owner = formatTelegramLockEntry(acquired.lock);
|
|
1327
|
+
return {
|
|
1328
|
+
ok: false,
|
|
1329
|
+
canTakeover: true,
|
|
1330
|
+
owner,
|
|
1331
|
+
message: `Telegram bridge is active in another Pi instance (${owner}).`,
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
takeoverCandidate = undefined;
|
|
1335
|
+
if (!(await runOwnedPollingStart(ctx, options))) {
|
|
1336
|
+
return {
|
|
1337
|
+
ok: false,
|
|
1338
|
+
canTakeover: false,
|
|
1339
|
+
message: "Telegram leadership changed during polling startup.",
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
deps.onTransportAvailabilityChanged?.();
|
|
1343
|
+
deps.updateStatus(ctx);
|
|
1344
|
+
const staleSuffix = acquired.replacedStale ? " Replaced stale lock." : "";
|
|
1345
|
+
return { ok: true, message: `Telegram bridge connected.${staleSuffix}` };
|
|
1346
|
+
},
|
|
1347
|
+
stop: async () => {
|
|
1348
|
+
await suspendPolling();
|
|
1349
|
+
const state = deps.lock.release();
|
|
1350
|
+
deps.onTransportAvailabilityChanged?.();
|
|
1351
|
+
if (state.kind === "active-elsewhere") {
|
|
1352
|
+
return `Telegram bridge is active in another Pi instance (${formatTelegramLockEntry(state.lock)}).`;
|
|
1353
|
+
}
|
|
1354
|
+
if (state.kind === "stale") {
|
|
1355
|
+
return `Removed stale Telegram bridge lock (${formatTelegramLockEntry(state.lock)}).`;
|
|
1356
|
+
}
|
|
1357
|
+
return "Telegram bridge disconnected.";
|
|
1358
|
+
},
|
|
1359
|
+
suspend: suspendPolling,
|
|
1360
|
+
onSessionStart: async (_event, ctx) => {
|
|
1361
|
+
if (!deps.hasBotToken()) return;
|
|
1362
|
+
if (!canStartPolling(ctx)) return;
|
|
1363
|
+
const ownsCurrentLock = deps.lock.owns(ctx);
|
|
1364
|
+
const state = ownsCurrentLock ? undefined : deps.lock.getState();
|
|
1365
|
+
const canResumeStaleSameCwd =
|
|
1366
|
+
state?.kind === "stale" && state.lock.cwd === ctx.cwd;
|
|
1367
|
+
const canHandoffSameProcess =
|
|
1368
|
+
state?.kind === "active-here" &&
|
|
1369
|
+
(!state.lock.cwd || state.lock.cwd === ctx.cwd);
|
|
1370
|
+
if (
|
|
1371
|
+
!ownsCurrentLock &&
|
|
1372
|
+
!canResumeStaleSameCwd &&
|
|
1373
|
+
!canHandoffSameProcess
|
|
1374
|
+
) {
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
sessionAutoStartGeneration += 1;
|
|
1378
|
+
const generation = sessionAutoStartGeneration;
|
|
1379
|
+
const startedAtMs = Date.now();
|
|
1380
|
+
deps.recordRuntimeEvent?.("lock", "Telegram auto-start scheduled", {
|
|
1381
|
+
phase: "auto-start-scheduled",
|
|
1382
|
+
});
|
|
1383
|
+
const run = (async () => {
|
|
1384
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1385
|
+
if (generation !== sessionAutoStartGeneration) return;
|
|
1386
|
+
if (canResumeStaleSameCwd || canHandoffSameProcess) {
|
|
1387
|
+
const acquired = deps.lock.acquire(
|
|
1388
|
+
ctx,
|
|
1389
|
+
canHandoffSameProcess
|
|
1390
|
+
? { force: true, expectedOwner: state?.lock }
|
|
1391
|
+
: undefined,
|
|
1392
|
+
);
|
|
1393
|
+
if (!acquired.ok) return;
|
|
1394
|
+
}
|
|
1395
|
+
if (generation !== sessionAutoStartGeneration) return;
|
|
1396
|
+
if (!(await runOwnedPollingStart(ctx, {}))) return;
|
|
1397
|
+
if (generation !== sessionAutoStartGeneration) return;
|
|
1398
|
+
deps.onTransportAvailabilityChanged?.();
|
|
1399
|
+
deps.updateStatus(ctx);
|
|
1400
|
+
deps.recordRuntimeEvent?.("lock", "Telegram auto-start completed", {
|
|
1401
|
+
phase: "auto-start-complete",
|
|
1402
|
+
durationMs: Date.now() - startedAtMs,
|
|
1403
|
+
});
|
|
1404
|
+
})()
|
|
1405
|
+
.catch((error) => {
|
|
1406
|
+
deps.recordRuntimeEvent?.("lock", error, { phase: "auto-start" });
|
|
1407
|
+
})
|
|
1408
|
+
.finally(() => {
|
|
1409
|
+
if (sessionAutoStartRun === run) sessionAutoStartRun = undefined;
|
|
1410
|
+
});
|
|
1411
|
+
sessionAutoStartRun = run;
|
|
1412
|
+
},
|
|
1413
|
+
registerFollowerWithOwner: deps.registerFollowerWithOwner
|
|
1414
|
+
? async (ctx, owner) => {
|
|
1415
|
+
const registered = await deps.registerFollowerWithOwner?.(ctx, owner);
|
|
1416
|
+
if (registered) deps.updateStatus(ctx);
|
|
1417
|
+
return registered === true;
|
|
1418
|
+
}
|
|
1419
|
+
: undefined,
|
|
1420
|
+
stopFollowerRegistration: deps.stopFollowerRegistration,
|
|
1421
|
+
};
|
|
1422
|
+
}
|