@llblab/pi-kit 0.1.9 → 0.1.11

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 (35) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +2 -2
  3. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +1 -1
  4. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +6 -0
  5. package/node_modules/@llblab/pi-grow-loop/README.md +2 -1
  6. package/node_modules/@llblab/pi-grow-loop/index.ts +11 -2
  7. package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
  8. package/node_modules/@llblab/pi-telegram/AGENTS.md +4 -4
  9. package/node_modules/@llblab/pi-telegram/BACKLOG.md +0 -1
  10. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +29 -0
  11. package/node_modules/@llblab/pi-telegram/README.md +1 -1
  12. package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -5
  13. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +22 -8
  14. package/node_modules/@llblab/pi-telegram/index.ts +18 -7
  15. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +49 -13
  16. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +15 -0
  17. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +17 -4
  18. package/node_modules/@llblab/pi-telegram/lib/bus.ts +56 -2
  19. package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +65 -4
  20. package/node_modules/@llblab/pi-telegram/lib/commands.ts +122 -28
  21. package/node_modules/@llblab/pi-telegram/lib/journal.ts +2 -9
  22. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +20 -18
  23. package/node_modules/@llblab/pi-telegram/lib/locks.ts +6 -1
  24. package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +15 -4
  25. package/node_modules/@llblab/pi-telegram/lib/menu.ts +10 -7
  26. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +2 -2
  27. package/node_modules/@llblab/pi-telegram/lib/queue.ts +49 -12
  28. package/node_modules/@llblab/pi-telegram/lib/routing.ts +5 -2
  29. package/node_modules/@llblab/pi-telegram/lib/status.ts +18 -6
  30. package/node_modules/@llblab/pi-telegram/lib/sync.ts +17 -0
  31. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +91 -45
  32. package/node_modules/@llblab/pi-telegram/lib/threads.ts +5 -0
  33. package/node_modules/@llblab/pi-telegram/lib/updates.ts +17 -9
  34. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  35. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to `@llblab/pi-kit` are documented here.
4
4
 
5
+ ## 0.1.11 - 2026-08-28
6
+
7
+ - `Telegram Hotfix`: Advances `@llblab/pi-telegram` to `0.39.4`, preventing repeated extension errors when print, RPC, ACP, or other headless hosts refresh lifecycle status without an initialized theme while preserving interactive status rendering.
8
+
9
+ ## 0.1.10 - 2026-08-26
10
+
11
+ - `Extension Releases`: Advances `@llblab/pi-grow-loop` to `0.7.3` for the current bounded continuation protocol and `@llblab/pi-telegram` to `0.39.3` for Windows IPC replacement fencing, trusted outbound voice command compatibility, and macOS lock-recovery contention handling.
12
+
5
13
  ## 0.1.9 - 2026-08-24
6
14
 
7
15
  - `Telegram Release`: Advances `@llblab/pi-telegram` to `0.38.0`, presenting the default voice mode as `manual`, retaining legacy `hidden` compatibility, and removing the redundant transcript-caption configuration and provider contract.
package/README.md CHANGED
@@ -8,8 +8,8 @@
8
8
  | --- | ---: | --- |
9
9
  | `@llblab/pi-actors` | `0.50.0` | Extension and Skills |
10
10
  | `@llblab/pi-codex-usage` | `0.9.3` | Extension |
11
- | `@llblab/pi-grow-loop` | `0.7.2` | Extension and Skills |
12
- | `@llblab/pi-telegram` | `0.38.0` | Extension and Skills |
11
+ | `@llblab/pi-grow-loop` | `0.7.3` | Extension and Skills |
12
+ | `@llblab/pi-telegram` | `0.39.4` | Extension and Skills |
13
13
 
14
14
  Versions are exact by design. Updating an extension does not change an installed kit until this repository explicitly advances that dependency and publishes a new kit version.
15
15
 
@@ -23,7 +23,7 @@
23
23
  - `grow-loop` skill owns continuation semantics: continue or stop.
24
24
  - Routing selects the protocol before worker execution: explicit names override inference; otherwise concrete iterative tasks may select Grow Loop when multiple validated slices and visible checkpoints are useful.
25
25
  - `while-true` skill owns only one portable worker pass: discover one canonical work surface, including neutral `Canonical open work: <path>` declarations from relevant project-local delivery instructions, assess reality, reconcile the plan, assemble one bounded validation cohort, execute independent low-coupling tasks with per-task falsification, run shared validation, and hand off. It batches by default when diagnosis remains clear and falls back to a single task for coupled, ordered, large, high-risk, or ambiguous work. A standalone invocation must not activate or call a continuation scheduler. It must not own continuation, user-stop semantics, command phrases, runtime tools, status text, or extension-specific controls; `grow-loop` plus `grow_loop` own sequential continuation only after Grow Loop was selected.
26
- - `grow_loop` tool only schedules the next visible iteration; its optional `after_seconds` argument accepts `3` through `3600` and defaults to `3`; the minimum preserves an operator-interrupt window before every continuation. Scheduling is idle-deferred: the tool first waits until Pi is idle and no user messages are pending, then starts the configured countdown and sends the compact trigger `while true | grow loop` only if Pi is still idle and no user messages are pending. A longer delay may serve as a continuation timer while asynchronous work finishes. The agent chooses it from evidence about expected remaining duration and reassesses after every wake; the one-hour maximum is exceptional rather than a polling default. If the runtime becomes busy during the countdown, the tool returns to deferred waiting instead of queueing a hidden follow-up. Status shows deferred scheduling as `loop ∞N` with the iteration number in warning color, countdown as `loop Ns`, and active iterations as `loop ∞N` with the iteration number dimmed; `N` is monotonic within the extension instance.
26
+ - `grow_loop` tool only schedules the next visible iteration; its optional `after_seconds` argument accepts `3` through `3600` and defaults to `3`; the minimum preserves an operator-interrupt window before every continuation. Scheduling is idle-deferred: the tool first waits until Pi is idle and no user messages are pending, then starts the configured countdown and sends the compact trigger `while true | grow loop` only if Pi is still idle and no user messages are pending. A longer delay may serve as a continuation timer while asynchronous work finishes. The agent chooses it from evidence about expected remaining duration and reassesses after every wake; the one-hour maximum is exceptional rather than a polling default. If the runtime becomes busy during the countdown, the tool returns to deferred waiting instead of queueing a hidden follow-up. Status shows deferred scheduling as `loop ∞N` with the iteration number in warning color, countdown as `loop Ns`, and active iterations as `loop ∞N` with the iteration number dimmed; `N` is monotonic within the extension instance and advances once per turn that arms a deferred iteration; repeated tool calls in that turn only replace the delay while retaining the same iteration number.
27
27
  - There is no slash-command control surface. Any ordinary user prompt exits the active runtime rhythm by clearing pending scheduling and hiding loop status; restart/continuation intent belongs to the agent and Grow Loop skill, not to a runtime latch or regex.
28
28
  - No start slash commands, budgets, cycle counts, hidden processes, or background agents.
