@llblab/pi-kit 0.36.1 → 0.36.2
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/CHANGELOG.md +5 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-claude-usage/CHANGELOG.md +4 -0
- package/node_modules/@llblab/pi-claude-usage/README.md +2 -2
- package/node_modules/@llblab/pi-claude-usage/lib/status-format.ts +2 -3
- package/node_modules/@llblab/pi-claude-usage/package.json +1 -1
- package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-codex-usage/README.md +1 -1
- package/node_modules/@llblab/pi-codex-usage/lib/status-format.ts +2 -3
- package/node_modules/@llblab/pi-codex-usage/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +2 -2
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +17 -5
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +23 -0
- package/node_modules/@llblab/pi-telegram/README.md +4 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +17 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +12 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +17 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +42 -10
- package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.js +9 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +3 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.d.ts +6 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.js +50 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +119 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +11 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +9 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +39 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +144 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.js +15 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +14 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +122 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +15 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +22 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +4 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +3 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +52 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.d.ts +17 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.js +25 -0
- package/node_modules/@llblab/pi-telegram/dist/package.json +2 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +10 -10
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +9 -3
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -0
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +24 -1
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +16 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +15 -5
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +3 -0
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +43 -11
- package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +12 -0
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +2 -0
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +3 -2
- package/node_modules/@llblab/pi-telegram/lib/pi.ts +2 -0
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +79 -16
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +137 -24
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +14 -0
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +173 -17
- package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +14 -8
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +166 -10
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +33 -6
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +8 -7
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +56 -2
- package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +44 -0
- package/node_modules/@llblab/pi-telegram/package.json +2 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@llblab/pi-kit` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.36.2: Bundled Package Updates (2026-10-23)
|
|
6
|
+
|
|
7
|
+
- `Exact pins`: `@llblab/pi-claude-usage` 0.2.2 → 0.2.3; `@llblab/pi-codex-usage` 0.13.1 → 0.13.2; `@llblab/pi-telegram` 0.57.3 → 0.57.7.
|
|
8
|
+
- `Packaging`: Synchronizes the lockfile, bundle aliases and README inventory. Package membership and resource order remain unchanged; packed validation does not certify extension safety or installed-client behavior.
|
|
9
|
+
|
|
5
10
|
## 0.36.1: Bundled Package Updates (2026-10-22)
|
|
6
11
|
|
|
7
12
|
- `Exact pins`: `@llblab/pi-state-flow` 0.26.4 → 0.26.5.
|
package/README.md
CHANGED
|
@@ -13,12 +13,12 @@ Package links lead to the owning repositories for usage, documentation, issues,
|
|
|
13
13
|
| Package | Version | Purpose |
|
|
14
14
|
| --- | ---: | --- |
|
|
15
15
|
| [`@llblab/pi-actors`](https://github.com/llblab/pi-actors) | `0.55.0` | Inspectable local Runs, reusable Recipes, persistent tools, and delegation Skills |
|
|
16
|
-
| [`@llblab/pi-claude-usage`](https://github.com/llblab/pi-claude-usage) | `0.2.
|
|
16
|
+
| [`@llblab/pi-claude-usage`](https://github.com/llblab/pi-claude-usage) | `0.2.3` | Claude subscription quota status and shared per-model Fast toggle for Opus, mirrored in Telegram |
|
|
17
17
|
| [`@llblab/pi-clean-room`](https://github.com/llblab/pi-clean-room) | `0.3.0` | Isolated nested Pi TUI with named npm extensions and compatible model selection |
|
|
18
|
-
| [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.13.
|
|
18
|
+
| [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.13.2` | OpenAI/legacy Codex subscription quota and Business credit status, with persistent priority Fast toggle mirrored in Telegram |
|
|
19
19
|
| [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.9.0` | Visible continuation scheduling and bounded worker Skills through compiled, manifest-owned resources |
|
|
20
20
|
| [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.26.5` | Scoped context/memory compiler with step-independent current memory, stable Passive/Active modes and visible blocked-state status |
|
|
21
|
-
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.57.
|
|
21
|
+
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.57.7` | Telegram companion with per-prompt Steer, queued controls, Reroute/Restore, recoverable connections, previews, follower Threads, files, and voice |
|
|
22
22
|
| [`@llblab/skills`](https://github.com/llblab/skills) | `1.15.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
|
|
23
23
|
|
|
24
24
|
Versions are exact by design. An upstream release does not change an installed kit until this repository explicitly advances the dependency and publishes a new kit version. Runtime defects and package-specific feature requests belong in the linked repository; package selection and kit installation issues belong here.
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.3: Empty Quota Bar Hotfix
|
|
4
|
+
|
|
5
|
+
- Aligned quota bars with rounded remaining percentages: when either window rounds to `0%`, its final segment disappears and the whole dual bar uses the error background, while the other half retains its quota. The 5-hour reset countdown follows the same zero boundary; near-full quota still rounds to the nearest 5% step.
|
|
6
|
+
|
|
3
7
|
## 0.2.2: Windows CI Validation
|
|
4
8
|
|
|
5
9
|
- Added Linux and Windows GitHub Actions validation that runs the full release check suite (typecheck, tests, extension import, dependency audit and package dry-run) on every push to `main` and pull request, so the shared-refresh SQLite mutex, atomic state writes and path handling are exercised on Windows. The extension runtime is unchanged from 0.2.1.
|
|
@@ -90,7 +90,7 @@ Pi 1.0.0 accepts the extra override but does not propagate `speed` to native req
|
|
|
90
90
|
claude ██████▀▀▀▀ 6d
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
The ten-character bar encodes two twenty-step limits at once: the top quadrants show the remaining 5-hour limit and the bottom quadrants show the remaining weekly limit.
|
|
93
|
+
The ten-character bar encodes two twenty-step limits at once: the top quadrants show the remaining 5-hour limit and the bottom quadrants show the remaining weekly limit. When either window's remaining percentage rounds to `0%`, that half becomes completely empty and the entire dual bar switches to the error background color; the other half still shows its remaining quota.
|
|
94
94
|
|
|
95
95
|
When the weekly reset time is available, it follows the bar. More than a day remains is shown in 144-minute day-tenth steps such as `7d`, `6.9d`, `1.1d`, rounded upward. At 24 hours and below it switches to upward-rounded hour-tenths such as `24h`, `23.7h`, `1.1h`, `1h`. Under an hour it shows floored minutes, then seconds. After the reset passes, `0s` is held until the next successful refresh.
|
|
96
96
|
|
|
@@ -100,7 +100,7 @@ When the 5-hour window is exhausted and exposes its reset time, the statusline a
|
|
|
100
100
|
claude ▄▄▄▄▄⠀⠀⠀⠀⠀ 5h/7d
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
-
If only one window is returned, the
|
|
103
|
+
If only one window is returned, the remaining percentage rounded to the nearest integer is shown:
|
|
104
104
|
|
|
105
105
|
```text
|
|
106
106
|
claude 67% 7d
|
|
@@ -262,9 +262,8 @@ function filledParts(
|
|
|
262
262
|
window: NormalizedRateLimitWindow | undefined,
|
|
263
263
|
totalParts: number,
|
|
264
264
|
): number {
|
|
265
|
-
if (!window) return 0;
|
|
265
|
+
if (!window || isQuotaWindowExhausted(window)) return 0;
|
|
266
266
|
const remaining = remainingPercent(window);
|
|
267
|
-
if (remaining <= 0) return 0;
|
|
268
267
|
return Math.max(1, Math.round(remaining / (100 / totalParts)));
|
|
269
268
|
}
|
|
270
269
|
|
|
@@ -275,7 +274,7 @@ function remainingPercent(window: NormalizedRateLimitWindow): number {
|
|
|
275
274
|
function isQuotaWindowExhausted(
|
|
276
275
|
window: NormalizedRateLimitWindow | undefined,
|
|
277
276
|
): boolean {
|
|
278
|
-
return window !== undefined && remainingPercent(window)
|
|
277
|
+
return window !== undefined && Math.round(remainingPercent(window)) === 0;
|
|
279
278
|
}
|
|
280
279
|
|
|
281
280
|
function hasExhaustedQuotaWindow(report: ClaudeUsageReport): boolean {
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.2: Empty Quota Bar Hotfix
|
|
4
|
+
|
|
5
|
+
- Aligned quota bars with rounded remaining percentages: when either window rounds to `0%`, its final segment disappears and the whole dual bar uses the error background, while the other half retains its quota. The 5-hour reset countdown follows the same zero boundary; near-full quota still rounds to the nearest 5% step.
|
|
6
|
+
- Release verification now tolerates up to approximately 30 minutes of npm registry processing before checking the published package identity and creating the GitHub Release.
|
|
7
|
+
|
|
3
8
|
## 0.13.1: Windows Codex CLI Fallback
|
|
4
9
|
|
|
5
10
|
- The Codex CLI fallback now starts on Windows. When npm installs Codex behind a `codex.cmd` shim that a shell-free `spawn` cannot launch, the extension resolves the native `codex.exe` from PATH or from the npm platform package and starts it directly, so fallback quota stays available when the primary endpoint fails. Non-Windows platforms keep plain PATH lookup, and the helper window is hidden.
|
|
@@ -103,7 +103,7 @@ When OpenAI returns only the weekly window, the status shows the exact rounded r
|
|
|
103
103
|
codex 67% 7d
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
When both windows exist, the ten-character bar encodes two twenty-step limits at once: the top quadrants show the 5-hour limit and the bottom quadrants show the weekly limit, with each step representing 5%.
|
|
106
|
+
When both windows exist, the ten-character bar encodes two twenty-step limits at once: the top quadrants show the 5-hour limit and the bottom quadrants show the weekly limit, with each step representing 5%. When either window's remaining percentage rounds to `0%`, that half becomes completely empty and the entire dual bar switches to the error background color; the other half still shows its remaining quota.
|
|
107
107
|
|
|
108
108
|
Before the first usable Codex report arrives, two half-height markers move through the same fixed-width themed bar. The upper 5-hour marker travels opposite the lower weekly marker; both reverse smoothly at the ends, and each loader run randomly starts from one of the two mirrored endpoint phases. Their motion distinguishes loading from 100% remaining quota while preserving the normal bar background. A first report claiming both windows are completely unused is treated as provisional for 15 seconds and retried every second by the refreshing instance before it is published, because providers can briefly emit zeroed windows while initializing. Once a usable report exists, refresh requests preserve that last good bar.
|
|
109
109
|
|
|
@@ -312,9 +312,8 @@ function filledParts(
|
|
|
312
312
|
window: NormalizedRateLimitWindow | undefined,
|
|
313
313
|
totalParts: number,
|
|
314
314
|
): number {
|
|
315
|
-
if (!window) return 0;
|
|
315
|
+
if (!window || isQuotaWindowExhausted(window)) return 0;
|
|
316
316
|
const remaining = remainingPercent(window);
|
|
317
|
-
if (remaining <= 0) return 0;
|
|
318
317
|
return Math.max(1, Math.round(remaining / (100 / totalParts)));
|
|
319
318
|
}
|
|
320
319
|
|
|
@@ -325,7 +324,7 @@ function remainingPercent(window: NormalizedRateLimitWindow): number {
|
|
|
325
324
|
function isQuotaWindowExhausted(
|
|
326
325
|
window: NormalizedRateLimitWindow | undefined,
|
|
327
326
|
): boolean {
|
|
328
|
-
return window !== undefined && remainingPercent(window)
|
|
327
|
+
return window !== undefined && Math.round(remainingPercent(window)) === 0;
|
|
329
328
|
}
|
|
330
329
|
|
|
331
330
|
function hasExhaustedQuotaWindow(
|
|
@@ -80,7 +80,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
|
|
|
80
80
|
- Session identity, not Pi context object identity, decides authority. Pi builds a fresh context object for every event, command, and tool call, so compare sessions through their stored generation and ID, never by object, and treat a fresh object of the same session as the same authority. How an instance came online — explicit `/telegram-connect`, session auto-restore, election promotion, or a same-process handoff — may decide only that it is online and which existing binding, slot, and target it takes; every post-online behavior, including election eligibility, promotion, routing, delivery, status, and Thread display, must read the same acknowledged registration state and must not branch on that mechanism. Only the documented connect-time gates (remembered-binding restore admission, restore/recovery envelope flags, capability negotiation, and predecessor handoff) may observe it. An in-flight registration survives a fresh same-session context and is dropped only by a replaced session generation or ID.
|
|
81
81
|
- Protocol compatibility is independent from package version. Registration negotiates protocol version, runtime build, and canonical capabilities before target provisioning or live publication. `durable-follower-admission-v1` gates source forwarding; `queue-handoff-v1` independently gates semantic queue transfer for every participant and is advertised only with exact source/recipient journal-binding composition. `follower.register`, capability-gated restore-only `follower.restoreWorkspace` and side-effect-free liveness `bus.probe` are bootstrap requests; other requests require exact live-registry generation authority, and `bus.ack` is response-only. `thread-display-mode-v1` gates follower display-setting requests; the leader serializes the shared preference write and owns title application.
|
|
82
82
|
- Long-lived timers, pollers, watchers, receivers, heartbeats, background delivery, and deferred dispatch are session-bound. Shutdown cancels/drains diagnostics publications and fences deferred status reads before they can acquire guards or repair files. Replacement stops stale activity and makes late work inert; teardown must recheck the captured session generation even when context identity is reused. Same-process handoff may preserve exact profile/target identity but never stale Pi context or cross-profile authority. Follower heartbeat and leader-to-follower Restore/live-rebind control reply deadlines align with the eight-second leader stale-liveness window rather than the generic one-second local-RPC default; an ordinary multi-second Pi/TUI event-loop stall or a slower Windows named pipe is not registration or recipient loss. Participating source observations must hold a reference for the actual read, including pending-mutation/count queries against a stopped worker's retained source; a scoped observation never restores receipt readiness or execution authority. A donor cancellation resumed after remote handoff awaits must also hold a source reference; only the existing exact journal CAS may cancel, never undo accepted recipient custody. Stable source keys do not certify captured callable lifetimes: snapshot prepared worker capabilities at construction and replace them on source-handle renewal without replaying unsettled input. Aborting a durable update generation does not release that `update_id`: replacement replay waits for its actual handler settlement, and effectful handlers use the shared execution fence immediately before commit and after awaited delegation. Admission also rechecks that fence after the default handler returns, before its outcome can settle custody; a stopped handler's ordinary return is not completion authority. Internal clones explicitly carry the hidden fence; reroute forwarding, thread-store mutation, cleanup, and Bot API boundaries retain the originating generation.
|
|
83
|
-
- Repeated identical diagnostics in one phase collapse into one run record plus one outcome record; the logging domain owns that recording policy, and other domains receive it as a structural port rather than growing a second owner. Runtime state is event-driven reconciliation of local assumptions against Telegram signals, not a complete bot read-model and not permission to query Telegram on every action.
|
|
83
|
+
- Repeated identical diagnostics in one phase collapse into one run record plus one outcome record; the logging domain owns that recording policy, and other domains receive it as a structural port rather than growing a second owner. Runtime state is event-driven reconciliation of local assumptions against Telegram signals, not a complete bot read-model and not permission to query Telegram on every action. Lifecycle Thread cleanup goes through `thread-reconciler` with current proof and leader fencing. Pressure retirement and explicit manual inactive-Workspace cleanup instead require their exact named Workspace-admission fence and one-shot deletion permit; neither may call ordinary unfenced deletion transport. Fresh Workspace Thread creation derives its initial Bot API title from the active display mode before issuance; the stable generated `threadName` remains separate from the acknowledged `displayTitle`.
|
|
84
84
|
|
|
85
85
|
### 4.3 Durable Admission And Settlement
|
|
86
86
|
|
|
@@ -88,7 +88,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
|
|
|
88
88
|
- Storage cutovers must reconcile actual consumer locations before correcting path adapters. Never normalize a relative historical reference into new authority or treat equal reference strings / empty canonical storage as source completeness. Exact-path preflight is lexical only; physical identity, historical coverage, writer closure and migration remain separate proofs.
|
|
89
89
|
- Workspace mutations acquire cross-process admission before their shared process-local gate and hold it through asynchronous API work and durable settlement. Topic lifecycle, complete unbound/reroute target handling, manual disconnect, and session-restart cleanup use profile-wide scope; either retained retirement-fence phase rejects them before state access. Cleanup admission spans intent publication, target mutation, persistence, and successful transport release. Explicit manual disconnect may stop only its captured local transport after cleanup admission/effects fail; it must not bypass Thread mutation fences, release a replacement connection, claim unconfirmed deletion, or retry an uncertain stop. Session-restart cleanup retains its admission-required behavior. Detached reconciliation that mutates Thread state must reacquire fresh profile admission through the same gate; it cannot inherit a caller lease that ended before its timer runs. A live operation ID has one process-local caller: concurrent reuse is rejected before lease acquisition, while retry after the caller exits may resume exact durable authority.
|
|
90
90
|
- One Thread keeps one owner across session replacement. A session replaced inside the same process reuses its preserved handoff Thread instead of provisioning another. A new session in a folder whose previous session is provably gone (no live registration for its target, absent runtime PID) adopts that binding's exact target and slot, and the predecessor binding is re-keyed to the successor rather than left as an orphan active row. A live predecessor, an ambiguous candidate set, an unknown runtime, or a missing liveness probe keeps ordinary provisioning.
|
|
91
|
-
- Workspace retirement is capacity-pressure-only. Elapsed time and heartbeat silence never trigger deletion; only complete `A`–`Z` exhaustion may propose the oldest continuously proven inactive, fully unprotected binding. Exact deletion, durable retirement, and fence completion precede slot reuse. Authorized demand-driven rotation retries one failed fresh allocation after retirement; restore-only follower startup never evicts. Release ordinary registration/provisioning leases before acquiring the destructive fence, retain the shared mutation gate across retirement, and validate the exact permit immediately before one non-retried deletion. Persist an exact method/target-matched rejection before withdrawing its intent; release that fence only after durable withdrawal, retaining the binding. A later attempt requires fresh operation authority. Protection reads must never repair, quarantine, or reset journals. Non-destructive owner detachment must atomically retain one exact Workspace binding and its letter while removing only its uniquely matched owner record and stamping first inactivity; it never manufactures Thread-deletion evidence or clears accepted work. During authenticated follower registration, a mismatched retained target record is repaired to the exact Workspace claim before binding commit; if same-target commit adopts the retained binding's slot, its returned binding then owns the registration result and owner-record slot. Unrelated bindings that own a stale or provisional letter remain untouched. Same-target re-keying retains historical journal keys and exact source tuples without upgrading unknown completeness. Retained prune observations are bounded, non-routing and registration/profile/epoch/runtime-fenced. One unfinished preservation operation retains its admission identity across fresh-PID-proof retries; it can never become deletion authority. Leader quit requires completed delivery/polling/worker teardown under the captured session generation, profile and epoch; reload/new/resume/fork never establish inactivity. Unknown deletion outcomes retain their fence; only confirmed durable completion permits reuse.
|
|
91
|
+
- Workspace retirement is capacity-pressure-only. Elapsed time and heartbeat silence never trigger deletion; only complete `A`–`Z` exhaustion may propose the oldest continuously proven inactive, fully unprotected binding. Exact deletion, durable retirement, and fence completion precede slot reuse. Authorized demand-driven rotation retries one failed fresh allocation after retirement; restore-only follower startup never evicts. Release ordinary registration/provisioning leases before acquiring the destructive fence, retain the shared mutation gate across retirement, and validate the exact permit immediately before one non-retried deletion. Persist an exact method/target-matched rejection before withdrawing its intent; release that fence only after durable withdrawal, retaining the binding. A later attempt requires fresh operation authority. Protection reads must never repair, quarantine, or reset journals. Non-destructive owner detachment must atomically retain one exact Workspace binding and its letter while removing only its uniquely matched owner record and stamping first inactivity; it never manufactures Thread-deletion evidence or clears accepted work. During authenticated follower registration, a mismatched retained target record is repaired to the exact Workspace claim before binding commit; if same-target commit adopts the retained binding's slot, its returned binding then owns the registration result and owner-record slot. Unrelated bindings that own a stale or provisional letter remain untouched. Same-target re-keying retains historical journal keys and exact source tuples without upgrading unknown completeness. Retained prune observations are bounded, non-routing and registration/profile/epoch/runtime-fenced. One unfinished preservation operation retains its admission identity across fresh-PID-proof retries; it can never become deletion authority. Leader quit requires completed delivery/polling/worker teardown under the captured session generation, profile and epoch; reload/new/resume/fork never establish inactivity. Unknown deletion outcomes retain their fence; only confirmed durable completion permits reuse. When retirement finds no eligible binding, fresh allocation falls back to metadata-only reuse of the least recently used letter (detached by inactivity start, live owners last, then letter): strip `slot` from that binding and its current Thread record in one durable publication, then retry the claim. This deletes nothing, leaves Threads, journals, accepted work and routing identity untouched, skips letters held by claims, reservations, provisions, cleanups, retirements or external admission, and never runs for restore-only or background recovery registrations. A slotless binding holds no letter and is not a retirement candidate.
|
|
92
92
|
- Foreign forwarding settles as `accepted`, `retryable`, or `terminal-rejected`. Only an authenticated acknowledgement carrying the expected `deliveryId` and `sourceUpdateId` releases leader journal authority. Negative, missing, stale, mismatched, or capacity-failed settlement remains durable; callback error answers are side effects only. Exception: the operator-approved fixed chooser deadline may discard its exact unclaimed donor pending source without a body archive or delivery-success claim. Accepted recipient queue/work and bound Threads are untouched; old donor controls and late reports cannot replay it. The lifetime and temporary cleanup contract lives in `docs/architecture.md`.
|
|
93
93
|
- A forwarding delivery id is stable across registration replacement and derives from envelope kind, source `update_id`, and stable recipient binding. Runtime instance and registration generation remain separate attempt fences. Persisted message ownership carries the stable binding so replay can rebind only to its current authenticated registration.
|
|
94
94
|
- A queued receipt persists its acquiring runtime instance, OS pid/process-birth identity, session generation, acquisition id, and acquisition time. Only exact authority may settle or discard it. Cached presence is not current execution proof: prepared custody must revalidate the exact queued owner/group without recovery and refuse offers or uncertain reads. Completion requires an exact removal acknowledgement, never merely `!ready`; a retained acknowledgement permits local cleanup only, not replay. Same-process session replacement may reconstruct the claim and the original process may settle after transport ownership moves; a foreign process may neither replay nor settle it through generic removal or a copied acquisition id.
|
|
@@ -2,10 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
_This file owns unresolved project work only. Completed behavior belongs in `CHANGELOG.md`; durable contracts belong in `AGENTS.md` and `/docs`._
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Disposable Thread live acceptance
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- [ ] After separate operator authorization, use disposable Telegram tabs to check leader Settings review/Back, one confirmed cleanup of an inactive slotless binding, preservation of its replacement letter holder, and visible outcome/recovery copy. Native filesystem and injected-transport tests do not prove live Telegram/client or native Windows acceptance. Never use production history as the disposable fixture.
|
|
8
|
+
- [ ] On separately authorized disposable routing tabs, verify pending-choice protection, Cancel after the view becomes unavailable, expiry notice in the owned leader Thread and no duplicate deletion after an unknown outcome. Local native journal/API-decoder fixtures are not live client acceptance.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
10
|
+
## Prompt-cache live confirmation
|
|
11
|
+
|
|
12
|
+
The prompt-cache report remains open for the reporter's offered live Anthropic capture after the published fix. Native request/transcript checks do not replace cache-hit or signed-thinking measurements.
|
|
13
|
+
|
|
14
|
+
- [ ] Review the reporter's resume and mid-session tool-change results (versions, cache read/write counts and new thinking-drop events). Close the report if the live evidence confirms the fix, or narrow any residual failure into actionable follow-up. Do not reconstruct or claim recovery of previously lost thinking blocks.
|
|
15
|
+
|
|
16
|
+
## Stuck forwarded command after Thread re-creation
|
|
17
|
+
|
|
18
|
+
Evidence (2026-10-23 07:11–07:16): a `/start` sent in a former leader tab moved into a follower's journal as a raw original. After that follower's Thread was re-created by the restore probe, every execution attempt failed with `Telegram bus API call is not allowed for this follower` and retried every 60 s indefinitely, because the reply targeted a Thread the follower no longer owned. Terminal settlement alone is unsafe: the same rejection can be a benign race during a live-rebind pre-dispatch.
|
|
19
|
+
|
|
20
|
+
- [ ] Live acceptance: a forwarded command whose follower Thread was re-created is answered in the current Thread (disposable tabs only).
|
|
21
|
+
- [ ] Find why the temporary tab's selected `/start` stayed unresolved until expiry, and why that Restore moved custody without applying the rebind.
|
|
22
|
+
- [ ] Route an input that arrives in a tab bound to nothing through a fresh temporary routing tab (the All flow) instead of publishing the chooser in the orphan, deleting the orphan with it once nothing unspent remains. Needs its own durable-metadata design.
|
|
23
|
+
- [ ] Live acceptance on disposable tabs only: Cancel on an orphan tab deletes it; a second unspent input keeps it until the last Cancel; a plain `std::map` option button is delivered as an ordinary prompt.
|
|
@@ -4,6 +4,29 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.57.7: Routing Cleanup and Recipient Recovery
|
|
8
|
+
|
|
9
|
+
- `Recipient recovery`: A forwarded input journaled for a follower whose Thread was re-created no longer retries forever against the old Thread. After the leader refuses that reply, the input re-runs once against the follower's current Thread; a second refusal or any other failure keeps the durable retry state, and nothing is dropped.
|
|
10
|
+
- `Transit cleanup`: Cancel remains journal-first when its Telegram view is unavailable. A parsed exact-target deletion/absence response now finishes temporary-tab cleanup instead of retaining a stuck issued entry. Closed, denied, foreign, malformed and forged responses stay protected without retry. Native regressions also prove waiting-tab protection across cold reads and unavailable state, and refusal to cancel custody already handed to a queue.
|
|
11
|
+
- `Routing expiry`: Expired inputs now receive an explicit undelivered/unconfirmed notice even when the chooser message is gone. Cold expiry uses the journal's saved chooser; a disappearing or unavailable view falls back only to the leader's current owned Thread, never All. Selected/issued work is not falsely called undelivered, accepted recipient work continues, and stale authority or failed notices cannot replay input or authorize deletion.
|
|
12
|
+
- `Manual cleanup`: Leader Settings can review and separately confirm deletion of inactive Telegram tabs and their messages, including bindings without a letter. Pi history/project files and replacement letter holders stay untouched. Exact review, live-work protection, profile/session/leader fences and one-shot permits are rechecked; changed/protected tabs are skipped, unknown deletion never repeats and interrupted commits remain recoverable. Review and Back delete nothing.
|
|
13
|
+
- `Thread titles`: Reassigning a letter immediately attempts to retitle the displaced Thread with its retained name, preserving manual titles and all history/work. Slotless bindings participate in later display reconciliation without acquiring routing ownership. Failed or unacknowledged edits keep prior title evidence, record a diagnostic and never undo reuse or delete the Thread.
|
|
14
|
+
|
|
15
|
+
## 0.57.6: Stability Hotfix
|
|
16
|
+
|
|
17
|
+
- `Plain buttons`: A button without a Generative App binding stays an ordinary prompt unless it is a well-formed `app::method` for an installed app. Prompts containing `::` that fail that test (`std::map`, `cargo test a::b(c)`) no longer raise `Malformed Generative App bound action` or retry forever from the journal; bound buttons keep their fail-closed errors.
|
|
18
|
+
- `Unbound Cancel`: Cancelling the last unspent input in a tab that is bound to nothing now deletes that tab, like a routing tab, once no other chooser, queue, live owner, Restore record or retained journal arrival still names it. Missing proof or any failure keeps the tab with a diagnostic; the private original stays retained.
|
|
19
|
+
|
|
20
|
+
## 0.57.5: Workspace Slot Reuse
|
|
21
|
+
|
|
22
|
+
- `Slot reuse`: A full A–Z pool no longer refuses a new connection when every Thread is active or protected. After pressure retirement finds no candidate, the least recently used letter is reassigned as metadata: detached bindings first, live owners last. The previous holder keeps its Thread, history, journals and accepted work and only loses its letter label. Restore-only startup and background recovery never reassign a letter.
|
|
23
|
+
|
|
24
|
+
## 0.57.4: Prompt Cache Stability
|
|
25
|
+
|
|
26
|
+
- `Prompt composition`: Telegram guidance uses native Pi sections instead of forcing the request head, preserving its prefix across tool/context updates, connection changes and resume. Stable connection guidance is separate from hidden Telegram-turn messages; local/custom starts do not add new turn notes, while earlier notes remain in their original history. Refs #353.
|
|
27
|
+
- `Prompt compatibility`: An unchanged fallback produces no override. Hosts without structured sections, explicit custom hooks and an earlier extension-owned opaque prompt retain equality-guarded append/strip behavior for strings or ordered blocks without losing foreign instructions. Another extension's forced prompt remains outside the cache-stability guarantee.
|
|
28
|
+
- `CI`: macOS validation uses the supported macOS 26 Intel image to avoid hosted ARM runner-capacity failures, and runs its test files two at a time so wall-clock latency tests stop failing under contention. Full checks remain enabled on macOS, Linux and Windows, with unchanged test deadlines and assertions.
|
|
29
|
+
|
|
7
30
|
## 0.57.3: Session Thread Continuity
|
|
8
31
|
|
|
9
32
|
- `Session handoff`: A session replaced inside one Pi process now reuses its preserved Thread on every registration path, not only connect recovery. The successor re-keys onto the same tab and slot with the new session id, so `/new` no longer provisions a second tab and leaves the previous one without an owner.
|
|
@@ -257,10 +257,12 @@ Classic private DM mode is the base product mode. When Telegram private-chat Thr
|
|
|
257
257
|
- Each connected instance gets a Telegram thread target.
|
|
258
258
|
- Queued work for a live follower transfers through authenticated exact-journal handoff rather than replaying under the transport owner.
|
|
259
259
|
- Reopening, resuming, or replacing the process for the same Pi session restores its remembered Thread; distinct sessions in one directory keep independent bindings and letter slots.
|
|
260
|
-
- Unknown threads are preserved and offered explicit reroute/restore choices.
|
|
261
|
-
- When all `A`–`Z` slots are reserved, a new connection may reclaim the oldest inactive, unprotected Thread. **This deletes that Thread and all its messages**, not Pi session history or project files.
|
|
260
|
+
- Unknown threads are preserved and offered explicit reroute/restore choices. Retained temporary routing tabs stay protected while choices are pending. Cancel settles exact journal custody even if the Telegram view is unavailable; an unknown tab-deletion outcome never causes another deletion.
|
|
261
|
+
- When all `A`–`Z` slots are reserved, a new connection may reclaim the oldest inactive, unprotected Thread. **This deletes that Thread and all its messages**, not Pi session history or project files. When every slot is active or protected, nothing is deleted: the new connection takes the least recently used letter instead, and that Thread keeps its history and any accepted work. The leader immediately attempts to replace its old letter title with its retained name (manual titles stay); a failed edit is recorded for later display reconciliation, without undoing reuse or deleting anything. Restoring a remembered session never evicts another binding.
|
|
262
262
|
- Telegram never launches hidden Pi processes.
|
|
263
263
|
|
|
264
|
+
To remove retained inactive tabs, open Settings → **🧹 Thread cleanup** → **Review inactive tabs** on the leader instance. Review and Back delete nothing. The separate **Clean inactive tabs** action deletes eligible reviewed Telegram tabs **and all their messages**, not Pi session history or project files. Tabs without a letter are included when inactivity and clear protection are proven; live owners, accepted work and uncertain evidence remain protected. An unknown deletion outcome is retained without automatic retry.
|
|
265
|
+
|
|
264
266
|
In Settings → **🧵 Thread display**, choose Letters (default), Names, Directory Title or Directory Snake. The choice is shared across all profiles through `threads.displayMode`; profiles contain only bot identity and the paired owner. Use `/name` for a custom Thread name and **Reset to automatic** to restore the selected naming style. Renaming preserves the Thread and its queued work. See [Thread Display](./docs/architecture.md#threaded-mode-multi-instance-bus) for naming details.
|
|
265
267
|
|
|
266
268
|
| Mode | Best for | Runtime shape |
|
|
@@ -239,7 +239,23 @@ export function createTelegramGenerativeAppBoundButtonActionInvoker(deps) {
|
|
|
239
239
|
let boundAction;
|
|
240
240
|
let handedOff;
|
|
241
241
|
try {
|
|
242
|
-
|
|
242
|
+
// Only buttons emitted by a Generative App carry a binding. An unbound button is ordinary model text
|
|
243
|
+
// (`std::map`, `a::b(c)`): malformed or uninstalled-app prompts fall through to the normal prompt plane.
|
|
244
|
+
if (!action.binding) {
|
|
245
|
+
try {
|
|
246
|
+
boundAction = GenerativeApps.parseGenerativeAppBoundAction(action.prompt);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
if (boundAction &&
|
|
252
|
+
!(await GenerativeApps.isGenerativeAppInstalled(deps.agentDir, boundAction.app))) {
|
|
253
|
+
boundAction = undefined;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
boundAction = GenerativeApps.parseGenerativeAppBoundAction(action.prompt);
|
|
258
|
+
}
|
|
243
259
|
if (!boundAction)
|
|
244
260
|
return false;
|
|
245
261
|
deps.assertExecutionCurrent(query);
|
|
@@ -9,13 +9,13 @@ import { createHash } from "node:crypto";
|
|
|
9
9
|
import { basename } from "node:path";
|
|
10
10
|
import { isDeepStrictEqual } from "node:util";
|
|
11
11
|
import { getTelegramBusTransportRetryPolicy, TELEGRAM_BUS_REGISTRATION_RETRY, } from "./bus-transport.js";
|
|
12
|
-
import { createTelegramBusFollowerDeliveryIdentity, createTelegramBusForeignOwnedUpdateForwarder, createTelegramBusLocalServer, createTelegramBusRequestIdFactory, createUnauthorizedBusAck, rejectTelegramBusRequest, getTelegramBusProtocolCompatibility, getTelegramBusSocketPath, hasTelegramBusCapability, hasTelegramBusSteerQueueHandoffCapability, isTelegramBusEnvelopeAuthorized, parseTelegramBusEnvelope, resolveTelegramBusSocketPath, sendTelegramBusLocalEnvelope, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE, TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES, getTelegramBusHeldCommandCapabilities, TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES, TelegramBusLocalAuthorityError, } from "./bus.js";
|
|
12
|
+
import { createTelegramBusFollowerDeliveryIdentity, createTelegramBusForeignOwnedUpdateForwarder, createTelegramBusLocalServer, createTelegramBusRequestIdFactory, createUnauthorizedBusAck, rejectTelegramBusRequest, getTelegramBusProtocolCompatibility, getTelegramBusSocketPath, hasTelegramBusCapability, hasTelegramBusSteerQueueHandoffCapability, isTelegramBusEnvelopeAuthorized, parseTelegramBusEnvelope, resolveTelegramBusSocketPath, sendTelegramBusLocalEnvelope, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE, TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES, getTelegramBusHeldCommandCapabilities, TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES, TelegramBusLocalAuthorityError, TELEGRAM_BUS_FOLLOWER_API_NOT_ALLOWED_MESSAGE, } from "./bus.js";
|
|
13
13
|
import { parseTelegramUpdateJournalQueueOwner, } from "./journal.js";
|
|
14
14
|
import { TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS, } from "./locks.js";
|
|
15
15
|
import * as Machine from "./machine.js";
|
|
16
16
|
import { isPiStaleContextError } from "./pi.js";
|
|
17
17
|
import { parseTelegramTarget as parseTarget, } from "./target.js";
|
|
18
|
-
import { isTelegramApiMethodRetrySafe, assertTelegramApiCallAuthority, TelegramApiAuthorityError, TelegramApiCommitUnknownError, TelegramApiStaleTargetError, } from "./telegram-api.js";
|
|
18
|
+
import { isTelegramApiMethodRetrySafe, assertTelegramApiCallAuthority, TelegramApiAuthorityError, TelegramApiCommitUnknownError, TelegramApiFollowerNotAllowedError, TelegramApiStaleTargetError, } from "./telegram-api.js";
|
|
19
19
|
import * as Threads from "./threads.js";
|
|
20
20
|
import { isWireRecord as isRecord } from "./wire.js";
|
|
21
21
|
import { createTelegramWorkspaceAdmissionOperationId, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
|
|
@@ -1002,6 +1002,16 @@ export function createTelegramBusFollowerApiCaller(deps) {
|
|
|
1002
1002
|
response.error?.code === "commit-unknown") {
|
|
1003
1003
|
throw new TelegramApiCommitUnknownError(response.error.method ?? method, new Error(message ?? "Telegram bus API call result is ambiguous."));
|
|
1004
1004
|
}
|
|
1005
|
+
if (message === TELEGRAM_BUS_FOLLOWER_API_NOT_ALLOWED_MESSAGE) {
|
|
1006
|
+
const body = (method === "call" || method === "callMultipart") && isRecord(args[1])
|
|
1007
|
+
? args[1]
|
|
1008
|
+
: undefined;
|
|
1009
|
+
const chatId = Number(body?.chat_id);
|
|
1010
|
+
const threadId = Number(body?.message_thread_id);
|
|
1011
|
+
throw new TelegramApiFollowerNotAllowedError(message, Number.isSafeInteger(chatId) && Number.isSafeInteger(threadId)
|
|
1012
|
+
? { chatId, threadId }
|
|
1013
|
+
: undefined);
|
|
1014
|
+
}
|
|
1005
1015
|
throw new Error(message ?? "Telegram bus API call failed.");
|
|
1006
1016
|
};
|
|
1007
1017
|
return async (method, args, localOptions) => {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { isDeepStrictEqual } from "node:util";
|
|
8
8
|
import { getTelegramBusTransportRetryPolicy } from "./bus-transport.js";
|
|
9
|
-
import { createTelegramBusLocalServer, createUnauthorizedBusAck, rejectTelegramBusRequest, getTelegramBusEnvelopeTrafficClass, getTelegramBusFollowerSocketPath, getTelegramBusProtocolCompatibility, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, parseTelegramBusEnvelope, sendTelegramBusLocalEnvelope, stripTelegramBusApiMetadata, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF, TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY, hasTelegramBusSteerQueueHandoffCapability, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, } from "./bus.js";
|
|
9
|
+
import { createTelegramBusLocalServer, createUnauthorizedBusAck, rejectTelegramBusRequest, getTelegramBusEnvelopeTrafficClass, getTelegramBusFollowerSocketPath, getTelegramBusProtocolCompatibility, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, parseTelegramBusEnvelope, sendTelegramBusLocalEnvelope, stripTelegramBusApiMetadata, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF, TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY, hasTelegramBusSteerQueueHandoffCapability, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, TELEGRAM_BUS_FOLLOWER_API_NOT_ALLOWED_MESSAGE, } from "./bus.js";
|
|
10
10
|
import { escapeHtml } from "./rendering.js";
|
|
11
11
|
import * as Sync from "./sync.js";
|
|
12
12
|
import { parseTelegramIntegerId as asInteger, } from "./target.js";
|
|
@@ -136,6 +136,9 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
|
|
|
136
136
|
store: deps.topicTargetStore,
|
|
137
137
|
getLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
138
138
|
getExternalProtection: deps.captureWorkspaceExternalProtection,
|
|
139
|
+
onSlotReused: (bindingKey, isCurrent) => display
|
|
140
|
+
? reconcileThreadDisplay(bindingKey, isCurrent).then(() => undefined)
|
|
141
|
+
: Promise.resolve(),
|
|
139
142
|
recordEvent(message, details) {
|
|
140
143
|
deps.recordRuntimeEvent("bus", message, details);
|
|
141
144
|
},
|
|
@@ -174,6 +177,10 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
|
|
|
174
177
|
getLeaderEpoch: () => deps.getCurrentLeaderEpoch?.(),
|
|
175
178
|
captureLiveBindingKeys,
|
|
176
179
|
captureBindingAuthority(binding) {
|
|
180
|
+
// Slotless retained bindings need display repair even without a live owner.
|
|
181
|
+
// The reconciler still fences the exact binding snapshot, profile, mode and epoch.
|
|
182
|
+
if (binding.slot === undefined)
|
|
183
|
+
return () => true;
|
|
177
184
|
const matches = (target) => target?.chatId === binding.target.chatId &&
|
|
178
185
|
target?.threadId === binding.target.threadId;
|
|
179
186
|
const leader = deps.topicTargetStore.getActiveByInstanceId(deps.instanceId);
|
|
@@ -194,16 +201,20 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
|
|
|
194
201
|
callApi: deps.callApi,
|
|
195
202
|
})
|
|
196
203
|
: undefined;
|
|
197
|
-
const reconcileThreadDisplayOperation = async () => {
|
|
198
|
-
const result = await display.reconcile();
|
|
204
|
+
const reconcileThreadDisplayOperation = async (bindingKey) => {
|
|
205
|
+
const result = await display.reconcile(bindingKey);
|
|
199
206
|
deps.onThreadDisplayChanged?.();
|
|
200
207
|
return result;
|
|
201
208
|
};
|
|
202
|
-
const reconcileThreadDisplay = () => runWorkspaceOperation({
|
|
209
|
+
const reconcileThreadDisplay = (bindingKey, isCurrent = () => true) => runWorkspaceOperation({
|
|
203
210
|
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
204
211
|
operationKind: "workspace.reconcile-display",
|
|
205
212
|
scopes: [{ kind: "profile" }],
|
|
206
|
-
},
|
|
213
|
+
}, () => {
|
|
214
|
+
if (!isCurrent())
|
|
215
|
+
throw new Error("Telegram Thread display update lost allocation authority.");
|
|
216
|
+
return reconcileThreadDisplayOperation(bindingKey);
|
|
217
|
+
});
|
|
207
218
|
let startupDisplayTimer;
|
|
208
219
|
let startupDisplayGeneration = 0;
|
|
209
220
|
let startupDisplaySettled = true;
|
|
@@ -2759,7 +2770,7 @@ async function handleFollowerApiCall(envelope, deps) {
|
|
|
2759
2770
|
method: envelope.method,
|
|
2760
2771
|
args: envelope.args,
|
|
2761
2772
|
})) {
|
|
2762
|
-
return rejectTelegramBusRequest(envelope.requestId,
|
|
2773
|
+
return rejectTelegramBusRequest(envelope.requestId, TELEGRAM_BUS_FOLLOWER_API_NOT_ALLOWED_MESSAGE);
|
|
2763
2774
|
}
|
|
2764
2775
|
if (!deps.callApi) {
|
|
2765
2776
|
return rejectTelegramBusRequest(envelope.requestId, "Telegram bus leader does not expose API calling.");
|
|
@@ -744,6 +744,7 @@ export declare function createTelegramBusLiveRebindController(deps: {
|
|
|
744
744
|
})) => Promise<TelegramBusLiveRebindSaveObservation | TelegramBusLiveRebindApplyObservation | TelegramBusLiveRebindSettlementObservation | TelegramBusLiveRebindWorkObservation | TelegramBusLiveRebindCommandObservation | undefined>;
|
|
745
745
|
export declare function isTelegramBusEnvelopeAuthorized(envelope: TelegramBusEnvelope, secret: string | undefined): boolean;
|
|
746
746
|
/** Negative acknowledgement carrying only a diagnostic message. */
|
|
747
|
+
export declare const TELEGRAM_BUS_FOLLOWER_API_NOT_ALLOWED_MESSAGE = "Telegram bus API call is not allowed for this follower.";
|
|
747
748
|
export declare function rejectTelegramBusRequest(requestId: string, message: string): Extract<TelegramBusEnvelope, {
|
|
748
749
|
kind: "bus.ack";
|
|
749
750
|
}>;
|
|
@@ -1596,6 +1596,7 @@ export function isTelegramBusEnvelopeAuthorized(envelope, secret) {
|
|
|
1596
1596
|
return auth.length === expected.length && timingSafeEqual(auth, expected);
|
|
1597
1597
|
}
|
|
1598
1598
|
/** Negative acknowledgement carrying only a diagnostic message. */
|
|
1599
|
+
export const TELEGRAM_BUS_FOLLOWER_API_NOT_ALLOWED_MESSAGE = "Telegram bus API call is not allowed for this follower.";
|
|
1599
1600
|
export function rejectTelegramBusRequest(requestId, message) {
|
|
1600
1601
|
return { kind: "bus.ack", requestId, ok: false, message };
|
|
1601
1602
|
}
|
|
@@ -454,17 +454,32 @@ export default function (pi) {
|
|
|
454
454
|
return WorkspaceRetirement.isCurrentTelegramWorkspaceBinding(threadStore, binding);
|
|
455
455
|
},
|
|
456
456
|
});
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
457
|
+
const inactiveThreadCleanupRuntime = ThreadCleanupManager.createTelegramInactiveThreadCleanupRuntime({
|
|
458
|
+
store: threadStore,
|
|
459
|
+
captureAuthority() {
|
|
460
|
+
const ledger = workspaceAdmissionRuntime.resolve();
|
|
461
|
+
const leaderEpoch = getCurrentLeaderEpoch();
|
|
462
|
+
const operator = configStore.getAllowedUserId();
|
|
463
|
+
if (!ledger || leaderEpoch === undefined || operator === undefined)
|
|
464
|
+
return undefined;
|
|
465
|
+
const stamp = telegramTransportStampRuntime.getStamp();
|
|
466
|
+
const generation = telegramSessionContextStore.getGeneration();
|
|
467
|
+
return {
|
|
468
|
+
profileName: configStore.getActiveProfileName() ?? "default",
|
|
469
|
+
leaderEpoch, ledger,
|
|
470
|
+
isCurrent() {
|
|
471
|
+
return getCurrentLeaderEpoch() === leaderEpoch &&
|
|
472
|
+
telegramTransportStampRuntime.isActive(stamp) &&
|
|
473
|
+
configStore.getAllowedUserId() === operator &&
|
|
474
|
+
telegramSessionContextStore.getGeneration() === generation;
|
|
475
|
+
},
|
|
476
|
+
};
|
|
460
477
|
},
|
|
461
|
-
listBindings: threadStore.listWorkspaceBindings,
|
|
462
478
|
getProtection: captureWorkspaceExternalProtection,
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const profileName = configStore.getActiveProfileName() ?? "default";
|
|
479
|
+
getProcessLiveness: ProcessIdentity.getTelegramProcessLiveness,
|
|
480
|
+
runExclusive: telegramWorkspaceOperationRuntime.runExclusive,
|
|
481
|
+
getWorkStore(authority) {
|
|
482
|
+
const profileName = authority.profileName;
|
|
468
483
|
const botToken = configStore.getBotToken();
|
|
469
484
|
if (!botToken)
|
|
470
485
|
throw new Error("Telegram Thread cleanup review requires an active bot token.");
|
|
@@ -474,9 +489,16 @@ export default function (pi) {
|
|
|
474
489
|
tokenSha256: Journal.createTelegramUpdateJournalBotIdentity({
|
|
475
490
|
botToken,
|
|
476
491
|
}).tokenSha256,
|
|
492
|
+
admissionProfileKey: authority.ledger.getProfileKey(),
|
|
477
493
|
});
|
|
478
494
|
},
|
|
479
495
|
runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
|
|
496
|
+
deleteThread(authorize) {
|
|
497
|
+
return directTelegramApiRuntime.deleteWorkspaceThread(authorize);
|
|
498
|
+
},
|
|
499
|
+
onError(error) {
|
|
500
|
+
recordRuntimeEvent("telegram", error, { phase: "manual-inactive-thread-cleanup" });
|
|
501
|
+
},
|
|
480
502
|
});
|
|
481
503
|
const updateAdmissionRuntimeBinding = Updates.createTelegramUpdateAdmissionRuntimeBinding({
|
|
482
504
|
isFollowerRegistered: telegramBusFollowerRegistrationState.isRegistered,
|
|
@@ -940,7 +962,8 @@ export default function (pi) {
|
|
|
940
962
|
sendInteractiveMessage,
|
|
941
963
|
answerCallbackQuery,
|
|
942
964
|
...configControls,
|
|
943
|
-
reviewInactiveThreads:
|
|
965
|
+
reviewInactiveThreads: inactiveThreadCleanupRuntime.review,
|
|
966
|
+
cleanInactiveThreads: inactiveThreadCleanupRuntime.clean,
|
|
944
967
|
getThreadDisplayMode() {
|
|
945
968
|
return threadStore.getBotState().threadMode === "enabled"
|
|
946
969
|
? Config.resolveTelegramThreadDisplayMode(configStore.get())
|
|
@@ -1377,6 +1400,15 @@ export default function (pi) {
|
|
|
1377
1400
|
return binding.hasAuthority?.() === true;
|
|
1378
1401
|
});
|
|
1379
1402
|
},
|
|
1403
|
+
recoverRejectedRecipientTarget(error) {
|
|
1404
|
+
const rejected = error instanceof TelegramApi.TelegramApiFollowerNotAllowedError
|
|
1405
|
+
? error.requestTarget
|
|
1406
|
+
: undefined;
|
|
1407
|
+
const current = telegramBusFollowerRegistrationState.getTarget();
|
|
1408
|
+
return rejected && current?.threadId !== undefined
|
|
1409
|
+
? { rejected, current: { chatId: current.chatId, threadId: current.threadId } }
|
|
1410
|
+
: undefined;
|
|
1411
|
+
},
|
|
1380
1412
|
prepareUpdateForExecution(update) {
|
|
1381
1413
|
return BusFollower.prepareTelegramBusFollowerJournaledUpdateForExecution(update, textGroupRuntime.prepareForwardedMessage);
|
|
1382
1414
|
},
|
|
@@ -159,6 +159,7 @@ export interface TelegramBindToolRegistrationDeps extends GenerativeAppRuntimeOp
|
|
|
159
159
|
recordRuntimeEvent?: (category: string, error: unknown, details?: Record<string, unknown>) => void;
|
|
160
160
|
}
|
|
161
161
|
export declare function resolveGenerativeAppDir(agentDir: string, app: string): string;
|
|
162
|
+
export declare function isGenerativeAppInstalled(agentDir: string, app: string): Promise<boolean>;
|
|
162
163
|
export declare function resolveGenerativeAppModulePath(agentDir: string, app: string): string;
|
|
163
164
|
export declare function invokeGenerativeApp(options: GenerativeAppRuntimeOptions & {
|
|
164
165
|
argument?: unknown;
|
|
@@ -86,6 +86,15 @@ export function resolveGenerativeAppDir(agentDir, app) {
|
|
|
86
86
|
assertAppName(app);
|
|
87
87
|
return join(getAppsRoot(agentDir), app);
|
|
88
88
|
}
|
|
89
|
+
export async function isGenerativeAppInstalled(agentDir, app) {
|
|
90
|
+
try {
|
|
91
|
+
await stat(resolveGenerativeAppModulePath(agentDir, app));
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
89
98
|
export function resolveGenerativeAppModulePath(agentDir, app) {
|
|
90
99
|
return join(resolveGenerativeAppDir(agentDir, app), `${app}.mjs`);
|
|
91
100
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Transport authority, durable bindings, and queue custody remain with their owners.
|
|
6
6
|
*/
|
|
7
7
|
import * as BusFollower from "./bus-follower.ts";
|
|
8
|
-
import type { AgentEndEvent, AgentSettledEvent, AgentStartEvent, AssistantMessageEvent, BeforeAgentStartEvent, ExtensionAPI, ExtensionContext, InputEvent, MessageEndEvent, SessionBeforeCompactEvent, SessionCompactEvent, SessionCompactFailedEvent, SessionShutdownEvent, SessionStartEvent, ToolExecutionEndEvent, ToolExecutionStartEvent, ToolExecutionUpdateEvent, TurnEndEvent, TurnEndEventResult, UIPromptEndEvent, UIPromptStartEvent } from "./pi.ts";
|
|
8
|
+
import type { AgentEndEvent, AgentSettledEvent, AgentStartEvent, AssistantMessageEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, ExtensionAPI, ExtensionContext, InputEvent, MessageEndEvent, SessionBeforeCompactEvent, SessionCompactEvent, SessionCompactFailedEvent, SessionShutdownEvent, SessionStartEvent, ToolExecutionEndEvent, ToolExecutionStartEvent, ToolExecutionUpdateEvent, TurnEndEvent, TurnEndEventResult, UIPromptEndEvent, UIPromptStartEvent } from "./pi.ts";
|
|
9
9
|
import * as Machine from "./machine.ts";
|
|
10
10
|
import * as Queue from "./queue.ts";
|
|
11
11
|
export declare function createAgentStartDedupHook(inner: (event: AgentStartEvent, ctx: ExtensionContext) => Promise<void>, resetReplyDedup: () => void, schedulePublication?: (task: () => Promise<void>) => void): (event: AgentStartEvent, ctx: ExtensionContext) => Promise<void>;
|
|
@@ -14,6 +14,7 @@ type TelegramBeforeAgentStartEvent = Omit<BeforeAgentStartEvent, "systemPrompt">
|
|
|
14
14
|
};
|
|
15
15
|
export interface TelegramBeforeAgentStartResult {
|
|
16
16
|
systemPrompt?: string | string[];
|
|
17
|
+
message?: BeforeAgentStartEventResult["message"];
|
|
17
18
|
}
|
|
18
19
|
type TelegramBeforeAgentStartReturn = Promise<TelegramBeforeAgentStartResult | undefined> | TelegramBeforeAgentStartResult | undefined;
|
|
19
20
|
type TelegramLifecycleModel = ExtensionContext["model"];
|
|
@@ -49,7 +49,7 @@ export function buildAutomaticThreadCleanupSettingsText(enabled) {
|
|
|
49
49
|
"<code>-</code> <code>on</code> (default): delete the bound thread and release Telegram authority on graceful quit.",
|
|
50
50
|
"<code>-</code> <code>off</code>: preserve the tab as a restart hint; manual <code>/telegram-disconnect</code> still confirms and deletes it.",
|
|
51
51
|
"",
|
|
52
|
-
"Review inactive tabs checks current owner and work evidence. Review never deletes tabs.",
|
|
52
|
+
"On the leader instance, Review inactive tabs checks current owner and work evidence, including tabs without a letter. Review never deletes tabs.",
|
|
53
53
|
].join("\n");
|
|
54
54
|
}
|
|
55
55
|
function buildInactiveThreadReviewText(count) {
|
|
@@ -58,7 +58,8 @@ function buildInactiveThreadReviewText(count) {
|
|
|
58
58
|
"",
|
|
59
59
|
`${count} proven inactive tab${count === 1 ? "" : "s"}.`,
|
|
60
60
|
"No tabs were deleted.",
|
|
61
|
-
"
|
|
61
|
+
"Clean inactive tabs deletes these Telegram tabs and all their messages. Pi session history and project files are not removed.",
|
|
62
|
+
"Clean rechecks owner and work protection; a changed or protected tab is skipped. Choose Back to leave without deleting anything.",
|
|
62
63
|
].join("\n");
|
|
63
64
|
}
|
|
64
65
|
export function buildInactiveThreadReviewReplyMarkup(operationId, canCleanInactiveThreads = false) {
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Owns direct pi SDK imports and exposes narrow bridge-facing helpers/types for the extension composition layer
|
|
5
5
|
*/
|
|
6
6
|
import type { AssistantMessageEvent } from "@earendil-works/pi-ai";
|
|
7
|
-
import { type AgentEndEvent, type AgentSettledEvent, type AgentStartEvent, type BeforeAgentStartEvent, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type InputEvent, type MessageEndEvent, type SessionBeforeCompactEvent, type SessionCompactEvent, type SessionShutdownEvent, type SessionStartEvent, type SlashCommandInfo, type TurnEndEvent, type TurnEndEventResult, type UIPromptEndEvent, type UIPromptStartEvent } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
export type { AgentEndEvent, AgentSettledEvent, AgentStartEvent, AssistantMessageEvent, BeforeAgentStartEvent, ExtensionAPI, ExtensionCommandContext, ExtensionContext, InputEvent, MessageEndEvent, SessionBeforeCompactEvent, SessionCompactEvent, SessionShutdownEvent, SessionStartEvent, SlashCommandInfo, TurnEndEvent, TurnEndEventResult, UIPromptEndEvent, UIPromptStartEvent, };
|
|
7
|
+
import { type AgentEndEvent, type AgentSettledEvent, type AgentStartEvent, type BeforeAgentStartEvent, type BeforeAgentStartEventResult, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type InputEvent, type MessageEndEvent, type SessionBeforeCompactEvent, type SessionCompactEvent, type SessionShutdownEvent, type SessionStartEvent, type SlashCommandInfo, type TurnEndEvent, type TurnEndEventResult, type UIPromptEndEvent, type UIPromptStartEvent } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
export type { AgentEndEvent, AgentSettledEvent, AgentStartEvent, AssistantMessageEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, ExtensionAPI, ExtensionCommandContext, ExtensionContext, InputEvent, MessageEndEvent, SessionBeforeCompactEvent, SessionCompactEvent, SessionShutdownEvent, SessionStartEvent, SlashCommandInfo, TurnEndEvent, TurnEndEventResult, UIPromptEndEvent, UIPromptStartEvent, };
|
|
9
9
|
export interface SessionCompactFailedEvent {
|
|
10
10
|
type: "session_compact_failed";
|
|
11
11
|
reason: "manual" | "threshold" | "overflow";
|