@llblab/pi-telegram 0.52.0 → 0.52.1

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.
@@ -460,13 +460,13 @@ This restart barrier applies once the retention copy is atomically published and
460
460
 
461
461
  Historical classification cannot rely on a clean v1 pending entry alone. The native integration test `Historical pending source alone cannot prove no follower acceptance` holds the acknowledgement after durable recipient admission, then revokes the source generation: reopening the source yields exactly its pre-forward snapshot, with no queue receipt, owner, input claim or provenance, while the recipient still owns admitted input. A topic-created reply marker does not distinguish those histories either. This demonstrates an information boundary, not that the reporter's particular inputs were forwarded. Recreating a chooser after startup cannot supply the missing historical evidence. Each worker generation now records all update IDs in its first fully validated journal snapshot before batching or execution. Without retention evidence, those sources cannot acquire ordinary fresh-cancellation capabilities, including through a newly published chooser or direct worker call. The baseline is not advanced on read/validation failure and is renewed for every generation; entries arriving before the first valid snapshot are conservatively included. Later admitted pending inputs retain normal cancellation, and independently protected retention attempts retain exact recovery. This fresh-cancellation fence does not establish non-delivery. Historical prior recipient acceptance remains unknown; removal of its review UI grants neither cancellation nor replay.
462
462
 
463
- Production leader admission composes routing's narrow historical classifier; follower custody never enters this classifier. It selects supported private plain-text startup inputs with no currently live bound Thread (also protecting them when Threaded Mode is unavailable). It does not infer deletion or repair bindings. Eligible raw unsupported commands/media/unknown historical private-Thread originals without receipt/claim/provenance authority receive a distinct `retain` verdict regardless of whether their target is bound or Restore/temporary membership survives. This operator-approved policy stops ordinary bound startup dispatch for that subset; same-kind live arrivals stay outside the startup census and dispatch normally. Business, forwarded and known owner-bearing sources keep their existing owner paths. Holding is evidence protection, not sender authorization; it exposes no historical-review UI. Missing routing authority or unreadable Thread state fails closed. The domain-owned `shouldReviewHistoricalInput` classifier receives detached v1 pending evidence, the context and abort signal before any companion/default handler runs. Generation, context, journal binding, transport and exact source evidence are rechecked across its await; classification/read failure does not fall through to dispatch. Boolean true selects legacy `historical` review/spending; `retain` installs a separate `retained` claim, also included in `historicalClaimCount`, without a journal mutation or invented cancellation intent. Retain-only sources cannot be cold-spent, recovered through historical retry, generically cancelled, expired through an old routing clock or claimed by grouped queue outcomes. Each classifier receives detached evidence and its verdict is source-rechecked before installation. Each new generation must classify them again. Routing can also report a last-boundary historical hold if ownership changes after early classification: this suspends the carrier, refuses prior selection/queue reports, and cannot be requested retroactively after the handler returns. Its source-kind predicate receives detached original evidence through the current worker capability; a handler projection that strips forwarding/media metadata cannot manufacture eligibility. Predicate calls and their results remain generation-fenced. The native update-plan boundary recognizes only that exact carrier's acknowledged terminal hold; intentional suspension must not become a retryable execution error.
463
+ Production leader admission composes routing's narrow historical classifier; follower custody never enters this classifier. It selects supported private plain-text startup inputs with no currently live bound Thread (also protecting them when Threaded Mode is unavailable). It does not infer deletion or repair bindings. Eligible raw unsupported commands/media/unknown historical private-Thread originals without receipt/claim/provenance authority receive a distinct `retain` verdict regardless of whether their target is bound or Restore/temporary membership survives. This operator-approved policy stops ordinary bound startup dispatch for that subset; same-kind live arrivals stay outside the startup census and dispatch normally. Business, forwarded and known owner-bearing sources keep their existing owner paths. Holding is evidence protection, not sender authorization; it exposes no historical-review UI. Missing routing authority or unreadable Thread state fails closed. The domain-owned `shouldReviewHistoricalInput` classifier receives detached v1 pending evidence, the context and abort signal before any companion/default handler runs. Generation, context, journal binding, transport and exact source evidence are rechecked across its await; classification/read failure does not fall through to dispatch. Boolean true selects legacy `historical` review/spending; `retain` installs a separate `retained` claim, also included in `historicalClaimCount`, without a journal mutation or invented cancellation intent. Retain-only sources cannot be cold-spent, recovered through historical retry, generically cancelled or claimed by grouped queue outcomes. An exact acknowledged chooser clock has its own operator-approved expiry policy below; no clock is inferred from age alone. Each classifier receives detached evidence and its verdict is source-rechecked before installation. Each new generation must classify them again. Routing can also report a last-boundary historical hold if ownership changes after early classification: this suspends the carrier, refuses prior selection/queue reports, and cannot be requested retroactively after the handler returns. Its source-kind predicate receives detached original evidence through the current worker capability; a handler projection that strips forwarding/media metadata cannot manufacture eligibility. Predicate calls and their results remain generation-fenced. The native update-plan boundary recognizes only that exact carrier's acknowledged terminal hold; intentional suspension must not become a retryable execution error.
464
464
 
465
- `inspectTelegramHistoricalInputs` remains a package-private bounded worker capability requiring a fresh current carrier and caller authority; it scans neither archive directories nor journal files. Ordinary abandonment still rejects historical sources, and late or grouped queue outcomes cannot steal their claims. The Historical inputs main-menu entry and historical review/Stop retrying submenu are removed. The reserved `reroutecancel:history:` prefix consumes old controls with an unavailable answer, never forwarding them to companion handlers or Pi, reopening a view, acquiring mutation admission, or abandoning a source. Held originals remain unchanged; this UI removal adds no expiry, retention intent or Thread deletion. Existing interrupted private-retention attempts still use the separate Pending cancellations review, which warns that previously accepted work may continue. Native worker and cold-router tests cover retired list, selection, confirmation, paging and refresh controls. Cross-domain IPC fixtures lose an ACK after recipient admission and prove that removal leaves uncertain source evidence and independently accepted recipient work intact. Protected-attempt recovery is tested with a supplied interrupted-retention precondition, not a retired UI grant. A production-root fixture proves that the normal menu omits Historical inputs while retained Restore originals remain held. Newly confirmed eligible prompt choosers now use the source-only lifetime described below; historical evidence never acquires a fabricated deadline. Elapsed time never proves non-delivery or authorizes Thread deletion. The operator accepted this behavior in the operator's 0.52.0 live smoke.
465
+ `inspectTelegramHistoricalInputs` remains a package-private bounded worker capability requiring a fresh current carrier and caller authority; it scans neither archive directories nor journal files. Ordinary abandonment still rejects historical sources, and late or grouped queue outcomes cannot steal their claims. The Historical inputs main-menu entry and historical review/Stop retrying submenu are removed. The reserved `reroutecancel:history:` prefix consumes old controls with an unavailable answer, never forwarding them to companion handlers or Pi, reopening a view, acquiring mutation admission, or abandoning a source. Held originals remain unchanged; this UI removal adds no expiry, retention intent or Thread deletion. Existing interrupted private-retention attempts still use the separate Pending cancellations review, which warns that previously accepted work may continue. Native worker and cold-router tests cover retired list, selection, confirmation, paging and refresh controls. Cross-domain IPC fixtures lose an ACK after recipient admission and prove that removal leaves uncertain source evidence and independently accepted recipient work intact. Protected-attempt recovery is tested with a supplied interrupted-retention precondition, not a retired UI grant. A production-root fixture proves that the normal menu omits Historical inputs while retained Restore originals remain held. Newly confirmed eligible prompt choosers now use the source-only lifetime described below; historical evidence never acquires a fabricated deadline. Elapsed time never proves non-delivery; the explicit chooser expiry policy below may end donor attempts without that claim and separately license fully resolved disposable-tab cleanup. The operator accepted this behavior in the operator's 0.52.0 live smoke.
466
466
 
