Files
PaperClipAI/doc/idle-teams-consent-expiry.md
T
DottaandPaperclip 4a89456bb3 fix(server): schedule Teams consent expiry from saved deadlines (#17590)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Teams file delivery saves consent and transfer state in the
database.
> - Unused consent must expire even when no new message arrives.
> - The maintenance loop currently checks this state every 30 seconds.
> - This PR schedules each existing consent deadline and wakes on
committed changes.
> - Empty or settled consent work stops querying while expiry remains
recoverable.

## Linked Issues or Issue Description

**What existing behavior does this improve?**
Teams unused file-consent expiry. This is slice 26 of the
recurring-server migration stack, following merged #17586 and targeting
master.

**Current behavior**
The Teams maintenance callback scans for expired unused consent and
abandoned transport attempts together, even when consent work is empty.

**Proposed behavior**
The Teams owner reconstructs unused consent at startup and reloads one
transfer after a committed write. The shared scheduler runs its saved
expiry. Abandoned attempts keep their current recovery until a separate
slice migrates them.

**Reason and benefit**
Remove unnecessary consent queries without losing expiry, atomic
publication projection, or recovery after restart.

**Breaking changes**
No schema or API change. The explicit combined recovery method remains
available. Cloud sleep eligibility and external alarms remain unchanged.

## What Changed

- Add a Teams-owned scoped deadline worker for the three unused consent
phases. Registered reconstruction yields after one 100-row page per
scope refresh and resets cursors on registration; the explicit
diagnostic inventory still returns every eligible row.
- Signal before insertion and the existing versioned update inside the
owning transaction. Nested writes wait for outer settlement.
- Extract the existing locked per-transfer recovery operation. Preserve
authority checks, atomic projection, bounded lock waits and
unknown-outcome quarantine.
- Remove unused consent from periodic maintenance selection and
execution. Keep abandoned attempts as a separate obligation.
- Register the worker through existing app lifecycle gates. Add
lifecycle, producer, production wiring and precision regressions.
- Document supported writers, remaining sleep limits and the failure
ledger in `doc/idle-teams-consent-expiry.md` and its linked audit.

## Verification

Published head: `9e84cfab1f795279c8cec413a946631f1d781bd2`. This
follow-up changes only the inherited voice-retention test and its CI
failure ledger; all production behavior is unchanged from `36f43922`. It
normally merges actual landed master/report parent
`8049fbf51d0701ca6b242e4b1198e5449f7661b2`. The verified 12-path Teams
patch was byte-identical across the actual-parent ancestry integration.
This test-only CI follow-up adds one existing retention test path,
bringing the final scope to 13 paths. Every unrelated master change,
report documentation fix, and migration 0397 is preserved. No database,
shared-worker, workflow or lockfile change belongs to this slice.

- **223 distinct selected cases pass:** 209 across five complete
transfer/publication/consent/runtime/app-wiring suites, plus 14 selected
real Board-producer and Teams-authority integration cases. The other
1,580 integration cases were skipped, not passed. The protocol and
production consumers use separate new PostgreSQL databases migrated
through the application migrator.
- A real 105-transfer regression failed against the original worker
because one registered startup pass selected 105 rows instead of
yielding at 100. The corrected worker uses the existing paged shared
contract. Restarting the same service midway through discovery selects
the first 100 again, restores all 105 saved deadlines and expires them
at the original deadline. SQL is quiet before the deadline and after
settlement, with no consent-card or upload requests. The one-shot
inventory still returns all 105 rows.
- A guarded mutation removing only cursor reset fails at five rows
instead of 100 after restart. The original production source was
restored before the complete suites. The initial regression fixture
incorrectly faked network timers before its first SQL connection and
timed out; that setup failure is retained separately from the corrected
105-versus-100 regression. No assertion or timeout was weakened.
- The real Board producer registers the worker before publication,
commits the transfer wake, preserves the saved expiry, remains SQL-quiet
until due and atomically projects expiry to a cancelled publication
without another provider request. Existing authority and consent
transitions remain covered.
- Complete lifecycle coverage retains all three consent phases,
precision rounding, rollback and held outer commit, exact-row hints,
expiry recheck, failed projection retry, corrupt-row fairness,
drain/resume, shutdown and separation from abandoned-attempt
maintenance. Production server typecheck, build and module boundaries
pass. Server production typechecking does not statically check test
files; those files were executed.
- [Current bounded reconstruction and failure
ledger](https://github.com/paperclipai/paperclip/blob/36f43922b0a408f76adb7de31280812e9b752019/doc/plans/2026-10-25-teams-consent-expiry-sleep-audit.md#bounded-reconstruction-and-current-parent-acceptance-october-28)
records exact scope, counts, guarded mutation, corrected timer setup,
earlier fixture failures and limitations. Earlier 222-case and
combined-stack results belong to their stated historical heads; they are
not added to 223 or claimed rerun here. The [stack
ledger](https://github.com/paperclipai/paperclip/blob/36f43922b0a408f76adb7de31280812e9b752019/doc/plans/2026-10-24-recurring-server-sleep-stack.md)
retains inherited failure history. No complete local repository-suite,
live Teams tenant, Cloud wake, host/database suspension or savings claim
is made.
- **Fresh exact-head Greptile 5/5 and all CI are required.** Actual
failing Ponytail findings must be repaired. A missing/stopped/paused
turn without a verdict is an authorized nonblocker; an active no-verdict
review waits at most 30 minutes from its original request/start
(25-minute monitor cutoff). Older scores and parent checks are not
accepted as current passes.

- **Preserved inherited CI failure:** prior head `36f43922` had
exact-head Greptile/Ponytail 5/5 but CI run `40708566195` server6 job
`122887395220` failed the retention fairness assertion (2,291 passed/one
failed/one skipped in that shard). A fixed final UUID reproduces the
wrong expectation that an undiscovered page-two row is already deleted.
The corrected test controls time, expects that row to remain before
discovery and requires its deletion in the next fair batch, then all
remaining history and idle SQL. The existing five-second wait budget and
global timeout are unchanged. All 13 retention lifecycle cases pass;
bypassing company rotation fails the corrected assertion, and the
original shared helper is restored byte-for-byte. Earlier 223-case Teams
acceptance remains valid for unchanged Teams source and is not relabeled
as a new complete repository run. [Full CI repair
ledger](https://github.com/paperclipai/paperclip/blob/9e84cfab1f795279c8cec413a946631f1d781bd2/doc/plans/2026-10-25-teams-consent-expiry-sleep-audit.md#inherited-retention-ci-fairness-repair-october-28).
Fresh CI and exact-head reviews are required for this test-only
follow-up; old scores are stale.

- **Current merge gates:** all 58 checks are now clean for this exact
head and Greptile is 5/5 with no actionable findings. The current
Ponytail turn is paused without a verdict, an authorized nonblocking
state. Workflow `40712464720` attempt 1 independently hit npm
`ECONNRESET` during the serialized8 job's tool self-installer, before
its tests ran. Exactly one targeted job retry was requested after the
original workflow finished; attempt 2 completed successfully on the
unchanged head. No code, dependencies, workflow, timeout or review timer
changed for that retry. The original setup failure is retained in its
[job
log](https://github.com/paperclipai/paperclip/actions/runs/40712464720/job/122901243926);
the successful replacement is [job
122910350074](https://github.com/paperclipai/paperclip/actions/runs/40712464720/job/122910350074).

## Risks

- Missed supported producer signals could delay expiry until restart.
Real insertion, cancellation, consent transitions, caller-owned
transactions and Board publication are covered.
- Corrupt or locked rows must retain retry without starving healthy
work. Paging, fairness, atomic rollback, redaction, drain/resume and
shutdown tests cover these paths.
- Independent SQL writers and other processes need their own
notification contract. No persistent DB listener is added.
- Attempt recovery still scans periodically until the next slice. A
local deadline is not a durable Cloud alarm. This PR does not claim
whole-instance sleep.
- This slice is reconciled with its landed parent; current-head remote
checks/review and live-master composition proof still gate merge. Later
stack duties remain separate PRs.

## Model Used

OpenAI GPT-6 through Codex, with repository inspection, tool use and
code execution. The exact serving model ID and context-window size are
not exposed in this session.

## 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
Paperclip ticket id or instance-derived details
- [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
- [x] All Paperclip CI gates are green
- [x] 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 10:32:30 -05:00

4.2 KiB
Raw Blame History

Teams consent expiry and idle work

Unused Teams file consent now uses the shared delivery coordinator and scheduler. Existing chat_teams_file_transfers rows own its state. There is no new table, independent timer, provider request or external-wake capability.

Obligation and trigger

Only consent_pending, awaiting_consent and upload_pending rows are eligible. Each uses its saved expiresAt. Registered startup reconstruction yields after one 100-row UUID page per scope refresh and resumes through the shared scheduler. Registration resets the owner’s cursors so a partial scan restarts safely. The explicit one-shot inventory method still returns the complete inventory. Committed writes reload the affected transfer ID. The service registers intent before insertion and its central versioned update, within the existing transaction. Nested savepoints wait for outer settlement. The shared work-signals protocol retains ambiguous outcomes; callers do not manage recovery wrappers.

Configured endpoints, completed history, unknown outcomes and active transport attempts do not keep this worker scanning. Pending consent retains its actual future deadline. PostgreSQL submillisecond timestamps round up to the next millisecond; checking a deadline does not extend it.

Expiry and recovery

At the deadline, the worker reloads the current row and delegates to the existing Teams protocol owner. That owner locks publication then transfer, verifies the current authority digest, rechecks phase and expiry, and updates the transfer and publication projection in the same transaction. It does not send a consent card, upload data or send a file-info card. Lock waits remain bounded. A changed phase is re-evaluated; a deleted or settled row removes the cached obligation.

A failed load, lock or projection retains retry work. Corrupt rows do not starve other transfers or companies. Errors emitted by this worker are generic and do not contain SQL parameters, private transfer envelopes or provider capabilities. The shared worker owns coalescing, batching, dirty hints, drain/resume and shutdown. Shutdown joins an in-flight expiry; restart reconstructs the saved deadline.

The explicit expireAndRecover diagnostic method retains combined behavior. Production periodic maintenance now selects and executes abandoned attempts only. Attempt recovery is a separate remaining migration. Unknown provider outcomes remain quarantined; this change does not authorize a resend.

Ownership and limitations

Provider phase selection and expiry live in the Teams module and existing Teams transfer service. Shared app wiring only registers the owner and lifecycle gate. The queue topic is a notification namespace, not a new durable queue. Other providers and the shared scheduler receive no Teams protocol policy.

Supported producers use the process's owning DB client. Independent SQL writers or another process require their own wake contract. On-use authorization remains with the protocol. Cloud eligibility and external alarm coverage are unchanged: a future in-process deadline cannot wake stopped compute. This proves the changed worker can become quiet, not fleet savings or complete instance suspension.

Verification

Real PostgreSQL protocol cases cover all three phases, producer activation, rollback and held outer commit, restart, pre-deadline quiet, timestamp precision, projection rollback, fairness, drain/resume and shutdown. Production integration registers the worker before Board publication and proves expiry cancels the publication without another provider call. Existing authority, publication and consent suites remain covered. See the audit and failure ledger.

Current local acceptance covers 223 distinct cases: 209 across five complete protocol/publication/consent/runtime/wiring suites, plus 14 selected real Board producer and Teams-authority integration cases. The other 1,580 integration cases were skipped. A real 105-row regression proves bounded startup, same-owner restart, complete saved-deadline expiry, and quiet SQL. A guarded missing-reset mutation fails that regression. See the audit for exact revisions, earlier fixture failures, current-parent integration status and deployment limits.