29
29
  - Loop status is dynamic: show warning-colored `loop ∞N` only while the next iteration is deferred until idle, `loop Ns` only during the configured countdown, and dim `loop ∞N` only while a loop-scheduled turn is active. Clear active status only after Pi fully settles without an armed successor; low-level run endings may still lead to retry or compaction recovery. Any user input except the runtime's exact expected continuation prompt clears pending loop scheduling and hides loop status, including operator input injected through another extension.
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.3: Turn-Scoped Iteration Hotfix
4
+
5
+ - [Runtime] Keep repeated `grow_loop` calls within one agent turn bound to the same next iteration, replacing only the deferred delay instead of incrementing the visible loop number.
6
+ - [Feedback] Report repeated same-turn scheduling with concise tool-specific copy that preserves the existing iteration identity and confirms the updated delay.
7
+ - [Evidence] Cover same-turn rescheduling before and during the countdown while preserving normal iteration advancement across visible turns.
8
+
3
9
  ## 0.7.2: Trusted Release Automation
4
10
 
5
11
  - [Release] Add tag-gated package validation, npm Trusted Publisher provenance, immutable package/tag identity checks, public registry verification, and workflow-owned GitHub Release creation through `.github/workflows/release.yml`.
@@ -131,6 +131,7 @@ A standalone `while-true` invocation ends at that handoff. Only a previously sel
131
131
  - Waits until Pi is idle and no user messages are pending.
132
132
  - Shows an interrupt countdown configured by the optional `after_seconds` argument from 3 through 3600, defaulting to 3 seconds. The minimum preserves a window for the operator to redirect the agent before continuation.
133
133
  - Can act as a continuation timer when the agent must wait for asynchronous work before checking again.
134
+ - Treats repeated calls in one agent turn as delay updates for the same next iteration, so one deferred prompt corresponds to one iteration-number increment.
134
135
  - Sends the compact prompt `while true | grow loop` only if Pi is still idle.
135
136
  - If Pi becomes busy during the countdown, returns to deferred waiting instead of queueing a hidden follow-up.
136
137
  - Shows loop status only while it is actively carrying the rhythm.
@@ -147,7 +148,7 @@ The tool never blocks future calls. Whether to continue belongs to the agent and
147
148
  - `loop Ns` countdown — Pi is idle and the configured delay is running.
148
149
  - `loop ∞N` dim — the compact loop prompt was sent for this iteration.
149
150
 
150
- `N` is monotonic within the current extension instance. Active status clears when the scheduled agent run fully settles without arming a successor, so automatic retry or compaction recovery does not produce a false idle state. There is no `loop stopped` or `loop paused` status; absence of loop status means the runtime rhythm is no longer active.
151
+ `N` is monotonic within the current extension instance and advances only once when a turn arms its deferred successor. Repeated `grow_loop` calls before that turn ends retain `N`, replace the pending delay, and report that the iteration was already scheduled. Active status clears when the scheduled agent run fully settles without arming a successor, so automatic retry or compaction recovery does not produce a false idle state. There is no `loop stopped` or `loop paused` status; absence of loop status means the runtime rhythm is no longer active.
151
152
 
152
153
  ## Interruption Model
153
154
 
@@ -124,6 +124,7 @@ export default function growLoopExtension(
124
124
  let lastCtx: ExtensionContext | undefined;
125
125
  let pendingIteration: PendingIteration | undefined;
126
126
  let ownPromptPending = false;
127
+ let scheduledThisTurn = false;
127
128
  const clearPending = () => {
128
129
  if (!pendingIteration) return;
129
130
  if (pendingIteration.timeout) clearTimeout(pendingIteration.timeout);
@@ -142,6 +143,7 @@ export default function growLoopExtension(
142
143
  });
143
144
  pi.on("session_shutdown", async () => {
144
145
  ownPromptPending = false;
146
+ scheduledThisTurn = false;
145
147
  clearPending();
146
148
  lastCtx?.ui.setStatus(STATUS_KEY, undefined);
147
149
  });
@@ -151,6 +153,7 @@ export default function growLoopExtension(
151
153
  });