467
- New private-owner prompt choosers, including grouped sources, arm a source-only 60-minute lifetime after positive publication acknowledgement. The worker captures that clock before storage contention; `journal.routingInputs.arm` atomically retains each exact v1 source's operator, `publishedAtMs`, fixed `expiresAtMs` and `waiting` phase. Refresh and restart cannot renew it. The root also uses this capability for supported acknowledged command choosers, including Telegram-provided unbound tabs and source-bound temporary tabs under the owned leader epoch; unsupported compatibility carriers retain their legacy behavior. Actual destination validation precedes `routingInputs.select` under the existing Workspace admission; the complete source cohort becomes `selected` atomically before dispatch or Restore effects. Menu browsing, invalid targets and refused Restore capability leave `waiting` intact. Source/epoch/operator/context/binding/process/session fences and post-publication readback remain mandatory; uncertain clock or selection ACK refuses issuance rather than bypassing the deadline. Selected metadata prevents cold redispatch and automatic expiry, including unknown accepted work. A failed clock publication holds the source without automatic handler retry or a made-up replacement clock; reconstruction can use only positively retained metadata.
467
+ Acknowledged private-owner routing choosers, including commands and grouped prompts, own one fixed 60-minute lifetime from their first positive publication. `journal.routingInputs.arm` retains each exact v1 source's operator, `publishedAtMs`, `expiresAtMs` and `waiting` phase; refresh/restart never renews it. Destination validation precedes atomic `select` under Workspace admission. Selection still prevents another issuance, but does not extend the donor deadline: an unclaimed selected source with an unknown delivery outcome also expires. Queue admission carries the selected clock into the queued entry; queued/running recipient work is outside donor expiry. Unsupported carriers, unconfirmed clocks and historical originals without lifetime metadata acquire no invented deadline.
468
468
 
469
- The worker schedules one generation-fenced deadline wake, also checking retained lifetimes during normal drain and restart. Routing expiry reacquires profile-wide Workspace admission, checks current source/operator/context/leader/binding authority and refuses retained Restore or live selection/cleanup work. Only exact `waiting` sources can use the existing `abandonPending` private-copy/tombstone transaction; readback and positive disposition precede claim/control retirement, never a task-completion observer. A failed retention attempt stays protected; unsuccessful deadline wakes schedule a one-minute retry, and normal drains also recheck it. Lost removal replies reconcile the exact held original against the existing private copy and committed tombstone; absence alone cannot release it. UI edits are best-effort after ACK. No tab visibility probe, Thread deletion, target retirement, Pi stop or reset of an issued Restore grant occurs. Deadline expiry works even if the chooser/tab was manually removed; optional reliable disappearance detection is not implemented. Historical, unconfirmed, selected, queued, running and unknown issued sources remain outside automatic expiry. All peers and successor writers must understand the new v1 entry metadata before candidate reload. Native journal/producer/worker fixtures cover grouped sources, late selection, expiry boundary, restart, stale wakes/buttons, missing views, retention/clock/selection ACK faults and changed authority; they do not prove live operator acceptance.
469
+ The operator-approved optimistic chooser policy accepts nondelivery after the fixed deadline. Expiry reacquires profile-wide Workspace admission and exact owner/context/leader/source authority, then CAS-removes the unclaimed donor pending source and writes a body-free legacy-compatible discard tombstone. It creates no private prompt archive, execution completion or recipient cancellation. Exact tombstone readback reconciles lost ACKs; changed/queued/owner-bearing sources refuse. The originating carrier loses authority, old controls are retired, and late reports cannot revive the input or block unrelated work. A lost selection ACK may reconcile only the waiting-to-selected phase of an otherwise identical source, never grant another send. Clock-bearing historical sources survive cold spending until the same deadline; positively unbound temporary frames associated with them survive new-world forgetting. The worker retries only unconfirmed source disposition. A positive expiry ACK immediately drops its deferred source and claim, before observer/UI awaits. Failed cleanup metadata is retried once per minute by the existing one-timer-per-tab scheduler, reconstructing known groups from body-free journal evidence rather than retaining prompt bodies. Future metadata retries do not join settlement waits; only running attempts do. Same-instance session replacement may reprepare unissued expiry frames under fresh authority without resurrecting a source. Missing/unknown grant state or an issued destructive cleanup forbids retry.
470
470
 
471
471
  With live bindings or a retained temporary entry, a known command or a fresh private plain-text prompt from All first enters the shared source-bound temporary-Thread path under current owner/profile/journal/context/leader authority and profile-wide Workspace admission. Threads atomically reserves a source/token intent before one `createForumTopic` attempt and records only its exact acknowledged target. The tab owns no Pi binding or A–Z slot; its presentation target is separate from the original All source. A creation error, missing target or lost publication result leaves unknown custody held, never a second creation attempt. A `created` entry can republish its chooser while its source is still pending after restart; a `creating` entry supplies no creation or dispatch grant. The acknowledged tab offers the shared Forward/Restore chooser and Cancel only when the current source has fresh private-abandonment capability. Publication defers rather than completes the original command; selection retains command semantics, and no current Pi is chosen automatically. Commands already completed by the compatibility path are not reconstructed. Unpresented expired commands keep the existing command-expiry fence; a confirmed retained tab is not evidence of expiry. Fresh threadless plain-text prompts use `New chat` with the same source-bound routing menu; fallback guidance remains when exact temporary-creation authority is unavailable. Recognized commands include built-in handlers (not only bot-menu entries), registered Telegram extension commands regardless of menu visibility, and discovered prompt-template commands. Bound-Thread and classic routing are unchanged. Local candidate composition is default with the required stores and owned epoch; there is no temporary-tab feature flag. Actual Pi/Telegram behavior was accepted in the operator's 0.52.0 live smoke; no owner-visible uncertainty recovery is scheduled.
472
472
 
@@ -474,7 +474,7 @@ With live bindings or a retained temporary entry, a known command or a fresh pri
474
474
 
