Files
PaperClipAI/doc/idle-voice-session-recovery.md
T
DottaandPaperclip e1ba77f067 fix(server): schedule live voice recovery from session changes and deadlines (#17576)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Voice calls retain durable session state and provider identities.
> - The chat maintenance timer scans live voice sessions even when no
call exists.
> - Session writes, saved expiry and active-call inspection deadlines
identify real work.
> - This pull request moves live-session recovery to the shared
coordinator.
> - Idle session recovery becomes quiet without losing exact-call
cleanup or current authorization checks.

## Linked Issues or Issue Description

Refs #17572. This PR is stacked on receiver discovery. It owns only live
voice session recovery. Reply delivery, inbound admission cleanup,
inbound history retention and call reports remain separate migration
slices.

**What happened?**
The one-second chat maintenance lane reads voice settings and scans live
sessions even when an instance has no calls or only terminal history.

**What did you expect?**
Reconstruct live calls on startup. Wake on committed session or
configuration changes. Inspect active calls and retry cleanup at actual
deadlines. Stop querying when no live obligation remains.

**Steps to reproduce**
Start an instance with no live voice sessions. Let startup settle. The
old maintenance method continues to read settings and session inventory
every chat maintenance pass.

## What Changed

- Add a voice-owned bounded deadline reader and scoped worker on the
existing coordinator. Retain concurrent writes across pages and reset
partial scans on restart. No tables or migrations are added.
- Separate generic connection health observations from configuration
changes. Existing receiver and command consumers retain their
notifications. Voice eligibility and provider operations remain in the
voice owner.
- Signal existing session transactions and configuration/feature
changes. Keep provider calls outside database transactions. Standalone
session writes use one small transaction helper.
- Preserve saved expiry, the two-minute unknown-creation grace,
active-call authorization and missed-hangup inspection, and
exact-provider cleanup. Recovery never creates a provider call.
- Retry transient authority lookup failures instead of treating them as
confirmed revocation.
- Fence expiration against a provider identity bound after discovery. A
late known call remains eligible for cleanup.
- Preserve work through rollback, drain, restart, concurrent refresh,
backoff and shutdown. Page inventory and rotate bounded batches across
companies.
- Leave the four other voice maintenance duties in the periodic lane.
- Document producer coverage and the supported writer contract in
`doc/idle-voice-session-recovery.md` and the dated audit.

## Verification

Published head: `f0b2e3cc22a18e7d08b10d4fc02aae4a19a52f30`. Receiver
#17572 has landed in master at
`dce004259394486f6361d0c0dd5a33a5272acc0a`. This normal merge reconciles
squash ancestry; the complete 22-file voice recovery patch is
byte-for-byte identical to the reviewed and tested `ad79c7e805` patch,
while all unrelated master changes are preserved. The PR now targets
master. Fresh exact-head CI and Greptile are running; interrupted
Ponytail turns are not completed verdicts under the maintainer’s
clarified merge policy, and an actual failing verdict still blocks.

- **210 distinct targeted cases pass on the current repair**: 19
complete recovery lifecycle cases and 191 complete voice-store,
coordinator, batching, startup and hostname cases. Production server
typecheck and build pass. Later edits record evidence only.
- CI `40633667141` on prior head `1b6323c164` failed the feature-change
assertion with `34050 < 25000`; its verification aggregate also failed.
The test now disables automatically advancing fake time, drives 20
controlled scheduler turns and requires all 205 calls to close before an
unrelated inspection deadline can mask a lost wake. Removing generation
replay fails with missing closures. The repair does not benchmark
database/host speed or raise the elapsed-time bound.
- A real committed exact-call regression reproduces wildcard-backoff
starvation. The worker keeps the broad retry but processes independent
exact hints and cached due calls. Each check reloads current state and
authority. Failed exact scopes remain fenced. Removing wildcard
isolation fails the intended assertion; a successful broad retry then
leaves the worker SQL-quiet. Full-row and lean queries now share the
state/scope/cursor predicate.
- Earlier acceptance on `712f3862b5` passed 725 distinct targeted cases.
Its measurements remain useful history: five real metadata writes and
one health check over 205 future calls selected 1,230 rows before the
inventory repair and zero afterward, without changing the saved
deadline. Those older test counts are not added to the current 210-case
count.
- Both external voice fixtures self-migrate; the current full
voice-store run uses a separate initially empty database. Earlier
fixture/method/observer failures, source mutations and partial edit
attempts remain in the [current
audit](https://github.com/paperclipai/paperclip/blob/ad79c7e805367fc7e2df3ea53c942ca6c7d27365/doc/plans/2026-10-25-voice-maintenance-sleep-audit.md#wildcard-failure-and-deterministic-generation-acceptance-october-28).
Source is restored after guarded mutations.
- Older CI `40632057899` failed because its trusted policy script was
absent from the checkout, while build separately received a runner
shutdown signal. E2E shards succeeded but their aggregate failed on
policy. The current checkout includes the landed policy tooling and its
checker/tests pass locally. The shutdown cause is unproven; neither old
failed run is counted as passing. Earlier full-local attempts stopped on
embedded PostgreSQL setup failures and did not pass.
- Fresh exact-head Greptile, security and all CI must pass before merge.
A completed failing Ponytail verdict blocks; an interrupted turn without
a verdict does not. No full current local repository matrix, live
provider acceptance, multi-writer support, Cloud wake, host suspension
or savings is claimed.

## Risks

- Process-local hints cover the existing active writer and shared
clients. Independent SQL writers or another replica need explicit wake
integration.
- Membership, task binding, phone preferences and other authority inputs
remain checked on the existing 30-second active-call inspection deadline
and on use. This is not an instant cleanup guarantee for every authority
mutation.
- Unknown creation retains its existing maximum-duration grace. Cleanup
failures retain retries. A provider request is never replayed merely
because its creation response is unknown.
- Genuine configuration changes retain bounded reconstruction. Metadata
writes can probe parent eligibility, while health-only observations
leave voice recovery quiet. Paging state is process-local and
reconstructs from durable rows after restart.
- Active calls, the other voice maintenance owners and connector
transports remain real obligations. This does not expand Cloud alarm
coverage, relax sleep eligibility or prove deployed suspension.
- The receiver and Discord parents have landed. Current-master
compatibility and exact-head CI and Greptile remain merge gates.

## Model Used

OpenAI GPT-6 via Codex, with repository inspection, reasoning, code
execution and test tools. The runtime does not expose a more specific
deployment identifier or context-window size.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either linked existing issues or described the issue in-PR
following the relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal
ticket identifier
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-28 07:38:48 -05:00

12 KiB

Voice session recovery without idle discovery

The live-session worker replaces the session-discovery portion of the recurring voice maintenance call. It uses the existing delivery coordinator, transaction signals and shared scheduler. It adds no tables or migrations. Reply delivery, inbound admission cleanup, inbound history retention and call reports are separate obligations; their existing maintenance remains until their own migrations land.

Work and deadlines

Existing chat_voice_sessions rows are authoritative. Startup reconstructs live sessions in UUID pages of 100, joined to their same-company endpoint and connection. Each background refresh reads at most one inventory page. An unfinished pass has a one-second continuation; it does not create a permanent empty scan. Only timing and eligibility fields are selected for discovery. The explicit foreground reconciliation method keeps its complete inventory behavior. Terminal history does not create a live-session recovery obligation. With no live sessions, the worker also avoids reading experimental settings after reconstruction.

State Next obligation
No provider identity yet Existing expiresAt plus two minutes of teardown grace; no provider creation replay
Known provider, ending or invalidated configuration Immediate cleanup; a recorded cleanup failure waits 30 seconds
Connecting or active call Earlier of session expiry and updatedAt + 30 seconds for current authorization and missed-hangup inspection
Other live state with provider identity Saved session expiry
Ended, failed or expired No live-session work; reports have a separate owner

Provider maximum duration remains the final bound for uncertain creation. A late provider response can bind identity while expiry is being considered. The expiration write requires that provider identity is still absent, so a newly known call retains exact-call cleanup rather than being marked terminal prematurely.

Each due call is reloaded before execution. The voice store checks current company, endpoint, credentials, caller, task, guest sandbox and binding authority. A 403/404 means authority no longer permits the call. A transient lookup error retains a retry; it does not authorize ending an otherwise valid call. The existing voice service owns provider inspection and termination, including single-flight termination and ambiguous-response confirmation. Recovery never creates a call.

Producers and transaction ownership

Owner Changes that wake live-session recovery
voice-session-store.ts Reservation, guest promotion, connecting-to-active transition and reply cursor writes that change the inspection timestamp
voice-session-service.ts Creation claim, binding, known/unknown creation failure, user end, cleanup outcome and foreground inspection
voice-inbound-service.ts Guest session creation, guest denial and signed terminal lifecycle callbacks
voice-reply-push.ts Confirmed remote end and unknown delivery outcome that changes session state/timestamp
Chat configuration helper Endpoint configuration, pause, reconnect, generation/credential changes, connection hints and company removal
Generic tool/access services Voice connection API mutation, removal and credential revocation during access removal
Instance settings Explicit chat-connector feature enable/disable

Existing transactions register the awaited signal before the session/configuration write. Standalone session updates use the small withVoiceSessionChange helper. Provider requests remain outside database transactions. Nested writes retain outer commit ownership, rollback does not dispatch work, and uncertain commit settlement stays with the shared database protocol.

Membership changes, task reassignment, phone preferences and other authorization inputs that do not mutate the session or connection remain checked on the existing 30-second active-call inspection deadline, as well as on use. This slice does not promise instant cleanup for every authority mutation or add cross-cutting voice hooks throughout those owners. Session activity that changes updatedAt retains the existing inspection-time semantics. The session expiry is always an upper bound for authorized active work.

The generic chat connection hint does not always carry a transport. It can cause a bounded empty voice lookup after a chat configuration write. It does not create idle polling or a recurring deadline. The shared helper contains only routing; voice eligibility, authority and provider policy stay with the voice owner.

Bounded configuration refreshes

The voice owner remembers the endpoint/connection fields that affect its existing deadline policy: endpoint status, runtime generation, connection enabled state, connection identity, and the chat feature flag. A broad configuration hint first reads a bounded page of these parent records. If those facts are unchanged, the cached call deadlines stay in place and no call inventory is read. Metadata-only writes do not move business deadlines. An unknown or changed parent requires bounded reconstruction; a real disablement still settles every affected call.

Only the voice owner interprets these facts. The generic health producer now sends a health-observation notification through the existing chat routing owner. It preserves receiver and command signals without generating a voice-configuration wake. An observation changes neither voice eligibility nor the saved call deadline. Supported session producers continue to signal their exact session ID.

The worker processes at most 25 dirty scopes and 25 provider checks per pass. Scope pagination continues without restarting on every newer hint. Failures retain their backoff and block stale entries in the failed scope. Successful pages can make progress while the remaining inventory is discovered. The existing coordinator can use the actual error deadline, including an already-due healthy backlog, rather than postponing it to an unrelated fallback retry.

Failure and lifecycle behavior

Refreshes coalesce by scope. Paged reconstruction keeps narrower producer hints, including a newly committed session behind the UUID cursor. A scope generation retains configuration changes that arrive between pages. Seen IDs protect concurrent refresh results; absent cached entries are removed only when the scope's inventory is complete. A new worker resets partial inventory state before reconstructing its saved obligations. Changes arriving during refresh remain dirty. Failed refreshes block use of stale cached entries in that scope. Per-scope and per-call failure backoff starts at 30 seconds and caps at five minutes. New hints do not erase failure backoff. Batches rotate across companies, process at most 25 calls, and continue an already due backlog after one second. A failed provider does not stall independent calls.

Drain and standby retain dirty work without passive SQL. Resume processes that work, and restart reconstructs deadlines from the rows. Shutdown joins admitted provider work and removes the coordinator's subscriptions and timers. Retained terminal history becomes quiet after any already committed hints settle.

Process-local signals support the existing single active database writer and its shared clients. Independent SQL writers or another replica need an explicit wake contract. This change neither expands Cloud alarm coverage nor relaxes sleep eligibility. Active calls and their real deadlines remain obligations; local worker quiet is not proof of host suspension or fleet savings.

Verification

The voice integration suite exercises actual service producers, guest intake, signed callbacks, current authority, unknown creation and adjacent reply/report behavior. The recovery-owner suite uses real PostgreSQL and controlled provider operations for outer commit/rollback, drain, restart, scope failure, mutation during refresh, paging, company fairness, shutdown and the late-provider-binding race. The latter race failed before the write-time guard and passes afterward.

The dated audit records exact commands, results, initial fixture failures and remaining acceptance work: voice migration audit.

Landed-foundation acceptance, October 27

The voice repair now integrates receiver parent 6e88ca0201, including Discord 547fdf2139 and master f8563d2507. Exact-patch comparison preserves the repaired 22-file scope and master's shared task-drain and native-checkpoint queue names. No database file is part of this slice.

All 725 distinct targeted cases pass again: 112 complete voice/shared/startup cases, 97 complete voice-store cases, 469 complete tool-access cases, and 47 complete health/receiver cases. Server production typecheck and build pass. Voice worker/store and health/receiver fixtures use separate initially empty self-migrating databases. Tool access uses a separate migrated clone. One initial tool-access invocation supplied the generic fixture variable instead of the suite-specific variable: 23 cases passed and 446 skipped. It was not a complete suite result or an assertion failure. The corrected invocation executes and passes all 469. Tests are not counted twice. Expected controlled failures and listener warnings remain in logs. Only documentation changes follow these checks. Fresh current-head reviews, CI and parent landing remain required. No full local matrix, provider acceptance, external wake or deployed sleep result is claimed.

Wildcard failure and deterministic generation acceptance, October 28

Current-head CI 40633667141 fails server shard 1 at the feature-change test's 34,050 ms result against its 25,000 ms bound; verification fails as an aggregate. This is an actual required assertion failure, distinct from the older interrupted build and missing policy script. The old test mixed automatically advancing fake time with real database latency. It now disables automatic clock advancement, drives 20 explicit scheduler turns, waits for each pass without advancing the clock, and requires every invalidated call to close before the unrelated periodic authority deadline. Removing generation replay still fails with missing closures. The test no longer benchmarks host/database speed or raises its business bound.

A real committed-session regression also reproduces wildcard starvation: while one broad reconstruction backs off, a newly ending exact call is not closed. The worker now retains the broad retry but considers ready exact hints and cached due calls independently. Each due call still reloads state and authorization in its owning check before effects. Failed exact scopes retain their own fence; the existing stale-cache failure regression still passes. Removing wildcard isolation fails the new exact-call assertion. Broad reconstruction becomes quiet after its successful retry, with no continuing SQL afterward.

The full-row execution lookup and lean inventory lookup now share the live-state, scope and cursor predicate. Their projections remain intentionally different. An initial edit script rejected the header cursor name before applying the timing and predicate changes; the intermediate 19-case result covers only the earlier worker repair. It is not the final repair evidence. The corrected source passes all 19 lifecycle cases and 191 complete voice-store/shared/startup cases: 210 distinct targeted cases. Production server typecheck and build pass. Both guarded regressions fail at their intended effect assertion and restore source afterward. The fresh voice-store database self-migrates. Existing controlled failure logs and signal-listener warnings remain recorded. The older 725-case acceptance belongs to its stated revision and is not added to this count. Fresh current-head reviews and all CI remain required before merging after receiver discovery.