ctrlrelay 0.8.0__tar.gz → 0.9.0__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.
Files changed (118) hide show
  1. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/CHANGELOG.md +154 -1
  2. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/PKG-INFO +1 -1
  3. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/config/orchestrator.yaml.example +7 -0
  4. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/docs/configuration.md +2 -0
  5. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/pyproject.toml +1 -1
  6. ctrlrelay-0.9.0/src/ctrlrelay/bridge/server.py +701 -0
  7. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/cli.py +89 -8
  8. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/core/config.py +29 -0
  9. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/core/github.py +106 -48
  10. ctrlrelay-0.9.0/src/ctrlrelay/core/question_expiry.py +193 -0
  11. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/core/state.py +71 -6
  12. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/core/worktree.py +26 -2
  13. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/pipelines/dev.py +71 -0
  14. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/pipelines/secops.py +68 -25
  15. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/setup.py +6 -0
  16. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/transports/__init__.py +1 -0
  17. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/transports/base.py +8 -2
  18. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/transports/file_mock.py +10 -2
  19. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/transports/socket_client.py +14 -2
  20. ctrlrelay-0.9.0/tests/test_bridge_server.py +1067 -0
  21. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_config.py +35 -0
  22. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_dev_pipeline.py +121 -0
  23. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_github.py +152 -27
  24. ctrlrelay-0.9.0/tests/test_question_expiry.py +438 -0
  25. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_secops_pipeline.py +202 -0
  26. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_transport.py +78 -0
  27. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_worktree.py +66 -0
  28. ctrlrelay-0.8.0/src/ctrlrelay/bridge/server.py +0 -421
  29. ctrlrelay-0.8.0/tests/test_bridge_server.py +0 -453
  30. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  31. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  32. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  33. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/.github/dependabot.yml +0 -0
  34. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/.github/workflows/build.yml +0 -0
  35. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/.github/workflows/cla.yml +0 -0
  36. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/.github/workflows/pages.yml +0 -0
  37. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/.github/workflows/publish.yml +0 -0
  38. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/.github/workflows/test.yml +0 -0
  39. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/.gitignore +0 -0
  40. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/CODE_OF_CONDUCT.md +0 -0
  41. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/CONTRIBUTING.md +0 -0
  42. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/LICENSE +0 -0
  43. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/README.md +0 -0
  44. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/SECURITY.md +0 -0
  45. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/docs/Gemfile +0 -0
  46. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/docs/_config.yml +0 -0
  47. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/docs/architecture.md +0 -0
  48. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/docs/bridge.md +0 -0
  49. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/docs/cli.md +0 -0
  50. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/docs/development.md +0 -0
  51. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/docs/feedback-loop.md +0 -0
  52. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/docs/getting-started.md +0 -0
  53. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/docs/index.md +0 -0
  54. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/docs/operations.md +0 -0
  55. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/docs/personalization.md +0 -0
  56. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/__init__.py +0 -0
  57. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/bridge/__init__.py +0 -0
  58. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/bridge/__main__.py +0 -0
  59. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/bridge/protocol.py +0 -0
  60. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
  61. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/core/__init__.py +0 -0
  62. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/core/audit.py +0 -0
  63. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/core/checkpoint.py +0 -0
  64. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/core/dispatcher.py +0 -0
  65. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/core/obs.py +0 -0
  66. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/core/poller.py +0 -0
  67. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/core/pr_verifier.py +0 -0
  68. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/core/pr_watcher.py +0 -0
  69. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/core/scheduler.py +0 -0
  70. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/dashboard/__init__.py +0 -0
  71. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/dashboard/client.py +0 -0
  72. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/gh_protocol.py +0 -0
  73. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/install.py +0 -0
  74. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/personalization/__init__.py +0 -0
  75. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/personalization/manager.py +0 -0
  76. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/personalization/paths.py +0 -0
  77. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/pipelines/__init__.py +0 -0
  78. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/pipelines/base.py +0 -0
  79. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/pipelines/post_merge.py +0 -0
  80. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/pipelines/task.py +0 -0
  81. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/templates/__init__.py +0 -0
  82. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/templates/global-CLAUDE.md.snippet +0 -0
  83. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/templates/launchd/__init__.py +0 -0
  84. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/templates/launchd/bridge.plist.template +0 -0
  85. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/templates/launchd/poller.plist.template +0 -0
  86. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/templates/systemd/__init__.py +0 -0
  87. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/templates/systemd/bridge.service.template +0 -0
  88. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/src/ctrlrelay/templates/systemd/poller.service.template +0 -0
  89. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/__init__.py +0 -0
  90. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/conftest.py +0 -0
  91. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_audit.py +0 -0
  92. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_bridge_protocol.py +0 -0
  93. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_checkpoint.py +0 -0
  94. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_cli_ci_wait.py +0 -0
  95. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_cli_dev.py +0 -0
  96. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_cli_repos.py +0 -0
  97. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_cli_secops.py +0 -0
  98. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_cli_start.py +0 -0
  99. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_cli_version.py +0 -0
  100. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_dashboard_client.py +0 -0
  101. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_dev_integration.py +0 -0
  102. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_dispatcher.py +0 -0
  103. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_docs_site.py +0 -0
  104. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_gh_protocol.py +0 -0
  105. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_install.py +0 -0
  106. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_obs.py +0 -0
  107. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_personalization.py +0 -0
  108. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_pipeline_base.py +0 -0
  109. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_poller.py +0 -0
  110. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_post_merge.py +0 -0
  111. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_pr_verifier.py +0 -0
  112. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_pr_watcher.py +0 -0
  113. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_scheduler.py +0 -0
  114. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_secops_integration.py +0 -0
  115. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_setup.py +0 -0
  116. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_state.py +0 -0
  117. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_task_pipeline.py +0 -0
  118. {ctrlrelay-0.8.0 → ctrlrelay-0.9.0}/tests/test_telegram_handler.py +0 -0