475
475
  Temporary command tabs use the command itself as their title (for example `/start` or `/status`), without a visible internal token; fresh plain-text prompt tabs use `New chat`. Both use the same mode chooser and differ only in their title and introductory copy. A command delivered with a Telegram-provided unbound private target uses the same chooser and fresh exact-source Cancel capability; its first chooser renames that target to the command without creating another tab. A received target or successful rename alone never manufactures temporary authority. A fresh owner-authenticated private `forum_topic_created` with literal `is_name_implicit: true` supplies native creation evidence: routing retains a bounded process-local observation tied to the exact context, session generation, admission scope, operator, journal binding and executor. A later live single-source input in that exact target registers it through Threads `registerImplicitTemporaryThread` under profile admission, without calling or fabricating `createForumTopic`; the publication CAS refuses any binding, owner record, reservation, provisioning, cleanup or Restore conflict. Normal grouped membership, one-shot Forward, source disposition, last-Cancel quiet period and cleanup issuance then apply identically. Historical creation signals, context/epoch changes, manual names, string flags, foreign creators and missing observations grant nothing. The observation is consumed on registration, never reconstructed from a title or restart. Existing bindings/reservations and acknowledged temporary targets retain their protections. Their root chooser offers `Reroute…`, `Restore…`, and eligible `Cancel routing`; each mode opens its own live-target submenu without dispatching the held input. Submenus place `⬆️ Back` first to return to the mode chooser with its exact original description, retained on that pending chooser; navigation never substitutes or recomputes the root copy. Cancel appears only at the root. After cancellation, the 1-second quiet-period cleanup remains proof-gated. Private-chat temporary tabs skip the unsupported `closeForumTopic` call and issue only one `deleteForumTopic` attempt; literal positive deletion acknowledgement is still required, and uncertainty never authorizes a retry.
476
476
 