152
154
  pi.on("input", async (event, ctx) => {
153
155
  lastCtx = ctx;
156
+ scheduledThisTurn = false;
154
157
  const isOwnPrompt =
155
158
  event.source === "extension" &&
156
159
  ownPromptPending &&
@@ -189,7 +192,11 @@ export default function growLoopExtension(
189
192
  lastCtx = ctx;
190
193
  ownPromptPending = false;
191
194
  clearPending();
192
- iteration += 1;
195
+ const isReschedule = scheduledThisTurn;
196
+ if (!isReschedule) {
197
+ iteration += 1;
198
+ scheduledThisTurn = true;
199
+ }
193
200
  const nextIteration = iteration;
194
201
  const delayMs =
195
202
  params.after_seconds === undefined
@@ -209,7 +216,9 @@ export default function growLoopExtension(
209
216
  content: [
210
217
  {
211
218
  type: "text",
212
- text: `\nGrow Loop iteration #${nextIteration} deferred until idle, then scheduled after ${delayMs / 1000}s delay`,
219
+ text: isReschedule
220
+ ? `\nTool grow_loop was already called this turn. Iteration #${nextIteration} remains scheduled; delay updated to ${delayMs / 1000}s`
221
+ : `\nGrow Loop iteration #${nextIteration} deferred until idle, then scheduled after ${delayMs / 1000}s delay`,
213
222
  },
214
223
  ],
215
224
  details: { iteration: nextIteration, delayMs },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-grow-loop",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "private": false,
5
5
  "description": "Semantic loop-engineering for agent-owned, visible, interruptible continuation in Pi",
6
6
  "keywords": [
@@ -80,14 +80,14 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
80
80
  - 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.
81
81
  - 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.
82
82
  - 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. 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.
83
- - Startup and elapsed time are not owner-death proof; queued authority has no time lease. Dead-owner recovery groups the complete receipt and transactionally rechecks pid liveness plus process-birth identity: only an absent PID or mismatched stable Linux/macOS birth proof returns all sources to `pending`; a matching proof is `alive`, while Windows or inaccessible birth metadata is `unverifiable`, and both non-dead outcomes keep authority queued. The live-transfer contract is authenticated offer → exact-generation bounded payload staging → recipient CAS acceptance → exact receipt-and-owner ACK → donor removal → recipient readiness. The offer freezes donor settlement/recovery; controls rebuild local closures; negative/mismatched pre-acceptance ACK cancels only an unaccepted offer and retains donor work; a lost post-acceptance ACK cannot cancel recipient authority and leaves donor memory frozen for explicit reconciliation.
84
- - Execution failures persist bounded diagnostics and attempt state as `retry-wait`. Automatic retry continues indefinitely with exponential `1s → 2s → 4s → 8s → 16s → 32s → 60s` delay capped at 60 seconds; later independent updates continue draining, durable authority is never silently discarded, and legacy `failed` entries resume automatically at startup. Snapshot-plus-segment journals compact only after 256 unapplied revisions or 4 MiB; snapshot-first cleanup tolerates redundant segments, and empty authority may atomically rebind bot/profile identity. Missing snapshots left by the retired broad temp cleanup rebuild only from a complete provably empty segment chain, while revisionless snapshots may recover from a validated later segment predecessor; otherwise the snapshot and segments move atomically under `tmp/telegram/recovery/` before a fresh journal is published and startup continues with informational recovery evidence.
83
+ - Startup and elapsed time are not owner-death proof; queued authority has no time lease. Dead-owner cleanup groups the complete receipt and transactionally rechecks pid liveness plus process-birth identity: only an absent PID or mismatched stable Linux/macOS birth proof discards all session-owned sources without replay; a matching proof is `alive`, while Windows or inaccessible birth metadata is `unverifiable`, and both non-dead outcomes keep authority queued. The live-transfer contract is authenticated offer → exact-generation bounded payload staging → recipient CAS acceptance → exact receipt-and-owner ACK → donor removal → recipient readiness. The offer freezes donor settlement/recovery; controls rebuild local closures; negative/mismatched pre-acceptance ACK cancels only an unaccepted offer and retains donor work; a lost post-acceptance ACK cannot cancel recipient authority and leaves donor memory frozen for explicit reconciliation.
84
+ - Execution failures persist bounded diagnostics and attempt state as `retry-wait`, except that an exact Telegram HTTP 400 stale/deleted-thread API failure with a proven `{chatId, threadId}` terminally settles the currently executing source after best-effort shared binding invalidation. Automatic retry continues indefinitely with exponential `1s → 2s → 4s → 8s → 16s → 32s → 60s` delay capped at 60 seconds; later independent updates continue draining, durable authority is never silently discarded, and legacy `failed` entries resume automatically at startup. Snapshot-plus-segment journals compact only after 256 unapplied revisions or 4 MiB; snapshot-first cleanup tolerates redundant segments, and empty authority may atomically rebind bot/profile identity. Missing snapshots left by the retired broad temp cleanup rebuild only from a complete provably empty segment chain, while revisionless snapshots may recover from a validated later segment predecessor; otherwise the snapshot and segments move atomically under `tmp/telegram/recovery/` before a fresh journal is published and startup continues with informational recovery evidence.
85
85
  - An unresolved reaction delays only the exact governed queue item identified by chat/message sources, not unrelated queue work. Queue receipt publication follows in-memory append and precedes dispatch request; receipt-bearing turns remain queued until every exact source commits.
86
86
  - The detailed implementation and release gates live in [`docs/architecture.md`](./docs/architecture.md), [`docs/multi-instance-bus.md`](./docs/multi-instance-bus.md), and [`BACKLOG.md`](./BACKLOG.md).
87
87
 
88
88
  ### 4.4 Queue, Delivery, And User Surfaces
89
89
 
90
- - Queue lane/kind admission is explicit. Dispatch waits for active-turn, pending-dispatch, control, compaction, `ctx.isIdle()`, and Pi pending-message guards; a dispatched prompt stays queued until `agent_start` consumes it. Each prompt is one object with one active lane and no reserved return slot. Normal and Priority are separate FIFO lanes: crossing lanes removes it from the source and appends it at the destination tail, while Keep/Skip and same-category emoji changes preserve lane position. Complete reaction sets independently derive Priority from recognized positive emoji and Skip from recognized negative emoji; both may coexist, Skip wins only at dispatch, and suppressed turns remain visible without blocking unrelated work.
90
+ - Queue lane/kind admission is explicit. Dispatch waits for active-turn, pending-dispatch, control, compaction, `ctx.isIdle()`, and Pi pending-message guards; a dispatched prompt stays queued until `agent_start` consumes it. Each prompt is one object with one active lane and no reserved return slot. Normal and Priority are separate FIFO lanes: crossing lanes removes it from the source and appends it at the destination tail, while Keep/Skip and same-category emoji changes preserve lane position. Complete reaction sets independently derive Priority from recognized positive emoji and Skip from recognized negative emoji; both may coexist, suppressed turns retain durable receipts while waiting, and Skip settles them only when the prompt reaches dispatch before dropping it without inference. Suppressed turns remain visible at a struck-through physical ordinal without contributing to executable queue counters, while graceful session shutdown discards all remaining queue authority before clearing memory.
91
91
  - `/stop`, `/abort`, `/next`, and `/continue` respectively reset+abort, abort while preserving queue, force the next turn, and enqueue a control-lane continuation. Abort-history folding applies only to Telegram-owned active turns.
92
92
  - Telegram extension side effects must not hold Pi's core lifecycle hostage after semantic completion. Preserve ordering in extension-owned background work, record failures, and fence target/profile/transport/session authority.
93
93
  - Complete assistant/guest model answers use Telegram-native Rich Markdown. Harness-owned menus, status, diagnostics, thinking, and tool evidence remain explicit HTML/plain or their documented native surface. Preserve literal code and structurally safe chunking; never split invalid markup.
@@ -125,7 +125,7 @@ The detailed map is canonical in [`docs/architecture.md`](./docs/architecture.md
125
125
  - Keep interfaces consistent with their owning exported contract. Use local structural `*Like`/view types only for deliberate narrow projections, not duplicate source-of-truth models.
126
126
  - Remove dead code immediately. Reachability from composition roots, public exports, tests, registered surfaces, and documented APIs—not recent usefulness—determines whether code is live.
127
127
  - Treat every meaningful `index.ts` edit as a composition-pressure check, but keep one-off live adapter wiring there when extraction would only hide cross-domain state.
128
- - Follow [`docs/ui-style.md`](./docs/ui-style.md) for interface copy, emoji, buttons, menus, and dialogs. Update the registry before assigning a new UI emoji meaning.
128
+ - Follow [`docs/ui-style.md`](./docs/ui-style.md) for interface copy, emoji, buttons, menus, and dialogs. Update the registry before assigning a new UI emoji meaning. Standalone notices use one fully bold emoji-led heading with terminal punctuation; callback alerts preserve equivalent emoji-led plain text because Telegram does not support rich formatting there.
129
129
  - Markdown tables use compact source formatting with `---` separator cells and one surrounding space per cell. Preserve vendored references unchanged.
130
130
  - Treat Windows filesystem, named-pipe, lock, heartbeat, and atomic-rename reports as high-signal evidence; reduce them to regressions or explicit platform caveats.
131
131
  - Route significant runtime failures through the redacted recent-event recorder. Keep the compact TUI status at generic `error`; details belong in diagnostics.
@@ -2,7 +2,6 @@
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
- - [ ] `Coherent voice reply policy`: Release the validated `manual`, `mirror`, and `always` policy cleanup as `0.38.0`, then reconcile issue #235 and open exact migration issues for `johnlam1968/pi-voice-telegram` and `luxus/pi-xai-voice`, whose TTS providers still consume the removed transcript-caption API.
6
5
  - [ ] `Operator-confirmed pairing`: Replace first-contact automatic ownership with an explicit pending request that the trusted Pi interface can allow or reject before `allowedUserId` is persisted or any Telegram prompt executes; retain manual preconfiguration, bound pending-request lifetime and multiplicity, fail closed across session/transport replacement, and cover unauthorized-first-contact races.
7
6
  - [ ] `Environment-backed bot tokens`: Let each profile store an exact Pi-native `$ENV_VAR` or `${ENV_VAR}` reference in `telegram.json` instead of copying the resolved secret; preserve the originating alias when `/telegram-setup` prefills from a supported environment variable, resolve only at validation/activation boundaries, fail closed with a redacted named-variable diagnostic when unresolved, retain literal-token compatibility, and cover named profiles, reload, persistence, status, and secret-redaction paths.
8
7
  - [ ] [`Inference bypass Generative Apps`](./docs/generative-apps.md): Let the agent install and bind one managed JavaScript app owner for a generated-prompt prefix so deterministic controls can bypass model inference without creating another button grammar.
@@ -2,6 +2,35 @@
2
2
 
3
3
  > Each release keeps at most 8 outcome records of at most 512 characters.
4
4
 
5
+ ## 0.39.4: Headless Status Bar Hotfix
6
+
7
+ - `Headless Hosts`: Skips status-bar rendering when a print, RPC, ACP, or other non-interactive host has not initialized its theme, preventing repeated extension errors during lifecycle status refreshes while preserving normal interactive status updates and error propagation.
8
+
9
+ ## 0.39.3: Windows IPC And Outbound Voice Hotfix
10
+
11
+ - `Windows IPC Replacement`: Replaces an older same-process Named Pipe server before a new session generation listens on the stable endpoint, preventing `EADDRINUSE` during reload while keeping a late stop from invalidating its successor.
12
+ - `Windows IPC Fencing`: Applies the ownership commit fence before publishing a Named Pipe, so a stale generation that loses authority cannot expose an endpoint or block its replacement.
13
+ - `Windows Voice Commands`: Executes trusted `.cmd` and `.bat` outbound-handler wrappers through escaped `%ComSpec%` adaptation while preserving direct shell-free execution for native executables, bounded process controls, npm command-shim argument isolation, and paths containing spaces.
14
+ - `Windows Template Paths`: Preserves backslashes in quoted and unquoted Windows executable and artifact paths without breaking intentional escaped whitespace or quotes, allowing configured TTS handlers to produce and upload OGG/Opus voice replies through direct and follower transport.
15
+
16
+ ## 0.39.2: Deleted Thread Receipt Hotfix
17
+
18
+ - `Deleted Thread Receipt`: Terminally settles the currently executing durable update when Telegram returns exact HTTP 400 stale/deleted-thread evidence for its `{chatId, threadId}`, including leader-first shared-store invalidation followed by follower settlement. Transient, ambiguous, unrelated, and stale-looking HTTP 5xx failures retain indefinite retry authority; persisted follower records remain restart hints rather than speculative live registrations.
19
+
20
+ ## 0.39.1: Compaction Failure Formatting Hotfix
21
+
22
+ - `Compaction Failure Formatting`: Preserves the HTML parse mode when a confirmed `/compact` callback reports asynchronous failure, preventing bold standalone failure headings from appearing as literal `<b>…</b>` markup in Telegram.
23
+
24
+ ## 0.39.0: Session-Owned Queue Lifecycle
25
+
26
+ - `Queue Counters`: Excludes Skip-marked prompts immediately from the shared executable queue count used by the Pi status bar and Telegram main menu while preserving physical queue position, deferred drop semantics, and reversible Keep behavior.
27
+ - `Skip Settlement`: Preserves durable receipts while a skipped prompt waits, then settles them atomically when that prompt reaches dispatch before dropping it without inference; settlement failure retains the head instead of creating replay ambiguity.
28
+ - `Session-Owned Queue`: Graceful shutdown discards all remaining queue receipts before clearing memory, while startup atomically discards receipts whose former process is proven dead instead of replaying them; replacement and unrelated sessions therefore start without inherited work.
29
+ - `Consistent Information Headings`: Standardizes standalone progress, success, empty, busy, unavailable, cancellation, and failure notices as fully bold emoji-led headings, while callback alerts retain equivalent plain text and automatic compaction waits for the preceding final answer before announcing its start.
30
+ - `Queue Menu`: Keeps skipped prompts visible in place with their physical ordinal struck through in list and detail views, so deferred removal is legible without renumbering or moving later work.
31
+ - `Agent Context`: Routes connected and Telegram-turn guidance across `telegram-bridge`, `generated-control-surface`, and `generative-apps` by activation role, asks the agent to load only missing Skill instructions, and removes Telegram tool context while transport is unavailable.
32
+ - `Bot API Reference`: Updates the vendored Telegram Bot API reference and lookup Skill through Bot API 10.3 while retaining the 10.0–10.3 freshness history and validated symbol, anchor, and line indexes.
33
+
5
34
  ## 0.38.0: Coherent Voice Reply Policy
6
35
 
7
36
  - `Voice Reply Policy`: Renames the default user-facing `hidden` mode to `manual` while retaining `hidden` as a read-only configuration and callback compatibility alias. `manual`, `mirror`, and `always` remain the complete policy set: explicit `telegram_voice`, modality mirroring, or automatic voice replacement.
@@ -199,7 +199,7 @@ Queue policy:
199
199
  - One prompt is one queue object with exactly one current lane and one current position; it never reserves a shadow place in the other lane.
200
200
  - Priority and Normal are separate FIFO lanes; Priority dispatches first.
201
201
  - Moving `Normal → Priority` removes the prompt from Normal and places it at the Priority tail. Moving `Priority → Normal` removes it from Priority and places it at the Normal tail; no former position is restored.
202
- - Keep/Skip never changes lane position. Skip remains reversible while waiting and drops the prompt without a model turn only when dispatch reaches it.
202
+ - Keep/Skip never changes lane position. Skip preserves durable authority while waiting so Keep remains reversible, then settles that authority and drops the prompt without a model turn when dispatch reaches it. Skipped prompts stay visible at their physical queue position with a struck-through ordinal, but are excluded immediately from the executable queue count shown in both the Pi status bar and Telegram main menu. Graceful session shutdown discards all remaining queued authority, so a new session starts empty.
203
203
  - Reactions control two independent dimensions; changing one category preserves the other:
204
204
  - `Positive`: `👍`, `⚡️`, `❤️`, `🕊`, `🔥` — controls Priority.
205
205
  - `Negative`: `👎`, `👻`, `💔`, `💩`, `🗑` — controls Skip.
@@ -209,17 +209,17 @@ The profile-scoped journal separates transport progress from semantic progress.
209
209
 
210
210
  Compaction runs under the journal transaction lock when either 256 unapplied segments or 4 MiB of segment bytes is reached. It publishes the complete private (`0600`) snapshot at revision `R` before best-effort deletion of segments `<= R`; failed cleanup leaves redundant segments that readers ignore. Interrupted cleanup therefore leaves either an older snapshot plus newer authoritative segments or a newer snapshot plus harmless redundant older segments. Revision gaps, conflicting duplicates, malformed segments, and identity mismatches fail closed. The logical reconstructed journal and aggregate unapplied segment bytes are independently bounded at 10,000 entries and 32 MiB as applicable; rejected growth publishes neither snapshot nor segment bytes. Compaction may temporarily require exactly one private complete snapshot of at most 32 MiB. Capacity pauses polling and valid authority files are never automatically deleted, reset, or quarantined. Only a missing-snapshot history that cannot be reconstructed safely uses the explicit evidence-preserving quarantine-and-reset compatibility fallback above.
211
211
 
212
- `pending` entries remain immediately executable while raw interception, routing, or grouping is incomplete. Every execution failure becomes `retry-wait` with durable attempt count, next eligible time, failure class, bounded summary, and latest failure time. The `failed` state remains schema-compatible only for legacy candidate journals and is converted to automatic retry during lifecycle startup. `queued` entries carry exact prompt/control receipts plus the acquiring Pi runtime instance, OS pid/birth identity, session generation, acquisition id, and acquisition time. Queueing alone is never completion.
212
+ `pending` entries remain immediately executable while raw interception, routing, or grouping is incomplete. Execution failures become `retry-wait` with durable attempt count, next eligible time, failure class, bounded summary, and latest failure time, except that an exact HTTP 400 stale/deleted Telegram thread error carrying its proven request `{chatId, threadId}` terminally settles the currently executing source after shared binding invalidation; follower settlement remains idempotent when the leader already persisted that stale binding. The `failed` state remains schema-compatible only for legacy candidate journals and is converted to automatic retry during lifecycle startup. `queued` entries carry exact prompt/control receipts plus the acquiring Pi runtime instance, OS pid/birth identity, session generation, acquisition id, and acquisition time. Queueing alone is never completion.
213
213
 
214
214
  Queue receipt ownership is independent from the Telegram transport lock. A same-instance, same-process generation may reconstruct its local receipt across a fenced session replacement and may settle it after transport ownership moves. A different process reports the receipt as foreign, never republishes it into its local queue, and cannot complete it even if it reads the acquisition id. Startup no longer treats process replacement as proof that an owner died: foreign and legacy unowned receipts remain durable.
215
215
 
216
- Recovery and live handoff are compare-and-set under the journal transaction. Queue discard during exact queue-lifecycle cancellation requires the exact local owner/acquisition and removes all receipt sources atomically. Before admission worker start, the lifecycle groups each foreign receipt and asks the journal to recheck OS pid liveness plus process-birth identity under the same transaction; a live owner returns `owner-alive` and a live owner without stable birth proof returns `owner-unverifiable`, both without mutation, while exact negative proof converts the complete receipt back to `pending`. Replacement registration carries its exact pid/process-birth before this check; when registration and recovery race, that live identity wins the liveness proof and the queued receipt remains untouched. Only then does the replacement worker start, replay, and acquire a fresh receipt id/acquisition, fencing every stale owner.
216
+ Cleanup and live handoff are compare-and-set under the journal transaction. Queue discard during exact queue-lifecycle cancellation requires the exact local owner/acquisition and removes all receipt sources atomically. Before admission worker start, the lifecycle groups each foreign receipt and asks the journal to recheck OS pid liveness plus process-birth identity under the same transaction; a live owner returns `owner-alive` and a live owner without stable birth proof returns `owner-unverifiable`, both without mutation, while exact negative proof atomically discards the complete session-owned receipt without replay. Replacement registration carries its exact pid/process-birth before this check; when registration and cleanup race, that live identity wins the liveness proof and the queued receipt remains untouched. A replacement or unrelated worker therefore never executes queue work whose prior owner is proven dead.
217
217
 
218
218
  Authenticated live handoff uses journal CAS plus bounded local IPC. The donor creates a one-time high-entropy token and durably offers the complete receipt to one exact recipient runtime/process/session identity; the journal stores only a digest bound to queue kind, receipt sources, donor acquisition, and recipient identity. While offered, donor completion/discard and dead-owner recovery fail closed, so authority cannot disappear during payload transfer. Prompt payloads carry all queue fields; control payloads carry only their stable `status`/`model` identity and rebuild executable closures locally. The separately negotiated `queue-handoff-v1` capability gates this envelope for leader and both peer generations. Each receipt carries its exact source journal binding; the donor derives the recipient follower-journal binding from the authenticated stable follower profile before routing. The bus validates payload shape/size and exact donor/recipient registration generations, and the recipient selects only that matching active lifecycle, stages one complete receipt idempotently, accepts the journal CAS, and returns the exact receipt plus newly minted owner in its ACK. Malformed, legacy-unbound, inactive-generation, or unavailable bindings fail closed.
219
219
 
220
220
  During recipient staging, presenting the token atomically replaces the journal owner with a fresh acquisition carrying the handoff digest, removes the offer, and permanently fences donor settlement. The donor treats only an ACK carrying that exact accepted owner as success and never repeats acceptance against a donor-bound journal runtime. The recipient can repeat the same acceptance idempotently; a different token cannot claim an already accepted receipt. The coordinator contract orders offer → stage/accept exact receipt-and-owner ACK → donor removal → recipient readiness for direct leader→follower and follower→follower routing. Before acceptance, negative or mismatched acknowledgement exactly cancels the offer and keeps donor work. After acceptance, a lost acknowledgement cannot roll authority back: cancellation fails closed and donor memory remains frozen until exact accepted-owner reconciliation removes it. Recipient registration carries exact process-birth/session identity, and staged payloads remain outside the live dispatch store until accepted journal authority has been reconstructed. Production advertises `queue-handoff-v1` only with this exact role/journal selection and uses the same coordinator ordering for direct leader→follower and follower→follower routes.
221
221
 
222
- Queued semantic authority has no elapsed-time lease. A timeout cannot prove either owner death or effect quiescence, so it cannot safely recover a receipt. Resolution is limited to authenticated live handoff, exact owner discard/settlement, or transaction-rechecked negative PID plus process-birth evidence. Live or unverifiable owners remain queued indefinitely rather than risking duplicate execution.
222
+ Queued semantic authority has no elapsed-time lease. A timeout cannot prove either owner death or effect quiescence, so it cannot safely resolve a receipt. Resolution is limited to authenticated live handoff, exact owner discard/settlement, or transaction-rechecked negative PID plus process-birth evidence that permits terminal cleanup without replay. Live or unverifiable owners remain queued rather than risking duplicate or cross-session execution.
223
223
 
224
224
  The initial `offset: -1` cursor bootstrap is allowed only when both cursor and journal are absent or empty. Thereafter process-level ordering is journal atomic rename → one monotonic offset atomic rename → worker signal. Failure before journal publication leaves the offset unchanged; failure after journal publication but before offset publication permits Telegram redelivery and journal dedupe; failure after offset publication but before worker signal replays from the journal on restart. Queue-owner, retry, terminal, handoff, and completion transitions use the same journal publication primitive and therefore share this process-crash boundary. The final completion window is at-least-once, so replay-sensitive external effects must use `update_id` or the stable delivery id as an idempotency key.
225
225
 
@@ -236,7 +236,7 @@ The canonical update transition contract is:
236
236
  - `retry-wait → executing`: only after `nextRetryAtMs`; repeated signals before eligibility do not execute the entry. Automatic retries continue indefinitely with exponential `1s → 2s → 4s → 8s → 16s → 32s → 60s` delay capped at `60s`, while later independent updates continue draining.
237
237
  - Legacy `failed → retry-wait`: startup atomically resumes terminal entries written by earlier `0.28.0` candidates. Runtime policy never silently discards durable inbound authority and exposes no Pi command for manual retry/discard.
238
238
  - `queued → offered → staged → queued`: only the exact persisted donor may offer or cancel a live handoff; an offer preserves donor ownership but freezes ordinary settlement and recovery. Authenticated bounded IPC stages one exact payload/receipt outside the live queue. Exact recipient acceptance mints a fresh acquisition, reconstructs local ownership, removes donor work, then publishes recipient dispatch readiness.
239
- - `queued → completed | pending`: only the exact persisted owner receipt may complete or discard queued sources; generic completion rejects queued state. Process-birth-proven owner death may atomically recover the complete unoffered receipt to `pending`; live, unverifiable, or offered owners remain queued.
239
+ - `queued → completed`: only the exact persisted owner receipt may complete or discard queued sources; generic completion rejects queued state. Process-birth-proven owner death atomically discards the complete unoffered session-owned receipt without replay; live, unverifiable, or offered owners remain queued.
240
240
 
241
241
  The worker executes at most 64 eligible entries from one validated journal snapshot, commits ordinary completions through one journal transaction, then yields through a generation-checked event-loop boundary. Retry, queue, or prior-generation boundaries first flush completed ids and force a fresh snapshot, preserving exact state-transition atomicity without per-entry parse/rewrite churn. A deterministic 2,048-entry stress gate requires exactly 32 completion publications, 33 reads including the final empty snapshot, continued 1ms timer progress, and less than 250ms maximum observed heartbeat delay. Byte-capacity tests cover failed and retry-wait diagnostics, queue receipt/owner and handoff metadata, and operator dispositions; every rejected growth leaves the prior authority bytes unchanged. It still scans later independent entries after retry or terminal persistence. An unresolved reaction remains a queue-mutation dependency even in `retry-wait` or `failed`, but dispatch checks that dependency against the candidate queue item's exact chat and source message ids instead of globally blocking unrelated targets. Successful replay or an exact discard disposition releases the dependency. Worker state, debug status, state snapshots, and redacted runtime events expose journal depth, retry/terminal counts, the next retry, latest terminal identity, copyable operator commands, and the exact first foreign queued owner identity (instance, PID/birth, session, and acquisition) when semantic authority belongs to another process.
242
242
 
@@ -270,7 +270,7 @@ A prompt is one queue object with exactly one active lane membership and one cur
270
270
 
271
271
  Telegram reactions are shortcut controls over those dimensions. Positive reactions (`👍`, `⚡️`, `❤️`, `🕊`, `🔥`) control Priority; negative reactions (`👎`, `👻`, `💔`, `💩`, `🗑`) control Skip. The runtime compares the complete old and new reaction sets and mutates only categories that changed, so adding or removing a negative reaction cannot silently change Priority, and changing a positive reaction cannot silently change Skip. The listed order selects the retained display emoji when several recognized emoji from one category coexist; it does not let one category override the other.
272
272
 
273
- Priority and Skip may coexist, including `👍 + 💩`. The prompt remains at its Priority-lane position while waiting, but Skip wins when dispatch reaches it: the dispatcher drops it without a model turn and continues. A skipped prompt remains visible with only its negative emoji, survives authenticated queue handoff, and can return to Keep until it reaches the head. Queue item detail exposes symmetric Priority/Normal and Keep/Skip selectors instead of an irreversible Delete action.
273
+ Priority and Skip may coexist, including `👍 + 💩`. The prompt remains at its Priority-lane position while waiting, and its durable journal receipts remain intact so Keep stays reversible and exact live handoff remains possible. Skip wins when dispatch reaches the prompt: the dispatcher first settles those receipts durably, then drops it without a model turn and continues; settlement failure retains the skipped head instead of allowing replay ambiguity. The prompt stays visible with only its negative emoji, is excluded immediately from the executable queue count shared by the Pi status bar and Telegram main menu, and keeps a struck-through physical ordinal. Returning it to Keep restores its contribution to the count without moving it. Graceful session shutdown discards all remaining queue receipts before clearing session-local memory, and startup discards receipts only after proving their former process dead, so a new or unrelated session never inherits queued work. Queue item detail exposes symmetric Priority/Normal and Keep/Skip selectors instead of an irreversible Delete action.
274
274
 
275
275
  Menu and reaction controls share the same canonical queue state. A menu Keep can clear internal Skip without changing Priority or queue position, but Telegram's Bot API cannot remove a reaction created by the user; the visible user reaction can therefore remain until that user removes it. Once Pi has consumed or dropped a prompt, later reactions cannot retract or restore it.
276
276
 
@@ -26,8 +26,8 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
26
26
  | `🧠` | Thinking level | `/thinking`, thinking menu headings, thinking status rows | Use only for reasoning/thinking controls. |
27
27
  | `🔢` | Queue list / ordered work | `/queue`, queue menu entrypoints | Queue item rows may also use numeric labels. |
28
28
  | `⏱️` | Queue is ticking / current work is active | Inline main-menu Queue row only | Running-clock queue state: the narrow present moment is being worked now. |
29
- | `⏳` | Queue has waiting prompts | Inline main-menu Queue row only | Hourglass queue state: sand above the neck is future work still waiting. |
30
- | `⌛` | Queue is empty / standing idle | Inline main-menu Queue row only | Standing hourglass queue state: no future work is waiting above the neck. |
29
+ | `⏳` | Waiting / temporarily busy | Inline main-menu Queue row, busy notices | Hourglass means work or availability is pending; the sentence must name what is busy. |
30
+ | `⌛` | Empty / standing idle | Inline main-menu Queue row, empty-queue notices | Standing hourglass means no future work is waiting above the neck. |
31
31
  | `⚙️` | Settings / configuration | Settings menu headings and Settings navigation rows | Extension-injected rows appear before the built-in `⚙️ Settings` row. |
32
32
  | `🧩` | Extension-provided surface | Extension command examples, extension section examples | Companion extensions may choose their own emoji, but `🧩` means generic extension/plugin. |
33
33
  | `👄` | Voice reply policy | Voice reply settings row and detail card | Not a generic audio attachment marker. |
@@ -43,19 +43,28 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
43
43
  | --- | --- | --- | --- |
44
44
  | `🟢` | Start / active / current positive state | `/start`, active row, current selected option, active `On` toggle | In command context it means “open/start menu”; in state context it means selected/active. |
45
45
  | `🗜` | Compact session | `/compact`, compact confirmation action | Do not use for generic cleanup/delete. |
46
- | `⏩` | Force next queued turn | `/next` command and matching menu action | Means skip/advance to next waiting item. |
47
- | `▶️` | Continue/resume generation | `/continue` command and matching menu action | Means resume/continue current session flow, not force-next. |
48
- | `⏹️` | Abort current Pi work | `/abort` command description | Stops active work but is not a destructive queue clear by itself. |
49
- | `🟥` | Stop / abort-and-clear danger | `/stop` command description | Stronger than `⏹️`; use for disruptive stop/clear semantics. |
46
+ | `⏩` | Abort and advance | Busy `/next` command result and matching menu action | Means the active turn is aborted before advancing to queued work. |
47
+ | `▶️` | Play / continue immediately | Idle `/next` result, `/continue` command, and matching menu action | Means work can start or resume directly without first aborting an active turn. |
48
+ | `⏹️` | Abort current Pi work | `/abort` command description and active `/stop` result | Stops active work; accompanying copy states separately when queued work is cleared. |
49
+ | `🟥` | Destructive stop command | `/stop` command description | Strong warning at the command/action entrypoint; standalone results use the more precise idle or abort state icon. |
50
50
  | `🆕` | New session / fresh start | Reserved visible extension command example for `/new`-like flows | Same-thread Telegram `/new` is currently blocked by Pi core API; keep this meaning reserved. |
51
51
  | `🌀` | Refresh | Queue refresh row and future refresh buttons | Re-fetch/re-render current surface, not transport reconnect. |
52
52
  | `↪️` | Reroute to an existing target | Thread chooser buttons that send a captured command/message from one thread to another live thread | Curved arrow means the message arrived here but bends to another target. |
53
53
  | `🔁` | Replace/restore mode | Thread replace/restore chooser entrypoints | Opens a second step for moving a Pi instance binding to the current source thread. |
54
54
  | `➡️` | Choose replacement target | Thread replace/restore target buttons that select which Pi instance should move to the current thread | Use inside the second replace/restore chooser, not for ordinary reroutes. |
55
55
  | `☑️` | Activate / choose this item | Model detail activation action, generated button-only choice heading | Positive selection cue; use `🟢 Active` for already-current state. |
56
- | `❌` | No / cancel | Confirmation cancel buttons | Use for safe cancellation, not destructive removal. |
56
+ | `❌` | No / cancel / terminal failure | Confirmation cancel buttons and terminal failure notices | Do not use for a recoverable operation failure that leaves session state intact. |
57
57
  | `🗑` | Delete / defer removal | Destructive confirmations and removal reaction | In the queue menu, reversible Keep/Skip selectors replace immediate deletion. |
58
58
 
59
+ ### Informational Feedback
60
+
61
+ | Emoji | Meaning | Canonical surfaces | Notes |
62
+ | --- | --- | --- | --- |
63
+ | `💤` | Nothing active | No-active-turn notices | Neutral idle result, not an error. |
64
+ | `✅` | Completed successfully | Compaction and other completion notices | Use only after the operation has completed. |
65
+ | `🚫` | Unavailable, denied, or cancelled operation | Missing capability/auth notices, access denial, and compaction cancellation | Callback alerts use the same emoji without bold markup. |
66
+ | `⚠️` | Recoverable operation failure | Compaction failure notice | The attempted operation failed, but the original session state remains usable. |
67
+
59
68
  ### State Indicators And Button Grammars
60
69
 
61
70
  | Emoji | Meaning | Canonical surfaces | Notes |
@@ -71,6 +80,8 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
71
80
 
72
81
  Queue reactions are shortcut controls for waiting turns. Preserve their semantics across Telegram reactions, queue-menu rows, status previews, and tests. Positive emoji control the Priority/Normal lane; negative emoji control Keep/Skip. These categories are independent, may coexist, and mutate only their own dimension. Crossing lanes appends the prompt at the destination FIFO tail; changing Keep/Skip or changing emoji within one category preserves lane position. Skip wins only when dispatch reaches the prompt.
73
82
 
83
+ Skipped queue ordinals strike only the numeric position. Detail HTML closes `<s>` before the period; list-button labels place a hair-space boundary between the combining-struck number and the plain period so Telegram font overhang cannot visually strike punctuation.
84
+
74
85
  Queue item detail renders two independent selector rows:
75
86
 
76
87
  - `🟡 Priority` / `⚫️ Normal` or `⚫️ Priority` / `🟣 Normal` selects the lane.
@@ -209,11 +220,14 @@ Examples:
209
220
 
210
221
  ## Message Cards
211
222
 
212
- Message cards sent by the bot should start with a strong heading.
223
+ Message cards and standalone informational notices sent by the bot should start with a strong heading.
213
224
 
214
225
  Rules:
215
226
 
216
227
  - Start with a bold heading or, for dialogs, a bold question.
228
+ - Format standalone notices as one fully bold line: relevant emoji, one space, concise sentence, and terminal period or colon.
229
+ - Apply the same hierarchy to success, progress, empty, busy, unavailable, cancellation, and failure notices; do not bold only a fragment of a standalone notice.
230
+ - Callback alerts remain plain text because Telegram does not support rich text there, but still keep the relevant emoji and concise sentence.
217
231
  - Setting detail cards may include an emoji in the heading, then a colon and the current value in `<code>`.
218
232
  - Explain what the setting does and what the options mean only as much as needed.
219
233
  - Order setting value descriptions exactly like the chooser: rows top-to-bottom and values in a shared row left-to-right. Keep `(default)` on the actual default wherever it falls; default status never changes order.
@@ -359,6 +359,7 @@ export default function (pi: Pi.ExtensionAPI) {
359
359
  getActiveToolExecutions: lifecycle.getActiveToolExecutions,
360
360
  hasPendingModelSwitch: pendingModelSwitchStore.has,
361
361
  getQueuedItems: telegramQueueStore.getQueuedItems,
362
+ getQueuedItemCount: Queue.countExecutableTelegramQueueItems,
362
363
  formatQueuedStatus: Queue.formatQueuedTelegramItemsStatus,
363
364
  getRecentRuntimeEvents: runtimeEvents.getEvents,
364
365
  getRuntimeLockState: lockRuntime.getStatusLabel,
@@ -843,6 +844,16 @@ export default function (pi: Pi.ExtensionAPI) {
843
844
  });
844
845
  },
845
846
  );
847
+ const staleTopicApiErrorRecoveryDeps = {
848
+ topicTargetStore: threadStore,
849
+ getSyncState: telegramSyncStateRuntime.getState,
850
+ setSyncState: telegramSyncStateRuntime.setState,
851
+ recordEvent: recordRuntimeEvent,
852
+ };
853
+ const recoverStaleTelegramTopicApiError =
854
+ Sync.createTelegramStaleTopicApiErrorRecoveryRuntime(
855
+ staleTopicApiErrorRecoveryDeps,
856
+ );
846
857
  const {
847
858
  owner: updateWorkerOwnerRuntime,
848
859
  leader: updateAdmissionLifecycleRuntime,
@@ -866,6 +877,12 @@ export default function (pi: Pi.ExtensionAPI) {
866
877
  worker: {
867
878
  defaultHandle: inboundRouteRuntime.handleUpdate,
868
879
  onStateChange: runtimeDiagnostics.scheduleSnapshotPersist,
880
+ settleTerminalExecutionFailure(error) {
881
+ return Sync.settleStaleTelegramTopicExecutionFailure(
882
+ error,
883
+ staleTopicApiErrorRecoveryDeps,
884
+ );
885
+ },
869
886
  },
870
887
  leader: {
871
888
  resolveBinding: resolveTelegramUpdateJournalBinding,
@@ -1055,13 +1072,6 @@ export default function (pi: Pi.ExtensionAPI) {
1055
1072
  onPollingStateChange: runtimeDiagnostics.scheduleSnapshotPersist,
1056
1073
  recordRuntimeEvent,
1057
1074
  });
1058
- const recoverStaleTelegramTopicApiError =
1059
- Sync.createTelegramStaleTopicApiErrorRecoveryRuntime({
1060
- topicTargetStore: threadStore,
1061
- getSyncState: telegramSyncStateRuntime.getState,
1062
- setSyncState: telegramSyncStateRuntime.setState,
1063
- recordEvent: recordRuntimeEvent,
1064
- });
1065
1075
  const authorizeFollowerApiCall = Bus.createTelegramFollowerApiCallAuthorizer({
1066
1076
  isMessageOwned: messageOwnershipRuntime.isOwnedByFollower,
1067
1077
  });
@@ -1234,6 +1244,7 @@ export default function (pi: Pi.ExtensionAPI) {
1234
1244
  prepareTempDir,
1235
1245
  updateStatus,
1236
1246
  unbindDeferredDispatchContext: deferredQueueDispatchRuntime.unbind,
1247
+ discardQueuedItems: queueMutationRuntime.clear,
1237
1248
  clearModelMenuState: modelMenuRuntime.clear,
1238
1249
  getActiveTurnChatId: activeTurnRuntime.getChatId,
1239
1250
  getActiveTurnTarget: activeTurnRuntime.getTarget,
@@ -95,11 +95,28 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
95
95
  sendUserMessage: Queue.TelegramQueueDispatchRuntimeDeps<TContext>["sendUserMessage"];
96
96
  recordRuntimeEvent?: TelegramRuntimeEventRecorder;
97
97
  }): TelegramQueueBindingRuntime<TContext> {
98
+ const settleDiscardedItems = (
99
+ items: readonly Queue.TelegramQueueItem<TContext>[],
100
+ ctx: TContext,
101
+ ): boolean => {
102
+ const durableItems = items.filter(
103
+ (item) => (item.admissionReceipts?.length ?? 0) > 0,
104
+ );
105
+ if (durableItems.length === 0) return true;
106
+ const settlement = deps.admission.getSettlement();
107
+ if (!settlement) return false;
108
+ settlement.onItemsDiscarded(durableItems, ctx);
109
+ return durableItems.every((item) => !settlement.isItemReady(item));
110
+ };
98
111
  const mutation = Queue.createTelegramQueueMutationController({
99
112
  ...deps.store,
100
113
  allocateLaneOrder: deps.queue.allocateItemOrder,
101
114
  onItemsDiscarded(items, ctx) {
102
- deps.admission.getSettlement()?.onItemsDiscarded(items, ctx);
115
+ if (!settleDiscardedItems(items, ctx)) {
116
+ throw new Error(
117
+ "Telegram queue items could not be discarded durably.",
118
+ );
119
+ }
103
120
  },
104
121
  updateStatus: deps.updateStatus,
105
122
  recordRuntimeEvent: deps.recordRuntimeEvent,
@@ -136,6 +153,9 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
136
153
  onControlSettled(item, ctx) {
137
154
  deps.admission.getSettlement()?.onControlSettled(item, ctx);
138
155
  },
156
+ onPromptSkipped(item, ctx) {
157
+ return settleDiscardedItems([item], ctx);
158
+ },
139
159
  updateStatus: deps.updateStatus,
140
160
  sendTextReply: deps.sendTextReply,
141
161
  recordRuntimeEvent: deps.recordRuntimeEvent,
@@ -909,6 +929,25 @@ export function registerTelegramLifecycleRuntimeHooks({
909
929
  { replyToPrompt: false },
910
930
  );
911
931
  };
932
+ let activeTurnDeliveryTail = Promise.resolve();
933
+ const scheduleActiveTurnDelivery = (task: () => Promise<void>): void => {
934
+ const previous = activeTurnDeliveryTail;
935
+ activeTurnDeliveryTail = (async () => {
936
+ await previous;
937
+ await new Promise<void>((resolve) => {
938
+ const timer = setTimeout(resolve, 0);
939
+ timer.unref?.();
940
+ });
941
+ await task();
942
+ })().catch((error) => {
943
+ recordRuntimeEvent("delivery", error, {
944
+ phase: "agent-end-background-delivery",
945
+ });
946
+ });
947
+ };
948
+ const waitForActiveTurnDelivery = async (): Promise<void> => {
949
+ await activeTurnDeliveryTail;
950
+ };
912
951
  const agentLifecycleHooks = Queue.createTelegramAgentLifecycleHooks<
913
952
  Queue.PendingTelegramTurn,
914
953
  Pi.ExtensionContext,
@@ -950,16 +989,7 @@ export function registerTelegramLifecycleRuntimeHooks({
950
989
  dispatchNextQueuedTelegramTurn,
951
990
  requestDeferredDispatchNextQueuedTelegramTurn:
952
991
  deferredQueueDispatchRuntime.request,
953
- scheduleActiveTurnDelivery(task) {
954
- const timer = setTimeout(() => {
955
- void task().catch((error) => {
956
- recordRuntimeEvent("delivery", error, {
957
- phase: "agent-end-background-delivery",
958
- });
959
- });
960
- }, 0);
961
- timer.unref?.();
962
- },
992
+ scheduleActiveTurnDelivery,
963
993
  clearPreview: previewRuntime.clear,
964
994
  setPreviewPendingText: previewRuntime.setPendingText,
965
995
  finalizeMarkdownPreview,
@@ -1079,7 +1109,11 @@ export function registerTelegramLifecycleRuntimeHooks({
1079
1109
  activityRuntime.onCompactionStart(Pi.getSessionCompactionReason(event));
1080
1110
  compactionObserver.onSessionBeforeCompact(event, ctx);
1081
1111
  if (shouldNotify) {
1082
- await sendCompactionNotice(Commands.TELEGRAM_COMPACTION_STARTED_TEXT);
1112
+ await waitForActiveTurnDelivery();
1113
+ if (!isSessionContextActive(ctx)) return;
1114
+ await sendCompactionNotice(
1115
+ Commands.TELEGRAM_COMPACTION_STARTED_MARKDOWN,
1116
+ );
1083
1117
  }
1084
1118
  },
1085
1119
  async onSessionCompact(event, ctx) {
@@ -1088,7 +1122,9 @@ export function registerTelegramLifecycleRuntimeHooks({
1088
1122
  compactionObserver.onSessionCompact(event, ctx);
1089
1123
  if (observedAutomaticCompaction) {
1090
1124
  observedAutomaticCompaction = false;
1091
- await sendCompactionNotice(Commands.TELEGRAM_COMPACTION_COMPLETED_TEXT);
1125
+ await sendCompactionNotice(
1126
+ Commands.TELEGRAM_COMPACTION_COMPLETED_MARKDOWN,
1127
+ );
1092
1128
  }
1093
1129
  },
1094
1130
  async onAgentStart(event, ctx) {
@@ -20,6 +20,7 @@ import type { TelegramTarget } from "./target.ts";
20
20
  import {
21
21
  isTelegramApiMethodRetrySafe,
22
22
  TelegramApiCommitUnknownError,
23
+ TelegramApiStaleTargetError,
23
24
  } from "./telegram-api.ts";
24
25
  import {
25
26
  createTelegramBusFollowerDeliveryIdentity,
@@ -898,6 +899,20 @@ export function createTelegramBusFollowerApiCaller(
898
899
  response?.kind === "bus.ack"
899
900
  ? response.message
900
901
  : "Telegram bus API call did not return an acknowledgement.";
902
+ if (
903
+ response?.kind === "bus.ack" &&
904
+ response.error?.code === "stale-target" &&
905
+ response.error.chatId !== undefined &&
906
+ response.error.threadId !== undefined
907
+ ) {
908
+ throw new TelegramApiStaleTargetError(
909
+ message ?? "Telegram thread target is stale.",
910
+ {
911
+ chatId: response.error.chatId,
912
+ threadId: response.error.threadId,
913
+ },
914
+ );
915
+ }
901
916
  if (
902
917
  response?.kind === "bus.ack" &&
903
918
  response.error?.code === "commit-unknown"
@@ -8,6 +8,7 @@
8
8
  import * as Sync from "./sync.ts";
9
9
  import * as ThreadReconciler from "./thread-reconciler.ts";
10
10
  import {
11
+ getTelegramApiErrorRequestTarget,
11
12
  isTelegramApiCommitUnknownError,
12
13
  type TelegramApiCallOptions,
13
14
  } from "./telegram-api.ts";
@@ -1756,6 +1757,10 @@ async function handleFollowerApiCall(
1756
1757
  result,
1757
1758
  };
1758
1759
  } catch (error) {
1760
+ const staleTarget =
1761
+ Threads.isTelegramTopicTargetStaleError(error)
1762
+ ? getTelegramApiErrorRequestTarget(error)
1763
+ : undefined;
1759
1764
  return {
1760
1765
  kind: "bus.ack",
1761
1766
  requestId: envelope.requestId,
@@ -1764,14 +1769,22 @@ async function handleFollowerApiCall(
1764
1769
  error instanceof Error
1765
1770
  ? error.message
1766
1771
  : "Telegram bus API call failed.",
1767
- ...(isTelegramApiCommitUnknownError(error)
1772
+ ...(staleTarget
1768
1773
  ? {
1769
1774
  error: {
1770
- code: "commit-unknown" as const,
1771
- method: error.method,
1775
+ code: "stale-target" as const,
1776
+ chatId: staleTarget.chatId,
1777
+ threadId: staleTarget.threadId,
1772
1778
  },
1773
1779
  }
1774
- : {}),
1780
+ : isTelegramApiCommitUnknownError(error)
1781
+ ? {
1782
+ error: {
1783
+ code: "commit-unknown" as const,
1784
+ method: error.method,
1785
+ },
1786
+ }
1787
+ : {}),
1775
1788
  };
1776
1789
  }
1777
1790
  }