@@ -7,6 +7,157 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.0] - 2026-09-07
11
+
12
+ ### Added
13
+
14
+ - **`transport.telegram.question_ttl_seconds`: unanswered questions now
15
+ expire.** A `pending_resumes` row deliberately outlives the in-session
16
+ wait so a late reply can still drive the resume — but nothing ever
17
+ retired one, so the table only grew. Measured on one deployment: 46
18
+ questions posted, 4 ever answered, 16 still live and five repos
19
+ re-asking the same question on consecutive days. An expired row stops
20
+ being offered as the routing target for an orphan Telegram reply, and
21
+ a late answer can no longer revive it. Defaults to 172800 (48h),
22
+ minimum 3600.
23
+ - **New hourly `question_expiry_sweeper` job.** Retires a question early
24
+ once every issue and PR it cites is closed or merged — the case where
25
+ someone merged the PR by hand and the question became meaningless
26
+ rather than merely unanswered. Fails safe in both directions: a
27
+ question citing nothing is never expired this way, and any probe error
28
+ leaves the row alone, because re-asking a dead question is recoverable
29
+ while dropping a live one is not. The TTL pass runs first and needs no
30
+ network, so a GitHub outage still lets age-based expiry make progress.
31
+ Probing is capped at 200 numbers per run so one pass cannot outlive its
32
+ own hourly interval — unbounded, a 2000-row backlog against a slow
33
+ GitHub would be 8.3h of serialized `gh api` reads. Rows past the cap
34
+ are examined on the next run, and the TTL pass retires them on age
35
+ regardless.
36
+
37
+ - **`transport.telegram.ask_timeout_seconds`: how long a pipeline waits on
38
+ your Telegram reply.** Was a hard-coded 300s in `SocketTransport.ask`,
39
+ with no call site overriding it and no way to configure it. Defaults to
40
+ 900s — deliberately short, because a sweep runs its repos sequentially
41
+ and holds each repo's lock and worktree for the whole wait, and the
42
+ poller awaits its issue handler inline. This value only buys the
43
+ in-session fast path; answering later is covered by the pending-resume
44
+ path below, so raising it to cover an overnight gap would turn a daily
45
+ sweep into a multi-day one for no benefit.
46
+
47
+ ### Fixed
48
+
49
+ - **An expired question can no longer be resumed by a stale bridge.**
50
+ `answer_pending_resume` already refuses to answer an expired row, but
51
+ that guard only holds while every process runs the same code. The
52
+ bridge writes the answer and the poller reads it back — separate
53
+ daemons — so a deploy that restarts one before the other leaves a
54
+ window where an old bridge stamps `answered_at` on a row the new
55
+ poller has already expired, and the resume fires against state that
56
+ moved on. Seen live: the bridge ran three-day-old code for an hour
57
+ after the poller was upgraded. `list_pending_resumes_to_execute` now
58
+ filters expired rows too, closing the window from the side that acts
59
+ on them.
60
+
61
+ - **A secops repo failure now says what broke.** A sweep that died before
62
+ the `sessions` INSERT — anything raised by `ensure_bare_repo` or
63
+ `create_worktree`, both network-bound and both ahead of it — left no
64
+ trace at all: the DB update is guarded on the insert having happened,
65
+ and the `except` block had no logging. With an exception whose `str()`
66
+ is empty the Telegram alert then fell back to its generic summary, so
67
+ the operator got `Error processing <repo>` and nothing else. The
68
+ failure is now logged as `secops.repo.failed` regardless of which
69
+ branch runs, and both `summary` and `error` carry the exception type.
70
+ - **`git` timeouts stringify to something.** `asyncio.wait_for` raises a
71
+ bare `TimeoutError` whose message is the empty string; it is now
72
+ re-raised as the same type with the command, timeout, and cwd
73
+ attached, so it stays useful wherever it surfaces.
74
+ - **Bare clone and fetch get a transfer-sized timeout.** Both inherited
75
+ the 120s budget meant for local git plumbing, which a multi-gigabyte
76
+ repo cannot meet on any normal link — a 1.8 GB repo failed its sweep
77
+ this way every day. Network transfers now use 1800s, matching claude's
78
+ own `default_timeout_seconds`.
79
+ - **secops no longer looks at PRs outside its two author-filtered queries.**
80
+ The prompt only ever tells the agent to run `gh pr list` with
81
+ `--author "app/dependabot"` and `--author "$OPERATOR"` — but a step-5
82
+ bullet for "PRs from anyone else" implied a wider list existed, so the
83
+ agent ran its own unfiltered `gh pr list` to populate the category and
84
+ then signalled BLOCKED on PRs it can never merge. That fired a Telegram
85
+ question every sweep, forever, and every one timed out unanswered; run
86
+ summaries reporting "no open PRs at all" were the fingerprint of the
87
+ out-of-scope query. A third party's PR is now explicitly out of scope:
88
+ not fetched, not evaluated, not named in the summary. An explicit scope
89
+ fence forbids broadening the search. Operator-authored code PRs still
90
+ ASK, because there the answer genuinely decides what happens next.
91
+ - **BLOCKED questions may no longer be padded with already-decided items.**
92
+ Real questions were bundling a genuine Dependabot decision with a
93
+ non-actionable tail ("...confirm no action wanted"), burying the part
94
+ that needed an answer. Decided items belong in the run summary; a repo
95
+ with nothing to decide now finishes DONE instead of signalling BLOCKED.
96
+ - **Telegram questions now say which repo and session they belong to.** A
97
+ secops sweep posts one consolidated question per repo back-to-back and
98
+ the agent's text rarely names its own repo, so the operator saw a run of
99
+ near-identical "approve #387?" messages with nothing to tell them apart.
100
+ Worse, the orphan router's "reply again with the session_id included"
101
+ instruction was impossible to follow — the session_id had never been sent
102
+ to the operator in the first place. `ASK` now prefixes the question with
103
+ `[owner/repo]`, the session_id, and (for dev) the issue number.
104
+ - **Replying to one question no longer answers a different one.** When the
105
+ bridge couldn't match an incoming reply it fell back to FIFO and
106
+ delivered the answer to the *oldest* outstanding question. Replying to
107
+ the third question while the first was still open silently answered the
108
+ first, with nothing shown to the operator. Reply-to now matches strictly;
109
+ a plain message routes only when exactly one question is outstanding, and
110
+ otherwise the operator is told to use Telegram's reply and shown what is
111
+ waiting.
112
+ - **A late reply now resumes the session it was actually aimed at.** The
113
+ bridge remembers which session each posted question belongs to (bounded
114
+ to the last 500), so a reply arriving after the ASK timed out resolves
115
+ its `pending_resumes` row exactly instead of reporting "multiple BLOCKED
116
+ sessions are unanswered" and asking for a session_id the operator was
117
+ never given.
118
+ - **An answer given after the pipeline stopped waiting is no longer thrown
119
+ away.** When `ask()` timed out, the client discarded the request but did
120
+ not close the socket — a secops sweep reuses one transport across every
121
+ repo — so the bridge went on treating the question as live. A later
122
+ reply-to matched that dead entry, and the ANSWER was written into a
123
+ request nobody was listening for: no log, no notice, nothing. The
124
+ operator replied exactly as instructed and the answer vanished. The
125
+ bridge now tracks each question's deadline and drops expired entries
126
+ before routing, sending the reply down the pending-resume path instead.
127
+ Expired entries also no longer inflate the "more than one question is
128
+ waiting" count, which was refusing plain answers to the one real
129
+ question.
130
+ - **The post-sweep "blocked on `<repo>`" message is answerable.** It is
131
+ sent as a one-way notice and its Telegram id was never recorded, so
132
+ replying to the most recent and most prominent message for a repo landed
133
+ in the ambiguous branch. A `SEND` that names a session is now tracked the
134
+ same way an `ASK` is.
135
+ - **Operator notices name repos and sessions, not internal ids.** The
136
+ "still waiting" listing rendered `telegram_msg_id: request_id` — Telegram
137
+ never shows message ids in its UI and `request_id` means nothing to a
138
+ human. It now uses the same `[owner/repo] (session: …)` shape as the
139
+ question header.
140
+
141
+ ## [0.8.1] - 2026-09-04
142
+
143
+ ### Fixed
144
+
145
+ - **`ctrlrelay ci wait` was unconditionally broken on `gh` releases without
146
+ `--json` support on `pr checks`** (confirmed on gh 2.45.0, the current
147
+ Ubuntu 24.04 package — no newer version exists in the distro repos, so
148
+ this wasn't an "apt upgrade" problem for anyone on that platform).
149
+ `GitHubCLI.get_pr_checks` shelled out to `gh pr checks --json ...`; on an
150
+ affected install every single poll failed with `unknown flag: --json`,
151
+ which `PRVerifier.wait_for_checks` treats as transient and retries —
152
+ so every `ci wait` call was guaranteed to eat its full timeout and then
153
+ fail, regardless of the PR's actual state. Reproduced directly and
154
+ confirmed as the likely cause behind some "session never signaled DONE"
155
+ reports. Switched to `gh pr view --json statusCheckRollup`, stable since
156
+ early gh 2.x and always exit-0 regardless of check state, with a new
157
+ normalizer mapping its two GraphQL shapes (`CheckRun` and the legacy
158
+ `StatusContext`) into the `{name, state, bucket, link}` shape
159
+ `PRVerifier` already expects. See #145.
160
+
10
161
  ## [0.8.0] - 2026-09-04
11
162
 
12
163
  ### Added
@@ -1247,7 +1398,9 @@ pipeline).
1247
1398
  per-phase implementation plans (Phase 0 through Phase 4).