477
- This multi-input lifecycle is implemented in the local candidate, not authority for live deletion. It supersedes unconditional per-chooser tab removal. The reject-only guard prevents cleanup/retirement while another known same-tab chooser remains, including unresolved cleanup-only work. `temporaryThreads.inputs` now records bounded append-only journal source groups through the unbound producer before chooser publication; duplicates are read-only, partial/foreign/overlapping groups refuse, and faults retain the source without a new chooser. Cold reads preserve this protection without process-local chooser memory. A group has no readiness or disposition flag, and an absent legacy field does not prove complete coverage. `cancelledInputs` now records exact whole known groups only after strict same-journal/operator abandonment inspection confirms committed tombstones plus matching private originals. Store publication and returned/retained acknowledgement recheck that proof and operator/executor authority; a lost reply reconciles the exact fact read-only, while missing/foreign/corrupt proof stays protective. The producer composes this into explicit Cancel before temporary cleanup or chooser retirement. Cancelled groups cannot Restore; independent sources remain untouched. More than one known group still blocks cleanup and raw retirement, including all-cancelled membership: these facts grant no deletion. Each successful Cancel retires only its own chooser. If durable membership is fully resolved by exact cancellation/completion facts and no chooser remains, a process-local quiet period starts (1 s; new input in the tab cancels it). Its single attempt, under profile admission, rechecks authority, whole-group tombstone plus retention proof, exact entry CAS, an empty strict journal census (any retained update naming the Thread, any state), absence of choosers and the normal reconciler protections. `issueTemporaryThreadCleanup` publishes canonical `cleanupIssued: true` only for an exact fully resolved entry with strict unbound canonical evidence checked inside the publication transaction; the normal target/source guards precede issuance and run again at API boundaries. The reconciler requires that exact retained issued frame and limits its plan to this tab; unrelated expired-provision cleanup never borrows the grant. Close/delete calls disable retries and transport fallback and require a positive acknowledgement. Confirmed current deletion alone permits entry retirement; skip, negative/lost reply, authority loss or post-rename publication interruption retains the marker. Cold reads classify already-issued entries as unknown, new membership refuses, and executor adoption never renews the grant. Missing evidence only refuses; an unpublished grant issues no API call and may still receive its first grant. The quiet period remains process-local, but issued uncertainty survives restart. The census cannot see updates in transit before journal append. Restore from a multi-input tab is allowed only when each other known group is durably cancelled, Forward-completed or a live unselected chooser that supports private abandonment. A previously completed Forward never supplies a new selection or cancellation grant. After positive settlement of every selected source, such siblings are abandoned with cancellation facts and their controls edited to cancelled, then `retireTemporaryThread(expected, authority, completed)` releases the entry (one newly completed selected group, all others durably cancelled or previously Forward-completed). Missing authority/proof or an unproven Restore leaves siblings pending and protected. On startup, a retained journal input recorded in a created temporary tab whose target a Workspace binding now owns is held as historical (never cancelled, discarded or routed). Live pending sources and due retry-wait sources (including startup retries) use the same fresh membership/binding predicate through `shouldHoldPendingInput` before execution, with source/context/binding/transport checks after classification awaits. The hold grants neither cancellation nor delivery and does not block unrelated inputs. Recipient custody never uses this direct-source classifier. A native production witness admits a recorded command after the worker has already processed an unrelated command, proving it cannot fall through to ordinary bound dispatch. A production retry witness keeps recorded failure metadata intact without command execution, cancellation or deletion. Claimed retries cannot schedule repeated due-time wakes; independent due and future retries remain executable. Real-IPC Forward/Restore sibling witnesses now include recipient journal drain through the real worker and follower router: only the selected message executes, source disposal is observed, and repeated controls do not replay the command. Existing message-ownership composition keeps a locally published chooser's callbacks at its leader publisher after follower rebinding; the native fixture now records publication ownership rather than relying only on target ownership. Four native delivery-reply-loss witnesses cover both routing actions (Forward/Restore), before recipient execution and after worker disposal: the leader source remains unresolved without ACK, independent sibling Cancel preserves the selected group/tab, and a late reply or repeated issued Restore click cannot settle/replay it. Explicit Forward retry after a lost ACK reproduced command duplication when the recipient worker had already disposed its journal record; stable delivery identity alone was not terminal deduplication. The follower's bounded process-local delivery window now acknowledges such a retry without re-execution while it retains that delivery. Ordinary Forward from a temporary tab first publishes a durable per-group `forwardedInputs` issuance fact in the canonical Threads owner under current authority, before either local command handling/prompt preparation/queue admission or follower RPC. Only positively unpublished issuance may receive its first grant; a published fact (even if its reply is ambiguous) is never retried, including after reload, restart or leader replacement. Local repeated selection refuses the retained grant before handler or queue re-entry. Captured All-tab provenance and acknowledged typed-input membership remain protective if the metadata reader disappears; unavailable evidence never falls back to unguarded generic dispatch. Restore retains its separate canonical routing grant. Native local fixtures interrupt source completion before/after commit, prompt preparation and issuance publication/authority; issued sources stay nonterminal, independent siblings remain cancellable and cold restart never manufactures completion or another attempt. A process-local `foreignForwardIssued` mirror gates the live chooser. An issued group is refused by repeat Forward, Cancel, expiry and Restore (the store also rejects them), keeps sibling accounting protective and is resolved only by a positive `completedInputs` fact; independent siblings remain operable and a restarted chooser cannot resend. Issuance is not proof of delivery, and recipient-side deduplication is limited to the bounded process-local delivery window; an operator-visible exit for a permanently unknown issued group, generic journal forwarding and non-temporary reroute retries are unchanged and not scheduled. Canonical Restore keeps its existing durable grants. Mixed native witnesses prove that a completed Forward allows Restore of another group without replay/recancellation, while an unknown Forward prevents sibling Restore before an intent is created. Prompt Restore leaves a command sibling pending at queue admission, then privately cancels it only after exact scoped worker receipt disposition. Independently Forwarded queued input is neither unassigned nor cancellation-capable: either receipt may dispose first, the other retains custody, and only both positive dispositions release temporary membership while keeping the bound tab/slot. An ordinary queued ACK emits the existing source-completion hint only after whole acknowledged removal and fresh context, owner, generation and originating journal-binding checks; it creates no immutable Restore marker. A published independent Forward group fact wakes pure inspection of already settled Restore only after its Workspace admission releases. The store refuses both Forward facts and cancellation for any source overlapping a retained Restore grant, regardless of target; malformed partial selected groups stay protective. Native ordinary prompt Forward plus sibling Cancel also delays deletion until receipt disposition and fresh all-resolved cleanup. Pi handoff is supplied by the fixture, not observed model/task completion. The retained All-command fixture registers 84 scenarios across creation, Forward, Cancel, cleanup/membership and Restore, including full slots, publication faults, sibling protection, native Unix IPC and recipient worker drain through the real non-reentrant Workspace gate. Full-slot follower cases hold a real Unix response before apply, after apply, and after read-only inspection until client timeout. Fresh same-session registration/context generations reconcile only through inspection; late replies never publish readiness, repeat apply or dispatch, cancel siblings, release independent queued recipient work or delete the tab. Successful continuation retains exact scoped source-disposition proof. A separate cold-successor witness loads fresh stores and a journal binding after an issued command lost acceptance authority, then starts a fresh context/generation/epoch under supplied same-session owner publication. Original command and sibling stay pending, spent routing/canonical facts remain unchanged, and stale Restore/Forward/Cancel controls cannot replay or dispose them. No chooser is reconstructed: the protective hold is proven, not a usable cold recovery surface. Pi effects and startup registration remain supplied adapters/preconditions; actual Pi startup and live behavior were accepted in the operator's 0.52.0 live smoke. Forward completes only its own input: no Forward removes the tab, the worker's completion ACK records a durable `completedInputs` group (process-local accumulation across the group's sources), and when every known group is cancelled or completed the delayed, fully rechecked cleanup removes the tab once. Cancelled and completed groups are disjoint, cannot Restore, and a completed Forward queued prompt holds the tab until its receipt completes. The tab chooser discloses removal only when no other input remains; a failed removal no longer edits the original chooser. Source-only prompt Cancel remains unchanged. The candidate's temporary-tab chooser is the default under the owned leader epoch, not a configurable feature flag. Upgrade all prospective canonical readers/writers before live activation; the metadata (including `forwardProtocol`, `forwardedInputs` and `cleanupIssued`) is not downgrade-safe through older snapshot writers, which could drop issuance facts.
477
+ This multi-input lifecycle is implemented in the local candidate, not authority for live deletion. It supersedes unconditional per-chooser tab removal. The reject-only guard prevents cleanup/retirement while another known same-tab chooser remains, including unresolved cleanup-only work. `temporaryThreads.inputs` now records bounded append-only journal source groups through the unbound producer before chooser publication; duplicates are read-only, partial/foreign/overlapping groups refuse, and faults retain the source without a new chooser. Cold reads preserve this protection without process-local chooser memory. A group has no readiness or disposition flag, and an absent legacy field does not prove complete coverage. `cancelledInputs` records exact whole known donor groups after either same-journal/operator retained-cancellation evidence or the separate body-free expiry tombstone. Expiry recording may clear a group's unknown Forward issuance or its expired Restore intent, but never mutates a canonical binding, recipient queue or immutable scoped settlement proof. If Restore already bound the tab, expiry releases the temporary frame without deletion. Otherwise fully resolved groups enter the existing one-second quiet-period cleanup. A partially acknowledged known cohort may finish donor disposition when every member is absent from that exact source journal and at least one member has same-operator expiry proof; this is not recipient completion or non-delivery proof. Fresh strict namespace/protection evidence still gates deletion; independent work and bound Threads remain protected. Store publication and returned/retained acknowledgement recheck that proof and operator/executor authority; a lost reply reconciles the exact fact read-only, while missing/foreign/corrupt proof stays protective. The producer composes this into explicit Cancel before temporary cleanup or chooser retirement. Cancelled groups cannot Restore; independent sources remain untouched. More than one known group still blocks cleanup and raw retirement, including all-cancelled membership: these facts grant no deletion. Each successful Cancel retires only its own chooser. If durable membership is fully resolved by exact cancellation/completion facts and no chooser remains, a process-local quiet period starts (1 s; new input in the tab cancels it). Its single attempt, under profile admission, rechecks authority, whole-group cancellation/expiry evidence, exact entry CAS, an empty strict journal census (any retained update naming the Thread, any state), absence of choosers and the normal reconciler protections. `issueTemporaryThreadCleanup` publishes canonical `cleanupIssued: true` only for an exact fully resolved entry with strict unbound canonical evidence checked inside the publication transaction; the normal target/source guards precede issuance and run again at API boundaries. The reconciler requires that exact retained issued frame and limits its plan to this tab; unrelated expired-provision cleanup never borrows the grant. Close/delete calls disable retries and transport fallback and require a positive acknowledgement. Confirmed current deletion alone permits entry retirement; skip, negative/lost reply, authority loss or post-rename publication interruption retains the marker. Cold reads classify already-issued entries as unknown, new membership refuses, and executor adoption never renews the grant. Missing evidence only refuses; an unpublished grant issues no API call and may still receive its first grant. The quiet period remains process-local, but issued uncertainty survives restart. The census cannot see updates in transit before journal append. Restore from a multi-input tab is allowed only when each other known group is durably cancelled, Forward-completed or a live unselected chooser that supports private abandonment. A previously completed Forward never supplies a new selection or cancellation grant. After positive settlement of every selected source, such siblings are abandoned with cancellation facts and their controls edited to cancelled, then `retireTemporaryThread(expected, authority, completed)` releases the entry (one newly completed selected group, all others durably cancelled or previously Forward-completed). Missing authority/proof or an unproven Restore leaves siblings pending and protected. On startup, a retained journal input recorded in a created temporary tab whose target a Workspace binding now owns is held as historical, never rerouted; an exact saved chooser clock may expire its unclaimed donor attempt without affecting the binding. Live pending sources and due retry-wait sources (including startup retries) use the same fresh membership/binding predicate through `shouldHoldPendingInput` before execution, with source/context/binding/transport checks after classification awaits. The hold grants neither cancellation nor delivery and does not block unrelated inputs. Recipient custody never uses this direct-source classifier. A native production witness admits a recorded command after the worker has already processed an unrelated command, proving it cannot fall through to ordinary bound dispatch. A production retry witness keeps recorded failure metadata intact without command execution, cancellation or deletion. Claimed retries cannot schedule repeated due-time wakes; independent due and future retries remain executable. Real-IPC Forward/Restore sibling witnesses now include recipient journal drain through the real worker and follower router: only the selected message executes, source disposal is observed, and repeated controls do not replay the command. Existing message-ownership composition keeps a locally published chooser's callbacks at its leader publisher after follower rebinding; the native fixture now records publication ownership rather than relying only on target ownership. Four native delivery-reply-loss witnesses cover both routing actions (Forward/Restore), before recipient execution and after worker disposal: the leader source remains unresolved without ACK, independent sibling Cancel preserves the selected group/tab, and a late reply or repeated issued Restore click cannot settle/replay it. Explicit Forward retry after a lost ACK reproduced command duplication when the recipient worker had already disposed its journal record; stable delivery identity alone was not terminal deduplication. The follower's bounded process-local delivery window now acknowledges such a retry without re-execution while it retains that delivery. Ordinary Forward from a temporary tab first publishes a durable per-group `forwardedInputs` issuance fact in the canonical Threads owner under current authority, before either local command handling/prompt preparation/queue admission or follower RPC. Only positively unpublished issuance may receive its first grant; a published fact (even if its reply is ambiguous) is never retried, including after reload, restart or leader replacement. Local repeated selection refuses the retained grant before handler or queue re-entry. Captured All-tab provenance and acknowledged typed-input membership remain protective if the metadata reader disappears; unavailable evidence never falls back to unguarded generic dispatch. Restore retains its separate canonical routing grant. Native local fixtures interrupt source completion before/after commit, prompt preparation and issuance publication/authority; issued sources stay nonterminal, independent siblings remain cancellable and cold restart never manufactures completion or another attempt. A process-local `foreignForwardIssued` mirror gates the live chooser. An issued group is refused by repeat Forward, explicit Cancel and Restore (the store also rejects them); before its fixed chooser deadline it keeps sibling accounting protective, while body-free donor expiry or positive `completedInputs` may resolve it; independent siblings remain operable and a restarted chooser cannot resend. Issuance is not proof of delivery, and recipient-side deduplication is limited to the bounded process-local delivery window; the chooser deadline supplies a loss-accepting exit for unclaimed unknown issued groups; generic journal forwarding and non-chooser retry policy are unchanged. Canonical Restore keeps its existing durable grants. Mixed native witnesses prove that a completed Forward allows Restore of another group without replay/recancellation, while an unknown Forward prevents sibling Restore before an intent is created. Prompt Restore leaves a command sibling pending at queue admission, then privately cancels it only after exact scoped worker receipt disposition. Independently Forwarded queued input is neither unassigned nor cancellation-capable: either receipt may dispose first, the other retains custody, and only both positive dispositions release temporary membership while keeping the bound tab/slot. An ordinary queued ACK emits the existing source-completion hint only after whole acknowledged removal and fresh context, owner, generation and originating journal-binding checks; it creates no immutable Restore marker. A published independent Forward group fact wakes pure inspection of already settled Restore only after its Workspace admission releases. The store refuses both Forward facts and cancellation for any source overlapping a retained Restore grant, regardless of target; malformed partial selected groups stay protective. Native ordinary prompt Forward plus sibling Cancel also delays deletion until receipt disposition and fresh all-resolved cleanup. Pi handoff is supplied by the fixture, not observed model/task completion. The retained All-command fixture registers 84 scenarios across creation, Forward, Cancel, cleanup/membership and Restore, including full slots, publication faults, sibling protection, native Unix IPC and recipient worker drain through the real non-reentrant Workspace gate. Full-slot follower cases hold a real Unix response before apply, after apply, and after read-only inspection until client timeout. Fresh same-session registration/context generations reconcile only through inspection; late replies never publish readiness, repeat apply or dispatch, cancel siblings, release independent queued recipient work or delete the tab. Successful continuation retains exact scoped source-disposition proof. A separate cold-successor witness loads fresh stores and a journal binding after an issued command lost acceptance authority, then starts a fresh context/generation/epoch under supplied same-session owner publication. Original command and sibling stay pending, spent routing/canonical facts remain unchanged, and stale Restore/Forward/Cancel controls cannot replay or dispose them. No chooser is reconstructed: the protective hold is proven, not a usable cold recovery surface. Pi effects and startup registration remain supplied adapters/preconditions; actual Pi startup and live behavior were accepted in the operator's 0.52.0 live smoke. Forward completes only its own input: no Forward removes the tab, the worker's completion ACK records a durable `completedInputs` group (process-local accumulation across the group's sources), and when every known group is cancelled or completed the delayed, fully rechecked cleanup removes the tab once. Cancelled and completed groups are disjoint, cannot Restore, and a completed Forward queued prompt holds the tab until its receipt completes. The tab chooser discloses removal only when no other input remains; a failed removal no longer edits the original chooser. Source-only prompt Cancel remains unchanged. The candidate's temporary-tab chooser is the default under the owned leader epoch, not a configurable feature flag. Upgrade all prospective canonical readers/writers before live activation; the metadata (including `forwardProtocol`, `forwardedInputs` and `cleanupIssued`) is not downgrade-safe through older snapshot writers, which could drop issuance facts.
478
478
 
