ctrlrelay 0.12.1__tar.gz → 0.12.4__tar.gz
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.
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/CHANGELOG.md +67 -1
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/PKG-INFO +1 -1
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/pyproject.toml +1 -1
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/bridge/server.py +86 -30
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/cli.py +1091 -79
- ctrlrelay-0.12.4/src/ctrlrelay/core/binaries.py +198 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/core/code_review.py +27 -1
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/core/config.py +23 -0
- ctrlrelay-0.12.4/src/ctrlrelay/core/dispatch_health.py +276 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/core/dispatcher.py +215 -21
- ctrlrelay-0.12.4/src/ctrlrelay/core/redact.py +321 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/core/state.py +455 -1
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/pipelines/base.py +12 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/pipelines/dev.py +436 -13
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/pipelines/secops.py +7 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/templates/launchd/bridge.plist.template +9 -1
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/templates/launchd/poller.plist.template +9 -1
- ctrlrelay-0.12.4/src/ctrlrelay/templates/systemd/bridge.service.template +37 -0
- ctrlrelay-0.12.4/src/ctrlrelay/templates/systemd/poller.service.template +42 -0
- ctrlrelay-0.12.4/tests/test_binary_resolution.py +1009 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_bridge_server.py +171 -14
- ctrlrelay-0.12.4/tests/test_claim_reconciliation.py +999 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_config.py +6 -2
- ctrlrelay-0.12.4/tests/test_dispatch_health.py +462 -0
- ctrlrelay-0.12.4/tests/test_dispatch_outcome_hook.py +1512 -0
- ctrlrelay-0.12.4/tests/test_issue_outcome_announcement.py +182 -0
- ctrlrelay-0.12.4/tests/test_notify_transport.py +1238 -0
- ctrlrelay-0.12.4/tests/test_redact.py +512 -0
- ctrlrelay-0.12.4/tests/test_unreviewed_pr_announcement.py +1009 -0
- ctrlrelay-0.12.1/src/ctrlrelay/templates/systemd/bridge.service.template +0 -20
- ctrlrelay-0.12.1/src/ctrlrelay/templates/systemd/poller.service.template +0 -25
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/.github/dependabot.yml +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/.github/workflows/build.yml +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/.github/workflows/cla.yml +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/.github/workflows/pages.yml +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/.github/workflows/publish.yml +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/.github/workflows/test.yml +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/.gitignore +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/CODE_OF_CONDUCT.md +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/CONTRIBUTING.md +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/LICENSE +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/README.md +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/SECURITY.md +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/config/orchestrator.yaml.example +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/docs/Gemfile +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/docs/_config.yml +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/docs/architecture.md +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/docs/bridge.md +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/docs/cli.md +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/docs/configuration.md +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/docs/development.md +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/docs/feedback-loop.md +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/docs/getting-started.md +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/docs/index.md +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/docs/operations.md +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/docs/personalization.md +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/__init__.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/bridge/__init__.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/bridge/__main__.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/bridge/factory.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/bridge/handler.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/bridge/mattermost_handler.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/bridge/protocol.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/core/__init__.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/core/archived.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/core/audit.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/core/checkpoint.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/core/github.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/core/network.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/core/obs.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/core/poller.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/core/pr_verifier.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/core/pr_watcher.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/core/question_expiry.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/core/scheduler.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/core/worktree.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/dashboard/__init__.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/dashboard/client.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/gh_protocol.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/install.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/personalization/__init__.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/personalization/manager.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/personalization/paths.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/pipelines/__init__.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/pipelines/post_merge.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/pipelines/task.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/setup.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/templates/__init__.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/templates/global-CLAUDE.md.snippet +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/templates/launchd/__init__.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/templates/systemd/__init__.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/transports/__init__.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/transports/base.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/transports/file_mock.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/src/ctrlrelay/transports/socket_client.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/__init__.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/conftest.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_archived.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_audit.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_bridge_protocol.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_checkpoint.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_cli_ci_wait.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_cli_dev.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_cli_repos.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_cli_secops.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_cli_start.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_cli_version.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_code_review.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_dashboard_client.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_dev_integration.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_dev_pipeline.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_dispatcher.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_docs_site.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_gh_protocol.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_github.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_install.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_mattermost_handler.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_network.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_obs.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_personalization.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_pipeline_base.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_poller.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_post_merge.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_pr_verifier.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_pr_watcher.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_question_expiry.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_resume_prompt_contract.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_scheduler.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_secops_integration.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_secops_pipeline.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_setup.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_state.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_task_pipeline.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_telegram_handler.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_transport.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_transport_resolution.py +0 -0
- {ctrlrelay-0.12.1 → ctrlrelay-0.12.4}/tests/test_worktree.py +0 -0
|
@@ -7,6 +7,70 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.12.4] - 2026-10-18
|
|
11
|
+
|
|
12
|
+
Carries 0.12.3's entries too: v0.12.3 was never tagged, so nothing has
|
|
13
|
+
shipped since v0.12.2 and this is the first release to reach a running
|
|
14
|
+
daemon with any of it.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Reconcile the claim comment with a failed dispatch, alert on a run of
|
|
19
|
+
failures, and keep the exit code and the worker's output (#206, closes
|
|
20
|
+
#204)
|
|
21
|
+
|
|
22
|
+
The dev lane posted *"a PR will be opened for review"* before the
|
|
23
|
+
worker started and never returned to the issue when the worker
|
|
24
|
+
exited non-zero, so that sentence stood on a beta tester's own ticket
|
|
25
|
+
for eighteen hours. Separately, 163 consecutive failed dispatches over
|
|
26
|
+
two days told nobody while both units stayed `active (running)`.
|
|
27
|
+
Separately again, `sessions.claude_exit_code` was never written on the
|
|
28
|
+
failure path and no worker output was retained, so the only durable
|
|
29
|
+
evidence of why they failed was that each took about two seconds.
|
|
30
|
+
|
|
31
|
+
New: `sessions.output_tail`, the `dispatch_health` table, and an
|
|
32
|
+
`alerts` config block (`alerts.enabled`,
|
|
33
|
+
`alerts.consecutive_dispatch_failures`, default 3). All additive and
|
|
34
|
+
defaulted — no config edit is needed.
|
|
35
|
+
|
|
36
|
+
**Known limits**, stated because they are decisions rather than
|
|
37
|
+
oversights: the relay-health alert is awaited inside the dispatcher,
|
|
38
|
+
so it can hold the per-repo lock for up to ~15 s against a wedged
|
|
39
|
+
bridge; delivery is at-least-once, so an alert that is sent but whose
|
|
40
|
+
delivery cannot be recorded may be sent once more; and the output
|
|
41
|
+
scrubber removes known credential shapes, which is not a proof that
|
|
42
|
+
nothing sensitive remains. A numeric value under a credential key is
|
|
43
|
+
kept, because treating every number as a possible secret destroyed the
|
|
44
|
+
token counts in an API error.
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- Announce a pull request whose review could not run (#202, closes #196)
|
|
49
|
+
|
|
50
|
+
### Fixed (0.12.3, never released)
|
|
51
|
+
|
|
52
|
+
- Resolve the reviewer binary before exec, and ship a unit PATH that finds
|
|
53
|
+
it (#201, closes #197)
|
|
54
|
+
- Resolve the notification transport at use, not at boot (#199, closes #198)
|
|
55
|
+
|
|
56
|
+
## [0.12.3] - 2026-10-12
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- Announce a pull request whose review could not run (#202, closes #196)
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- Resolve the reviewer binary before exec, and ship a unit PATH that finds
|
|
65
|
+
it (#201, closes #197)
|
|
66
|
+
- Resolve the notification transport at use, not at boot (#199, closes #198)
|
|
67
|
+
|
|
68
|
+
## [0.12.2] - 2026-10-06
|
|
69
|
+
|
|
70
|
+
### Fixed
|
|
71
|
+
|
|
72
|
+
- Stay quiet when somebody is talking in the channel (#194)
|
|
73
|
+
|
|
10
74
|
## [0.12.1] - 2026-10-06
|
|
11
75
|
|
|
12
76
|
### Fixed
|
|
@@ -1683,7 +1747,9 @@ pipeline).
|
|
|
1683
1747
|
per-phase implementation plans (Phase 0 through Phase 4).
|
|
1684
1748
|
- `docs/Claude_Code_Project_Guide.md` — project development guide.
|
|
1685
1749
|
|
|
1686
|
-
[Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.12.
|
|
1750
|
+
[Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.12.3...HEAD
|
|
1751
|
+
[0.12.3]: https://github.com/AInvirion/ctrlrelay/compare/v0.12.2...v0.12.3
|
|
1752
|
+
[0.12.2]: https://github.com/AInvirion/ctrlrelay/compare/v0.12.1...v0.12.2
|
|
1687
1753
|
[0.12.1]: https://github.com/AInvirion/ctrlrelay/compare/v0.12.0...v0.12.1
|
|
1688
1754
|
[0.12.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.11.3...v0.12.0
|
|
1689
1755
|
[0.11.3]: https://github.com/AInvirion/ctrlrelay/compare/v0.11.2...v0.11.3
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ctrlrelay
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.4
|
|
4
4
|
Summary: Local-first orchestrator for headless coding agents across multiple GitHub repos
|
|
5
5
|
Project-URL: Homepage, https://github.com/AInvirion/ctrlrelay
|
|
6
6
|
Project-URL: Documentation, https://ainvirion.github.io/ctrlrelay/
|
|
@@ -37,6 +37,35 @@ _log = logging.getLogger(__name__)
|
|
|
37
37
|
_ASKED_SESSIONS_MAX = 500
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
_PENDING_SHOWN = 5
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _format_pending(rows: list[dict]) -> str:
|
|
44
|
+
"""A sample of outstanding questions, never the whole catalogue.
|
|
45
|
+
|
|
46
|
+
Capped because this list is unbounded by nature - one row per blocked
|
|
47
|
+
session, and a config error can block every repo in a sweep. Measured
|
|
48
|
+
on the live channel at 20 pending: 4042 characters, about 202 per
|
|
49
|
+
row. This deployment sweeps 87 repos, and the server's MaxPostSize is
|
|
50
|
+
16383, so at roughly 81 blocked sessions the message exceeded the
|
|
51
|
+
limit and posted NOTHING - the notice whose job is to say a reply was
|
|
52
|
+
dropped, dropped.
|
|
53
|
+
|
|
54
|
+
The count of what is withheld is part of the message, not a detail:
|
|
55
|
+
truncating to "here are five" and not saying five of how many is the
|
|
56
|
+
same information loss one level down.
|
|
57
|
+
"""
|
|
58
|
+
shown = rows[:_PENDING_SHOWN]
|
|
59
|
+
lines = [
|
|
60
|
+
f" • `{r['session_id']}` ({r['repo']}): {(r['question'] or '')[:80]}"
|
|
61
|
+
for r in shown
|
|
62
|
+
]
|
|
63
|
+
rest = len(rows) - len(shown)
|
|
64
|
+
if rest > 0:
|
|
65
|
+
lines.append(f" … and {rest} more.")
|
|
66
|
+
return "Pending:\n" + "\n".join(lines)
|
|
67
|
+
|
|
68
|
+
|
|
40
69
|
def names_session(text: str, session_id: str) -> bool:
|
|
41
70
|
"""True when ``text`` names ``session_id`` as a whole token.
|
|
42
71
|
|
|
@@ -617,7 +646,7 @@ class BridgeServer:
|
|
|
617
646
|
match: _PendingQuestion | None = None
|
|
618
647
|
hinted_session_id: str | None = None
|
|
619
648
|
live_listing: str | None = None
|
|
620
|
-
|
|
649
|
+
unaddressed_pending: int | None = None
|
|
621
650
|
|
|
622
651
|
async with self._pending_lock:
|
|
623
652
|
self._drop_expired_questions()
|
|
@@ -629,20 +658,16 @@ class BridgeServer:
|
|
|
629
658
|
elif len(self._pending_questions) == 1:
|
|
630
659
|
match = next(iter(self._pending_questions.values()))
|
|
631
660
|
elif len(self._pending_questions) > 1:
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
"(Telegram: reply; Mattermost: reply in its thread)."
|
|
639
|
-
"\n\n"
|
|
640
|
-
f"{self._live_question_listing()}"
|
|
641
|
-
)
|
|
661
|
+
# A plain message pointing at nothing, with several
|
|
662
|
+
# questions live. This used to post the whole list; the
|
|
663
|
+
# bridge shares its channel with people, so that made
|
|
664
|
+
# every sentence typed in it produce a wall. Counted,
|
|
665
|
+
# not inferred.
|
|
666
|
+
unaddressed_pending = len(self._pending_questions)
|
|
642
667
|
|
|
643
668
|
if match is not None:
|
|
644
669
|
self._pending_questions.pop(match.request_id, None)
|
|
645
|
-
elif
|
|
670
|
+
elif unaddressed_pending is None:
|
|
646
671
|
# Snapshot what the orphan branch needs, so the lock can be
|
|
647
672
|
# released before we touch the chat app or the database.
|
|
648
673
|
hinted_session_id = (
|
|
@@ -656,10 +681,20 @@ class BridgeServer:
|
|
|
656
681
|
else None
|
|
657
682
|
)
|
|
658
683
|
|
|
659
|
-
if
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
684
|
+
if unaddressed_pending is not None:
|
|
685
|
+
# Silent on purpose. See `_format_pending` for the measurement
|
|
686
|
+
# and the tradeoff: a plain message while several questions
|
|
687
|
+
# are open is ignored, so an answer typed without the reply
|
|
688
|
+
# gesture gets no response. The alternative was a notice on
|
|
689
|
+
# every message in a shared channel, which is what the
|
|
690
|
+
# operator reported.
|
|
691
|
+
log_event(
|
|
692
|
+
_logger,
|
|
693
|
+
"bridge.reply.unaddressed",
|
|
694
|
+
pending=unaddressed_pending,
|
|
695
|
+
live=True,
|
|
696
|
+
text_length=len(text),
|
|
697
|
+
text_hash=hash_text(text),
|
|
663
698
|
)
|
|
664
699
|
return
|
|
665
700
|
|
|
@@ -764,19 +799,27 @@ class BridgeServer:
|
|
|
764
799
|
f"({outcome['reason']})"
|
|
765
800
|
),
|
|
766
801
|
)
|
|
767
|
-
elif outcome["status"] == "
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
802
|
+
elif outcome["status"] == "unaddressed":
|
|
803
|
+
# Somebody is talking in the channel. Say nothing: the bridge
|
|
804
|
+
# does not own this channel and a message that points at no
|
|
805
|
+
# question is not a failed answer. Logged so a genuinely
|
|
806
|
+
# missed answer is still diagnosable.
|
|
807
|
+
log_event(
|
|
808
|
+
_logger,
|
|
809
|
+
"bridge.reply.unaddressed",
|
|
810
|
+
pending=len(outcome["rows"]),
|
|
811
|
+
text_length=len(text),
|
|
812
|
+
text_hash=hash_text(text),
|
|
772
813
|
)
|
|
814
|
+
elif outcome["status"] == "ambiguous":
|
|
815
|
+
# Reached only when the operator DID point at something - they
|
|
816
|
+
# named more than one session id in one message. That is a
|
|
817
|
+
# failed answer and is worth a reply.
|
|
773
818
|
await self._send_notice(
|
|
774
|
-
"⚠️ Your reply wasn't routed —
|
|
775
|
-
"
|
|
776
|
-
"
|
|
777
|
-
|
|
778
|
-
"Reply to the question you mean, or paste "
|
|
779
|
-
"its session_id anywhere in your message."
|
|
819
|
+
"⚠️ Your reply wasn't routed — it names more than one "
|
|
820
|
+
"session.\n\n"
|
|
821
|
+
f"{_format_pending(outcome['rows'])}\n\n"
|
|
822
|
+
"Send one message per session."
|
|
780
823
|
)
|
|
781
824
|
elif live_listing is not None:
|
|
782
825
|
# Reply-to pointed at a message this bridge no longer tracks
|
|
@@ -830,11 +873,16 @@ class BridgeServer:
|
|
|
830
873
|
text.
|
|
831
874
|
|
|
832
875
|
Caller must hold ``_pending_lock``."""
|
|
876
|
+
qs = list(self._pending_questions.values())
|
|
877
|
+
shown = qs[:_PENDING_SHOWN]
|
|
833
878
|
rows = "\n".join(
|
|
834
879
|
f" - {q.repo or 'unknown repo'}"
|
|
835
880
|
+ (f" (session: {q.session_id})" if q.session_id else "")
|
|
836
|
-
for q in
|
|
881
|
+
for q in shown
|
|
837
882
|
)
|
|
883
|
+
rest = len(qs) - len(shown)
|
|
884
|
+
if rest > 0:
|
|
885
|
+
rows += f"\n … and {rest} more."
|
|
838
886
|
return f"Still waiting:\n{rows}"
|
|
839
887
|
|
|
840
888
|
async def _send_notice(self, text: str, log: str | None = None) -> None:
|
|
@@ -938,8 +986,16 @@ class BridgeServer:
|
|
|
938
986
|
elif len(rows) == 1:
|
|
939
987
|
target = rows[0]
|
|
940
988
|
else:
|
|
941
|
-
# Multiple unanswered,
|
|
942
|
-
|
|
989
|
+
# Multiple unanswered, and the message pointed at nothing: no
|
|
990
|
+
# reply-to, no session id. That is not a failed answer, it is
|
|
991
|
+
# somebody talking in the channel.
|
|
992
|
+
#
|
|
993
|
+
# This used to return "ambiguous", which posted every pending
|
|
994
|
+
# session as one block. Measured on the live channel: 4042
|
|
995
|
+
# characters, 29 lines, fired by `ok` and by a message
|
|
996
|
+
# addressed to @here. The operator could not hold a
|
|
997
|
+
# conversation in the channel the bot lives in.
|
|
998
|
+
return {"status": "unaddressed", "rows": rows}
|
|
943
999
|
|
|
944
1000
|
return await self._attach_orphan_answer(target, text)
|
|
945
1001
|
|