1248
1399
  - `docs/Claude_Code_Project_Guide.md` — project development guide.
1249
1400
 
1250
- [Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.8.0...HEAD
1401
+ [Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.9.0...HEAD
1402
+ [0.9.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.8.1...v0.9.0
1403
+ [0.8.1]: https://github.com/AInvirion/ctrlrelay/compare/v0.8.0...v0.8.1
1251
1404
  [0.8.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.7.0...v0.8.0
1252
1405
  [0.7.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.6.0...v0.7.0
1253
1406
  [0.6.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.5.0...v0.6.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ctrlrelay
3
- Version: 0.8.0
3
+ Version: 0.9.0
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,13 @@ transport:
37
37
  bot_token_env: "CTRLRELAY_TELEGRAM_TOKEN"
38
38
  chat_id: 123456789
39
39
  socket_path: "~/.ctrlrelay/ctrlrelay.sock"
40
+ # Seconds a pipeline waits on your reply before giving up and
41
+ # persisting the session as BLOCKED. This is only the in-session fast
42
+ # path — a reply that arrives later still attaches to the saved
43
+ # session and the pending-resume sweeper drives it. Don't raise this
44
+ # to cover being asleep: a sweep runs its repos one at a time and
45
+ # holds each repo's lock and worktree for the whole wait.
46
+ ask_timeout_seconds: 900
40
47
  file_mock:
41
48
  inbox: "~/.ctrlrelay/inbox.txt"
42
49
  outbox: "~/.ctrlrelay/outbox.txt"
@@ -118,6 +118,8 @@ transport:
118
118
  | `bot_token_env` | string | `"CTRLRELAY_TELEGRAM_TOKEN"` | Name of the environment variable holding the bot token. ctrlrelay never reads the token directly — only the variable name. |
119
119
  | `chat_id` | int | `0` | Telegram chat ID the bridge sends messages to and accepts replies from. |
120
120
  | `socket_path` | path | `"~/.ctrlrelay/ctrlrelay.sock"` | Unix socket path the bridge listens on. Pipelines connect to this socket as clients. |
121
+ | `ask_timeout_seconds` | int | `900` | How long a pipeline blocks waiting on your reply in-session. Minimum `60`. A reply arriving later still works: the question is persisted to `pending_resumes` and the every-minute sweeper drives the resume. |
122
+ | `question_ttl_seconds` | int | `172800` (48h) | How long an **unanswered** question stays routable before it is retired. Minimum `3600`. Without a ceiling those rows never die — a question whose PR was merged by hand weeks ago stays a live target for an orphan reply. The hourly `question_expiry_sweeper` also retires a question early once every issue/PR it cites is closed or merged. |
121
123
 
122
124
  See [Telegram bridge]({{ '/bridge/' | relative_url }}) for the full setup walkthrough.
123
125
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ctrlrelay"
3
- version = "0.8.0"
3
+ version = "0.9.0"
4
4
  description = "Local-first orchestrator for headless coding agents across multiple GitHub repos"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"