479
479
  - `Membership`: A positively source-linked disposable temporary Thread owns no Pi binding or A–Z slot and may contain multiple independent unassigned inputs, each with its own routing controls. Track exact journal source/group membership, including additional inputs, rather than treating the first chooser or a process-local count as the whole Thread. Serialize admission, selection, cancellation, Restore and cleanup through the existing Workspace admission and mutation owners; unreadable, incomplete, selected, accepted, running or unknown-issued evidence blocks deletion.
480
480
  - `Per-input Cancel`: Privately retain the exact original and positively commit source-only cancellation before acknowledging `Routing cancelled` and retiring its route controls. Other unassigned inputs retain their choosers and custody; cancelling one input neither delivers nor cancels siblings. No separate Cancel-and-delete action or extra confirmation dialog is required, but the chooser must disclose the conditional whole-tab deletion consequence.
@@ -583,7 +583,7 @@ Dispatch rank:
583
583
 
584
584
  #### Priority, Reactions, Keep, and Skip
585
585
 
586
- Waiting prompts expose two independent dimensions:
586
+ Waiting Normal/Priority prompts expose two independent dimensions:
587
587
 
588
588
  - `Priority` / `Normal` selects the FIFO lane and therefore scheduling order.
589
589
  - `Keep` / `Skip` selects whether the prompt executes when dispatch reaches it.
@@ -598,6 +598,8 @@ Menu and reaction controls share the same canonical queue state. A menu Keep can
598
598
 
599
599
  Guest Mode is the deliberate exception to deferred Keep/Skip semantics. A guest prompt has no source message reaction surface and may share zero-valued chat/message placeholders with other guest prompts, so queue-menu callbacks address it by immutable `queueOrder` and expose an immediate `Skip`. The leader settles its durable admission, removes it while other agent work may continue, stops placeholder animation, and replaces the inline globe placeholder with non-printing content. Bot API inline messages have no delete method, so this visual clear is the supported deletion equivalent. The guest prompt never reaches a model turn.
600
600
 
601
+ A standalone `/continue` is a source-addressed control-lane prompt, not a Normal/Priority prompt. An authorized negative reaction to its original chat/message immediately settles the exact waiting continuation's receipts before removing it, even while unrelated work is active. Failed or unknown settlement retains it for the existing reaction retry path; duplicate reactions cannot settle twice. Positive reactions and removal of the negative reaction neither change its control lane nor reconstruct cancelled work. The dispatched head awaiting `agent_start` and already-started work are never cancelled; appends and reorders preserve that Pi-owned head. Synthetic model-switch continuations have no source message IDs and are not reaction-addressable by their fallback reply anchor. Reaction authorization and chat/message namespace isolation remain unchanged.
602
+
601
603
  Admission and planning validate lane contracts. Invalid lane/kind pairings fail predictably instead of being silently coerced.
602
604
 
