Files
PaperClipAI/packages
839d41aabb fix(server): retire owned plugin listeners before idle sleep (#19358)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Plugins receive durable agent lifecycle events through a registered
host.
> - The host uses a PostgreSQL LISTEN connection to wake delivery.
> - An idle report could return `none` while that host-owned connection
stayed open.
> - This change gives that listener an exclusive owner and closes it
during the current idle hold.
> - Release or expiry restores listening before journal replay.
> - Failed close and pending work continue to block sleep.

## Linked Issues or Issue Description

Refs #18988, which is now merged. This focused ten-file change now
targets master. Current integrated candidate:
`0f83005122ef1bdb1f1e08f8f58424c9a840ebb1`, with actual master
`644ee66f5d4ae8365fe8b9eca57df2be7405b5ab` preserved through normal
merges. No history was rewritten.

**What happened?**

A normal local App with an installed lifecycle plugin could return an
idle `none` report while its lifecycle LISTEN backend and socket stayed
open.

**Expected behavior**

The host must close its own listener before it reports idle safety. It
must retain other subscriptions, durable events, company scope, retry
deadlines and useful reactivation.

**Steps to reproduce**

Start the normal server with a lifecycle plugin. Create an agent and
wait for its durable acknowledgment. Stop the server, create another
agent while it is stopped, and restart with the same files. Request an
owned idle hold after ordinary replay. Check the exact listener backend
and matched Node socket before shutdown. Let the valid hold expire
naturally, then make an ordinary authorized agent update and check its
durable plugin acknowledgment.

Live executing or persistent preview services remain active. The
separate hosted preview dependency
[paperclip-cloud#478](https://github.com/paperclipai/paperclip-cloud/pull/478)
retains its own App and operator prerequisites. This PR does not claim
deployed preview or suspension acceptance.

## What Changed

- Add a private, exclusive database listener owner that inherits the
normal client policy.
- Prepare plugin lifecycle retirement after native retirement under the
same current idle hold.
- Use one delivery-eligibility helper for dispatch and idle journal
inspection. It preserves the same manifest, capability, company-disable
query, and skip rules.
- Close only the owned listener. Keep failed close ownership and
inspection failures visible.
- Resume listening on normal release or expiry, before journal replay.
- Preserve disabled-feature retry history and the original deadline on
re-enable.
- Await lifecycle disposal during normal shutdown and consumer teardown.
- Add owner regressions and document the lifecycle contract.

## Verification

**Current integrated source:**
`0f83005122ef1bdb1f1e08f8f58424c9a840ebb1`. Fresh normal Runner
TypeScript passed in 0.785 seconds. The complete normal App layered
semantic check passed in 28.284 seconds. All tracked source, the
lockfile and the 18 separate private witness inputs stayed unchanged.
Known compiler owners closed and the isolated temporary home was
removed.

**Current product qualification:** at measured integration
`896cc9b62993e6ed10418d7a7417e1ddbf8dc89c`, all three whole files passed
with no failures or skips: lifecycle delivery 40, runtime hooks 20, and
runtime assignments 24. The actual physical LISTEN and natural-expiry
controls passed. All owned clients and captured processes closed.
Independent final session checks were zero before normal deletion of
each successful test database.

**Normal full-App physical retirement:** the original case passed in
47.574 seconds. It exercised real startup, readiness, SDK callbacks,
durable ACK, foreign-company exclusion, physical stop, offline input and
cold replay. The initial unknown hold and its ordinary release remain in
the evidence. After the original 30.5-second cadence, a new valid hold
returned quiescent. The exact current cold-App LISTEN backend and its
matched Node socket closed before server stop.

**Normal full-App natural expiry:** the separate original case passed in
77.391 seconds under its original 90-second body cap. The final valid
30-second hold expired naturally. A new listener backend and matched
current App socket appeared before fresh input. An ordinary authorized
agent-name PATCH produced journal event 20, the actual SDK callback and
durable ACK. Foreign exclusion and readiness assertions passed. The
final measured hold had no manual release, replacement hold or shortened
TTL. The original preparatory unknown hold, ordinary release and
30.5-second cadence remain explicit.

Both App cases used the normal current server helpers and actual SDK
bundling/worker entry. A private compatibility change supplied the same
owned HOME at the exact existing child-fork boundary. It did not change
the product helpers, business assertions or timing limits. These
separate acceptance fixtures remain outside the focused product diff.

**Source carry to the current candidate:** all nine other owned files,
listener mechanics, actual producer paths, SDK paths and measured hold
behavior are unchanged. The sole owned intersection from newer master is
an independent environment-delete cleanup callback in `app.ts`. The
plugin witnesses do not call that route. Normal merges preserve every
unrelated master blob and file mode. The fresh current semantic checks
above qualify the integrated source. New master Native crash-recovery
and saved-copyback Retry capabilities are not claimed by these plugin
cases.

**Actual master merge target:** newer master contains retained plugin
Runner release/resume and other Native recovery fixes. The projected
combined target passed fresh normal Runner TypeScript in 0.799 seconds
and the full App semantic check in 29.715 seconds. All ten plugin-owned
blobs and modes remain exact to the published head; every unrelated
master change is preserved. The measured plugin cases create no Native
provider turn or warm Native session. The unchanged
native-first/plugin-second ordering still checks the native result
before listener retirement. Later sparse-checkpoint and Native
response-publication changes do not enter those paths. This is a scoped
source carry, not new hosted Runner acceptance.

**Failures retained:** an earlier reactivation fixture's real timer
returned one millisecond before its original wall-clock expiry. Its
private fixture now waits any positive remainder and retains the same
expiry assertion and limits. The earlier partial-composition `unknown`
result remains distinct from normal-startup proof. Current qualification
also exposed missing normal workspace dependencies and a private guard
that assumed loose Git refs. Normal owned dependency setup and an exact
loose-or-packed reader fixed those setup failures. No business
assertion, timeout, permission boundary or product sleep gate was
relaxed.

Repeat focused product coverage with a fresh external fixture database
and the normal server Vitest command, one worker:

```sh
pnpm --filter @paperclipai/server exec vitest run src/__tests__/plugin-lifecycle-delivery.test.ts --maxWorkers=1 --no-file-parallelism
pnpm --filter @paperclipai/server exec vitest run src/__tests__/agent-runtime-hooks.test.ts --maxWorkers=1 --no-file-parallelism
pnpm --filter @paperclipai/server exec vitest run src/__tests__/agent-runtime-assignments.test.ts --maxWorkers=1 --no-file-parallelism
```

Exact current head `0f83005122ef1bdb1f1e08f8f58424c9a840ebb1` has 60
successful checks, three skips and one neutral result, with no pending
or failing contexts. Greptile completed its full review at 5/5 with no
actionable issue. All root and nested review pages were read; the
earlier eligibility finding is resolved. Ponytail's latest turn stopped
before a verdict. A normal retry was requested; no current Ponytail
verdict or score is claimed. Previous `3ca86ec` CI and scores are
historical. A full local repository typecheck/test/build or browser run
is not claimed. Controlled local App/SDK/PostgreSQL proof does not
establish live provider, deployed Railway/Neon suspension, signed Cloud
alarms or global exactly-once delivery.

## Risks

- A listener close failure or uncertain inspection must continue to
block sleep.
- Release, expiry, configuration changes, and journal arrival can race
with retirement. Epoch and current-hold checks retain conservative
results.
- Delivery remains at least once across uncertain ACK boundaries. This
change does not add global exactly-once delivery.
- The database helper closes only its exclusive listener client. Other
owners and subscriptions keep their resources.

## Model Used

OpenAI `gpt-6.1-sol`, reasoning and code/tool use. The managing Codex
agent implemented shared database/composition seams and reviewed the
change; its exact model ID was not exposed. The runtime does not expose
a verified 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 (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
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>
Co-authored-by: Codex <noreply@openai.com>
2026-11-06 22:56:47 -06:00
..