603
605
  Prompt preparation completes downloads, inbound handlers, and binary image reads before synchronous final assembly and queue commit. Enqueue captures only intended abort-history identities before preparation; afterward it reads the current queue, folds surviving selected prompts using their current text and receipts, allocates the new turn's order, and appends without another asynchronous boundary. The handed-off head remains in place until `agent_start` consumes it, even if preparation finishes first. Concurrent arrivals, removals, and lane/reaction edits remain intact; preparation failure or stale-generation completion leaves intervening queue changes untouched.
@@ -639,7 +641,7 @@ Immediate controls:
639
641
 
640
642
  Queued controls:
641
643
 
642
- - `/continue` creates a priority Telegram-owned `continue` prompt.
644
+ - `/continue` creates a Telegram-owned `continue` prompt in the highest-priority control lane. A negative reaction to the original standalone command cancels it only while waiting, with exact durable settlement as specified above.
643
645
  - Prompt-template commands expand Telegram-safe Pi template aliases before entering the prompt queue.
644
646
  - Model-switch continuation uses the control lane when any interruptible in-flight agent run in the current Pi session must be stopped and resumed. A Telegram-owned run retains its prompt target; otherwise the exact model-menu chat/Thread/message supplies continuation and reply ownership.
645
647
 
package/docs/ui-style.md CHANGED
@@ -247,7 +247,7 @@ Rules:
247
247
  - Keep the emoji and complete sentence or heading inside the single bold span; do not bold only a fragment. A material name or phrase may receive nested italic emphasis without breaking the outer bold hierarchy—for example `<b>📡 Instance <i>Cedar</i> connected.</b>`.
248
248
  - Apply the same hierarchy to success, progress, empty, busy, unavailable, cancellation, and failure notices.
249
249
  - Once an action has settled, describe only the completed result in completed-state language. Do not append transitional copy such as “returning” or “starting”; use a separate progress surface only while work is genuinely still pending.
250
- - Callback alerts remain plain text because Telegram does not support rich text there, but still keep the relevant emoji and concise sentence.
250
+ - Transient callback toasts remain plain text with the relevant emoji and concise wording, without a terminal sentence period. The shared typed callback-answer boundary applies this on direct and follower delivery while preserving question/exclamation marks and ellipses. In-chat notices, headings, blocking alerts and explicit raw API payloads retain their own punctuation contracts.
251
251
  - Setting detail cards may include an emoji in the heading, then a colon and the current value in `<code>`.
252
252
  - Explain what the setting does and what the options mean only as much as needed.
253
253
  - 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.
package/lib/bindings.ts CHANGED
@@ -116,6 +116,7 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
116
116
  };
117
117
  const mutation = Queue.createTelegramQueueMutationController({
118
118
  ...deps.store,
119
+ hasPendingDispatch: deps.lifecycle.hasDispatchPending,
119
120
  allocateLaneOrder: deps.queue.allocateItemOrder,
120
121
  onItemsDiscarded(items, ctx) {
121
122
  if (!settleDiscardedItems(items, ctx)) {
package/lib/bus-api.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  } from "./bus.ts";
11
11
  import {
12
12
  buildTelegramAnswerGuestQueryBody,
13
+ formatTelegramCallbackAnswerText,
13
14
  isTelegramMessageNotModifiedError,
14
15
  } from "./telegram-api.ts";
15
16
  import type { TelegramAttachmentSource } from "./media.ts";
@@ -321,15 +322,16 @@ export function createTelegramBusAwareApiRuntime(
321
322
  callbackQueryId: string,
322
323
  text?: string,
323
324
  ): Promise<void> {
325
+ const answer = formatTelegramCallbackAnswerText(text);
324
326
  if (deps.ownsDirect()) {
325
- await deps.directRuntime.answerCallbackQuery(callbackQueryId, text);
327
+ await deps.directRuntime.answerCallbackQuery(callbackQueryId, answer);
326
328
  return;
327
329
  }
328
330
  await deps.callFollowerApi("call", [
329
331
  "answerCallbackQuery",
330
332
  {
331
333
  callback_query_id: callbackQueryId,
332
- ...(text !== undefined ? { text } : {}),
334
+ ...(answer !== undefined ? { text: answer } : {}),
333
335
  },
334
336
  ]);
335
337
  },
package/lib/extension.ts CHANGED
@@ -1146,6 +1146,10 @@ export default function (pi: Pi.ExtensionAPI) {
1146
1146
  return telegramJournalBindingRuntime.inspectSourceAbandonment(journalBindingKey, updateId);
1147
1147
  });
1148
1148
  },
1149
+ inspectRoutingInputGroupExpiry(input) {
1150
+ return telegramJournalReferenceRegistry.withReference({ referenceClass: "operator-disposition", recoveryKey: input.journalBindingKey },
1151
+ function () { return telegramJournalBindingRuntime.inspectSourceGroupExpiry(input.journalBindingKey, input.updateIds); });
1152
+ },
1149
1153
  inspectTemporaryThreadSources(_target, requiredJournalBindingKeys) {
1150
1154
  const binding = resolveTelegramUpdateJournalBinding();
1151
1155
  const botToken = configStore.getBotToken();
package/lib/journal.ts CHANGED
@@ -750,11 +750,36 @@ export interface TelegramRoutingInputAuthority {
750
750
  isCurrent(): boolean;
751
751
  }
752
752
 
753
+ /** Expiry drops a donor attempt without retaining its body or claiming recipient execution/cancellation. */
754
+ export type TelegramRoutingInputExpiryResult = Omit<TelegramUpdateJournalPendingAbandonmentResult, "retainedPath">;
755
+ export type TelegramRoutingInputExpiryEvidence = Omit<TelegramUpdateJournalAbandonedPendingEvidence, "retainedPath">;
756
+
757
+ function inspectRoutingInputExpiry(file: TelegramUpdateJournalFile, journalBindingKey: string, updateId: number): TelegramRoutingInputExpiryEvidence | undefined {
758
+ const disposition = file.operatorDispositions?.find(value => value.updateId === updateId &&
759
+ "dispositionKind" in value && value.dispositionKind === "legacy-custody" && value.action === "discard" &&
760
+ value.failureId === `routing-expiry:${value.evidenceSha256}`);
761
+ if (!disposition || !("operatorAuthorityId" in disposition) || file.entries.some(entry => entry.updateId === updateId)) return undefined;
762
+ return { journalBindingKey, updateId, operatorAuthorityId: disposition.operatorAuthorityId };
763
+ }
764
+
765
+ function inspectRoutingGroupExpiry(file: TelegramUpdateJournalFile, journalBindingKey: string, updateIds: readonly number[]): TelegramRoutingInputExpiryEvidence[] | undefined {
766
+ if (!Array.isArray(updateIds) || !updateIds.length || updateIds.length > TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES ||
767
+ updateIds.some((id, index) => !isSafeNonNegativeInteger(id) || (index > 0 && id <= updateIds[index - 1]!))) return undefined;
768
+ if (file.entries.some(entry => updateIds.includes(entry.updateId))) return undefined;
769
+ const expired = updateIds.map(id => inspectRoutingInputExpiry(file, journalBindingKey, id)).filter(value => value !== undefined);
770
+ if (!expired.length || expired.some(value => value.operatorAuthorityId !== expired[0]!.operatorAuthorityId)) return undefined;
771
+ // Some members may have been acknowledged earlier. Whole donor absence plus one exact expiry ends this known cohort,
772
+ // not recipient custody; fresh Thread protection still independently gates deletion.
773
+ return updateIds.map(updateId => ({ journalBindingKey, updateId, operatorAuthorityId: expired[0]!.operatorAuthorityId }));
774
+ }
775
+
753
776
  /** Package-private v1 capability; raw input custody does not expose it. */
754
777
  export interface TelegramRoutingInputJournal {
755
778
  arm(input: TelegramRoutingInputAuthority & { publishedAtMs: number }): TelegramUpdateJournalEntry[];
756
779
  select(input: TelegramRoutingInputAuthority): { issued: boolean; entries: TelegramUpdateJournalEntry[] };
757
- expire(input: Omit<TelegramRoutingInputAuthority, "entries"> & { entry: TelegramUpdateJournalEntry }): TelegramUpdateJournalPendingAbandonmentResult;
780
+ expire(input: Omit<TelegramRoutingInputAuthority, "entries"> & { entry: TelegramUpdateJournalEntry }): TelegramRoutingInputExpiryResult;
781
+ inspectExpiry(updateId: number): TelegramRoutingInputExpiryEvidence | undefined;
782
+ inspectGroupExpiry(updateIds: readonly number[]): TelegramRoutingInputExpiryEvidence[] | undefined;
758
783
  }
759
784
 
760
785
  export interface TelegramUpdateJournalStore {
@@ -3133,6 +3158,7 @@ export interface TelegramUpdateJournalBindingRuntime {
3133
3158
  getActiveRecoveryKey: () => string | undefined;
3134
3159
  /** Exact historical proof lookup; no store, admission, execution, recovery or mutation port. */
3135
3160
  inspectSourceAbandonment: (journalBindingKey: string, updateId: number) => TelegramUpdateJournalAbandonedPendingEvidence | undefined;
3161
+ inspectSourceGroupExpiry: (journalBindingKey: string, updateIds: readonly number[]) => TelegramRoutingInputExpiryEvidence[] | undefined;
3136
3162
  inspectSourceCompletion: (journalBindingKey: string, expected: TelegramUpdateJournalSourceCompletion) => TelegramUpdateJournalSourceCompletion | undefined;
3137
3163
  inspectQueuedReceipt: (journalBindingKey: string, expected: TelegramUpdateJournalQueuedCompletion) => TelegramUpdateJournalQueuedReceiptEvidence | undefined;
3138
3164
  /** In-process `/new` succession: move unclaimed predecessor pending inputs into the successor session journal. */
@@ -3264,6 +3290,9 @@ export function createTelegramUpdateJournalBindingRuntime(deps: {
3264
3290
  return { journalBindingKey, updateId, retainedPath: original.path, operatorAuthorityId: disposition.operatorAuthorityId };
3265
3291
  });
3266
3292
  },
3293
+ inspectSourceGroupExpiry(journalBindingKey, updateIds) {
3294
+ return inspectHistoricalSource(journalBindingKey, file => inspectRoutingGroupExpiry(file, journalBindingKey, updateIds));
3295
+ },
3267
3296
  inspectSourceCompletion(journalBindingKey, expected) {
3268
3297
  const completion = validateJournalSourceCompletion(expected, getTelegramUpdateJournalBindingPath(journalBindingKey) ?? "journal");
3269
3298
  return inspectHistoricalSource(journalBindingKey, (file, _originals, path) => inspectJournalSourceCompletion(file, completion, path));
@@ -4899,6 +4928,7 @@ function createJournalStoreCore(
4899
4928
  update: entry.update,
4900
4929
  admittedAtMs: entry.admittedAtMs,
4901
4930
  ...(entry.preApprovalExcluded !== undefined ? { preApprovalExcluded: entry.preApprovalExcluded } : {}),
4931
+ ...(entry.routingInput ? { routingInput: { ...entry.routingInput } } : {}),
4902
4932
  state: "queued",
4903
4933
  queueKind: receipt.queueKind,
4904
4934
  queueReceiptId: receipt.receiptId,
@@ -5844,20 +5874,59 @@ function createJournalStoreCore(
5844
5874
  });
5845
5875
  },
5846
5876
  };
5847
- const abandonRoutingPending = journal.abandonPending.bind(journal);
5848
5877
  if (isTelegramUpdateJournalLegacyFamilyVersion(version)) journal.routingInputs = {
5849
5878
  arm(input) { return mutateRoutingInputs(input, false, input.publishedAtMs).entries; },
5850
5879
  select(input) { return mutateRoutingInputs(input, true); },
5880
+ inspectExpiry(updateId) {
5881
+ return runMutation(readCurrent => {
5882
+ if (!isSafeNonNegativeInteger(updateId)) throw createJournalError("invalid", path, "invalid routing expiry update ID");
5883
+ return inspectRoutingInputExpiry(readCurrent().file,
5884
+ createTelegramUpdateJournalBindingKey({ path, profileName: profile, botIdentity: expectedIdentity }), updateId);
5885
+ });
5886
+ },
5887
+ inspectGroupExpiry(updateIds) {
5888
+ return runMutation(readCurrent => inspectRoutingGroupExpiry(readCurrent().file,
5889
+ createTelegramUpdateJournalBindingKey({ path, profileName: profile, botIdentity: expectedIdentity }), updateIds));
5890
+ },
5851
5891
  expire(input) {
5852
- const original = validateJournalEntry(input.entry, path, version), lifetime = original.routingInput;
5853
- if (!lifetime || lifetime.phase !== "waiting" || lifetime.operatorUserId !== input.operatorUserId ||
5854
- !isSafePositiveInteger(input.operatorUserId) || typeof input.isCurrent !== "function") {
5855
- throw createJournalError("conflict", path, "routing input expiry lacks exact waiting authority");
5856
- }
5857
- const current = () => { const now = getNowMs(); return isSafeNonNegativeInteger(now) && now >= lifetime.expiresAtMs && input.isCurrent(); };
5858
- if (!current()) throw createJournalError("conflict", path, "routing input has not expired under current authority");
5859
- return abandonRoutingPending({ journalBindingKey: input.journalBindingKey, entry: original,
5860
- operatorAuthorityId: `telegram-owner:${input.operatorUserId}`, isCurrent: current });
5892
+ return runMutation(readCurrent => {
5893
+ const original = validateJournalEntry(input.entry, path, version), lifetime = original.routingInput;
5894
+ const binding = createTelegramUpdateJournalBindingKey({ path, profileName: profile, botIdentity: expectedIdentity });
5895
+ if (!lifetime || original.state !== "pending" || original.inputClaim || original.inputProvenance || original.queueOwner ||
5896
+ original.queueReceiptId || original.queueHandoff || original.failure || input.journalBindingKey !== binding ||
5897
+ lifetime.operatorUserId !== input.operatorUserId || !isSafePositiveInteger(input.operatorUserId) || typeof input.isCurrent !== "function") {
5898
+ throw createJournalError("conflict", path, "routing expiry requires an exact unclaimed chooser source");
5899
+ }
5900
+ const assertCurrent = () => {
5901
+ const now = getNowMs();
5902
+ if (!isSafeNonNegativeInteger(now) || now < lifetime.expiresAtMs || !input.isCurrent())
5903
+ throw createJournalError("conflict", path, "routing source has not expired under current authority");
5904
+ };
5905
+ assertCurrent();
5906
+ const current = sourceAccess ? readCurrent() : readCurrentStrict(false);
5907
+ const evidenceSha256 = createHash("sha256").update(JSON.stringify(original)).digest("hex");
5908
+ const failureId = `routing-expiry:${evidenceSha256}`;
5909
+ const existing = current.file.operatorDispositions?.find(value => value.updateId === original.updateId);
5910
+ const entry = current.file.entries.find(value => value.updateId === original.updateId);
5911
+ if (existing) {
5912
+ if (entry || !("dispositionKind" in existing) || existing.dispositionKind !== "legacy-custody" || existing.action !== "discard" ||
5913
+ existing.failureId !== failureId || existing.evidenceSha256 !== evidenceSha256 || existing.operatorAuthorityId !== `telegram-owner:${input.operatorUserId}`)
5914
+ throw createJournalError("conflict", path, "routing source has contradictory expiry evidence");
5915
+ assertCurrent();
5916
+ return { disposition: existing, duplicate: true, entryCount: current.file.entries.length, serializedBytes: current.serializedBytes };
5917
+ }
5918
+ if (!entry || !isDeepStrictEqual(entry, original)) throw createJournalError("conflict", path, "routing source changed before expiry");
5919
+ // A discard tombstone prevents old readers and delayed reports from replaying; no prompt-body archive is written.
5920
+ const now = getNowMs();
5921
+ const disposition: TelegramUpdateJournalLegacyCustodyDisposition = { dispositionKind: "legacy-custody", failureId,
5922
+ updateId: original.updateId, action: "discard", committedAtMs: now, authorizedAtMs: now, evidenceSha256,
5923
+ operatorAuthorityId: `telegram-owner:${input.operatorUserId}` };
5924
+ const published = publishMutation(current, current.file.entries.filter(value => value.updateId !== original.updateId), true,
5925
+ [...(current.file.operatorDispositions ?? []), disposition], current.file.acceptedThroughUpdateId,
5926
+ (boundary, target) => { onPublicationBoundary?.(boundary, target); assertCurrent(); });
5927
+ assertCurrent();
5928
+ return { disposition, duplicate: false, entryCount: published.file.entries.length, serializedBytes: published.serializedBytes };
5929
+ });
5861
5930
  },
5862
5931
  };
5863
5932
  if (!getInputContext) return { journal };
package/lib/queue.ts CHANGED
@@ -1028,7 +1028,7 @@ export function applyTelegramQueuePromptReactionDisposition<
1028
1028
  let nextItems = items;
1029
1029
  for (const [index, item] of items.entries()) {
1030
1030
  if (
1031
- !isPendingTelegramTurn(item) ||
1031
+ !isPendingTelegramTurn(item) || item.queueLane === "control" ||
1032
1032
  !isTelegramQueueItemInMessageScope(item, scope) ||
1033
1033
  !item.sourceMessageIds.includes(messageId)
1034
1034
  ) {
@@ -2405,6 +2405,7 @@ export interface TelegramQueueMutationRuntimeDeps<
2405
2405
  TContext,
2406
2406
  > extends TelegramQueueStore<TContext>, TelegramRuntimeEventRecorderPort {
2407
2407
  ctx: TContext;
2408
+ hasPendingDispatch?: () => boolean;
2408
2409
  allocateLaneOrder?: () => number;
2409
2410
  onItemsDiscarded?: (
2410
2411
  items: readonly TelegramQueueItem<TContext>[],
@@ -2416,6 +2417,7 @@ export interface TelegramQueueMutationRuntimeDeps<
2416
2417
  export interface TelegramQueueMutationControllerDeps<
2417
2418
  TContext,
2418
2419
  > extends TelegramQueueStore<TContext>, TelegramRuntimeEventRecorderPort {
2420
+ hasPendingDispatch?: () => boolean;
2419
2421
  allocateLaneOrder?: () => number;
2420
2422
  onItemsDiscarded?: (
2421
2423
  items: readonly TelegramQueueItem<TContext>[],
@@ -2739,7 +2741,14 @@ function commitReorderedTelegramQueueItemsRuntime<TContext>(
2739
2741
  items: TelegramQueueItem<TContext>[],
2740
2742
  deps: TelegramQueueMutationRuntimeDeps<TContext>,
2741
2743
  ): void {
2742
- deps.setQueuedItems([...items].sort(compareTelegramQueueItems));
2744
+ const pendingHead = deps.hasPendingDispatch?.() ? deps.getQueuedItems()[0] : undefined;
2745
+ const ordered = [...items].sort(compareTelegramQueueItems);
2746
+ if (pendingHead) {
2747
+ const index = ordered.findIndex(item => item.queueOrder === pendingHead.queueOrder && item.kind === pendingHead.kind &&
2748
+ item.chatId === pendingHead.chatId && item.replyToMessageId === pendingHead.replyToMessageId);
2749
+ if (index > 0) ordered.unshift(ordered.splice(index, 1)[0]!);
2750
+ }
2751
+ deps.setQueuedItems(ordered);
2743
2752
  updateTelegramQueueStatusRuntime(deps);
2744
2753
  }
2745
2754
 
@@ -2817,9 +2826,23 @@ export function applyTelegramQueuePromptReactionDispositionRuntime<TContext>(
2817
2826
  scope?: TelegramQueueMessageScope,
2818
2827
  ): boolean {
2819
2828
  const queuedItems = deps.getQueuedItems();
2829
+ const suppression = disposition.kind === "reaction-transition" ? disposition.suppressionEmoji :
2830
+ disposition.kind === "suppressed" ? disposition.emoji : disposition.kind === "priority-suppressed" ? disposition.suppressionEmoji : undefined;
2831
+ // Source-addressed control-lane prompts are explicit continuations. Synthetic model-switch turns have no source IDs.
2832
+ // A Pi-owned dispatched head cannot be removed: agent_start must consume that exact item, not its successor.
2833
+ const cancelled = suppression && deps.hasPendingDispatch ? queuedItems.filter((item, index) =>
2834
+ isPendingTelegramTurn(item) && item.queueLane === "control" && isTelegramQueueItemInMessageScope(item, scope) &&
2835
+ item.sourceMessageIds.includes(messageId) && !(index === 0 && deps.hasPendingDispatch!())) : [];
2836
+ if (cancelled.length) {
2837
+ if (cancelled.some(item => (item.admissionReceipts?.length ?? 0) > 0) && !deps.onItemsDiscarded) return false;
2838
+ deps.onItemsDiscarded?.(cancelled, deps.ctx);
2839
+ deps.setQueuedItems(queuedItems.filter(item => !cancelled.includes(item)));
2840
+ updateTelegramQueueStatusRuntime(deps);
2841
+ return true;
2842
+ }
2820
2843
  const changesLane = queuedItems.some((item) => {
2821
2844
  if (
2822
- !isPendingTelegramTurn(item) ||
2845
+ !isPendingTelegramTurn(item) || item.queueLane === "control" ||
2823
2846
  !isTelegramQueueItemInMessageScope(item, scope) ||
2824
2847
  !item.sourceMessageIds.includes(messageId)
2825
2848
  ) {