mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 17:45:53 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Paperclip starts local command-line sessions and stores provider credentials through managed connections > - One OpenAI Codex or xAI Grok subscription connection held a credential lease for the full agent run > - A second run then waited for the first run, and concurrent runs could overwrite a newer credential > - The write-back must compare fresh credentials while it holds the row lock > - This pull request removes the run lease, keeps the revocation guard, and bounds Codex timestamps against the host clock > - The benefit is safe concurrent use of one subscription connection with newest-credential selection ## Linked Issues or Issue Description **What happened?** A managed OpenAI Codex or xAI Grok subscription connection held a credential lease for the full agent run. A second run waited for the first run to finish. The write-back gate also rejected any row change before it compared credential freshness. **Expected behavior** Concurrent runs should start on one subscription connection. The server should keep the newest valid credential and reject a credential write after a person revokes the connection. **Steps to reproduce** 1. Start two runs that use one OpenAI or xAI subscription connection. 2. Let both provider tools refresh the credential. 3. Finish the runs in either order. 4. Confirm that the newest valid credential remains in the connection. **Paperclip version or commit** `ec25bf1e4a81d1729a6d7276e6a486587cff4a0b` **Deployment mode** Local dev (`pnpm dev`), built from source. **Agent adapter(s) involved** Codex. The server path also covers xAI Grok subscription connections. **Database mode** Embedded PGlite for local development, and external Postgres for deployments. **Access context** Both board and agent runs can use managed connections. **Additional context** The provider command-line tool refreshes credentials inside the sandbox. The server copies the result back after the run. Two long runs can still refresh one token hours apart, so the provider can reject the second refresh. The server cannot observe that provider call. ## What Changed - Remove the full-run credential lease for OpenAI Codex and xAI Grok subscription connections. - Lock and re-read the connection row before credential write-back. - Accept only a strictly newer credential, while keeping the connection revocation guard. - Reject Codex freshness timestamps more than five minutes ahead of the host clock. - Add tests for both completion orders, xAI cleanup, revocation, the Codex time bound, and agent hiring. - Update the connection and run-log documentation. ## Verification - `server/src/__tests__/ai-connections.test.ts` passes with 42 tests. - `packages/adapters/codex-local/src/server/codex-auth-merge-decision.test.ts` covers the five-minute boundary and the one-millisecond overflow. - `packages/adapters/codex-local/src/server/codex-auth-merge.test.ts` passes. - `server/src/__tests__/agent-hire-ai-connections.test.ts` covers OpenAI and Anthropic. - The project type check reports no new error in changed files. - GitHub Actions must pass on this pull request. ## Risks The write-back now permits concurrent runs, so the provider may reject a later refresh when both long runs use one token. The server keeps the revocation guard and rejects future-dated Codex timestamps. No schema change occurs. ## Model Used OpenAI Codex, GPT-5. Context window and exact deployment build are not exposed in this run. The model used tool calls, code inspection, and test verification. ## 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>
190 lines
10 KiB
Markdown
190 lines
10 KiB
Markdown
# Run-Log Events
|
|
|
|
Run-log events write to the `heartbeat_run_events` table
|
|
(`packages/db/src/schema/heartbeat_run_events.ts:6-20`). They are not
|
|
Paperclip Telemetry events, and they are not OpenTelemetry exports. A run-log
|
|
event needs no operator endpoint.
|
|
|
|
## Native PRP Run-Log Events
|
|
|
|
The hidden native coordinator writes each validated PRP event to the bound
|
|
run's existing event stream before it acknowledges the runner. The row keeps
|
|
the PRP `eventType`, source instance, source event ID, source sequence, protocol
|
|
schema version, and a SHA-256 digest of the canonical source envelope. Its
|
|
payload is `{ "prpEvent": <canonical PRP event> }`.
|
|
|
|
PostgreSQL JSONB cannot represent NUL (U+0000), which can occur in command
|
|
output such as Vite virtual-module paths. The run-event payload column uses a
|
|
lossless storage codec for these events: the JSONB projection renders NUL as
|
|
the literal `\u0000`, and the reserved `$paperclipRunEventJsonV1` field contains
|
|
the original serialized JSON as a doubly escaped string. Ordinary payloads
|
|
retain their existing representation. Drizzle reads restore the exact original
|
|
payload before replay, hash validation, redaction, or API presentation. SQL
|
|
queries can still inspect ordinary routing fields in the projection; raw SQL
|
|
readers of the whole payload must apply `decodeRunEventPayload`. The column
|
|
remains JSONB and requires no schema migration.
|
|
|
|
The writer locks the native `heartbeat_runs` row and allocates the existing
|
|
per-run `seq` cursor. A byte-equivalent retry reuses the first row; a changed
|
|
retry or source-sequence gap is rejected. Company, issue, agent, run, session,
|
|
and runner-source bindings must match the persisted native run. Bootstrap
|
|
tickets, reconnect leases, authentication proofs, encryption keys, and raw
|
|
credential material are never written to the run log.
|
|
|
|
These records remain run-log events. They do not create an OpenTelemetry or
|
|
Paperclip Telemetry export, and legacy adapters do not use this writer.
|
|
|
|
## Native Restart Recovery Run-Log Event
|
|
|
|
Paperclip writes a `native.recovery.transition` event for every native restart
|
|
classification and for graceful restart suspension. This immutable run-log
|
|
record lets operators reconstruct recovery decisions without exporting data to
|
|
Paperclip Telemetry or OpenTelemetry.
|
|
|
|
The payload contains the restart kind, recovery request id when one exists,
|
|
runner disposition, and the controller generation and provider attempt for a
|
|
claimed recovery. Live-runner adoption also records the runner PID, process
|
|
group, and process-start fingerprint. A non-claim disposition records a bounded
|
|
reason instead. Graceful suspension records the signal and confirms that it did
|
|
not create a retry run.
|
|
|
|
The event never includes bootstrap tickets, reconnect leases, authentication
|
|
proofs, encryption keys, environment variables, provider credentials, command
|
|
arguments, or an unsanitized stderr stream. Detailed failed-attempt diagnostics
|
|
remain in the bounded `native_run_finalizations.recovery_history` ledger.
|
|
|
|
## Native Local Process Stop Evidence
|
|
|
|
The server writes `native.local_process_stopped` in the same transaction that
|
|
clears a local run's process identity, after it verifies that its PID and process
|
|
group are absent. The payload contains only those process IDs. Remote process
|
|
IDs are never checked against the control-plane host.
|
|
|
|
The server writes `native.process_start_requested` before a backend can spawn,
|
|
and `native.process_identity_recorded` when it stores a new native process
|
|
identity. Either invalidates an earlier local stop receipt, including a crash
|
|
before the new PID callback. Continuation admission accepts only the latest
|
|
server-authored event among these three types; provider
|
|
source events cannot supply stop authority. These records stay in the local run
|
|
log and do not add Telemetry or OpenTelemetry data.
|
|
|
|
## Verified Local Codex Replacement Evidence
|
|
|
|
The server writes `native.stopped_text_turn_verified` in the same transaction
|
|
that schedules a fresh successor for a stopped local Codex run. It records the
|
|
runner and provider process identities, retained-state digests, provider thread
|
|
and turn IDs, and IDs of exactly receipted task-completion calls. The server first
|
|
checks the complete turn inventory, process-stop receipt, and execution binding.
|
|
Unknown actions or changed retained state prevent this event and replacement.
|
|
|
|
The record documents why the old execution can be retired. It does not make the
|
|
old session resumable, rewrite provider files, or authorize replay on its own.
|
|
It remains in the local run log and adds no Telemetry or OpenTelemetry export.
|
|
|
|
## Sandbox Startup Run-Log Event
|
|
|
|
Paperclip writes one `run.startup.step` event to the run log for each bring-up
|
|
step. This event is a run-log record, not a first-party telemetry event. The
|
|
generated telemetry contract does not cover it, so this section is its canonical
|
|
contract.
|
|
|
|
The event payload carries only three fields.
|
|
|
|
| Field | Type | Meaning |
|
|
| --- | --- | --- |
|
|
| `step` | string | The bring-up step name, for example `stage.sync`. |
|
|
| `durationMs` | number | The wall time of the step. A skipped step reports `0`. |
|
|
| `outcome` | string | The step outcome (`ok`, `skipped`, or `failed`). |
|
|
|
|
The event no longer carries the per-step round-trip count or the provider
|
|
duration fields. It dropped `roundTrips`, `providerExecMs`, `providerGetMs`,
|
|
`createRuntimeMs`, and `ensureSessionMs`. The startup spans in
|
|
[`doc/observability.md`](observability.md) carry that detail now. The
|
|
`sandbox.exec` child spans hold the round-trip and provider durations. The
|
|
`acp.handshake` step span holds the create-runtime and ensure-session
|
|
sub-times.
|
|
|
|
To read the detailed timing, use the startup spans. The spans need an OTLP
|
|
endpoint. A run with no endpoint keeps only the three run-log fields above.
|
|
|
|
## Run Phase Timing Run-Log Event
|
|
|
|
Paperclip writes one `run.phase.timing` event to the run log for each
|
|
run-lifecycle phase. This event is a run-log record, not a first-party telemetry
|
|
event. The generated telemetry contract does not cover it, so this section is its
|
|
canonical contract. The producer is `emitRunPhaseTiming` in
|
|
`packages/adapter-utils/src/acpx-engine/startup-timing.ts`.
|
|
|
|
The event payload carries only three fields.
|
|
|
|
| Field | Type | Meaning |
|
|
| --- | --- | --- |
|
|
| `phase` | string | The run-lifecycle phase name from the closed allowlist below. |
|
|
| `durationMs` | number | The wall time of the phase. A negative or a non-finite value clamps to `0`. |
|
|
| `outcome` | string | The phase outcome (`ok` or `failed`). |
|
|
|
|
The `phase` field is one member of a closed, low-cardinality allowlist. The
|
|
producer drops any event whose phase name is outside this allowlist, so a
|
|
free-form label never reaches the run log. The allowlist has twelve phase names.
|
|
|
|
| Phase | Meaning |
|
|
| --- | --- |
|
|
| `place_workspace` | Place the run workspace. |
|
|
| `start_transport` | Start the agent transport. |
|
|
| `create_runtime` | Create the agent runtime. |
|
|
| `ensure_session` | Ensure the agent session exists. |
|
|
| `configure_session` | Configure the agent session. |
|
|
| `prepare_turn` | Prepare the turn. |
|
|
| `turn` | Run the turn. |
|
|
| `end_session` | End the agent session. |
|
|
| `settle_reuse` | Settle the session for reuse. |
|
|
| `stop_transport` | Stop the agent transport. |
|
|
| `sync_back` | Sync the workspace back. |
|
|
| `release_staging_lease` | Release the staging lease. |
|
|
|
|
The payload never carries a command, an argument, a path, an environment value,
|
|
or a raw identifier. The event rides the `ctx.onEvent` run-event bridge and is
|
|
run-log-only. It needs no OTLP endpoint.
|
|
|
|
## Related instrumentation
|
|
|
|
The sandbox duplex transport also writes one run-log event as one of its three
|
|
sinks. See the
|
|
[Sandbox Duplex Transport Instrumentation](observability.md#sandbox-duplex-transport-instrumentation)
|
|
section in the Observability contract.
|
|
|
|
## Execution recovery
|
|
|
|
Provider identity diagnostics remain in the local run log. They record the notification method, expected and received thread/turn identifiers, and the classification (root, verified descendant, stale, unrelated informational, or invalid authoritative). They omit the original provider payload and credentials. Repeated informational notices are bounded.
|
|
|
|
Recovery lifecycle events retain the original structured failure code, retry attempt, next retry time, and predecessor/successor identifiers. Durable status delivery uses an idempotency marker; delivery grants no provider authority. Failed publication is retried without repeating provider work. These records are not first-party Telemetry.
|
|
|
|
Bounded retry exhaustion writes one lifecycle receipt per run, retry reason,
|
|
scheduled attempt, and retry limit. Repeated or concurrent recovery checks reuse
|
|
that receipt, including receipts from earlier builds, without advancing the event
|
|
sequence or publishing another live event. Attention reads select the latest
|
|
matching receipt in PostgreSQL and project only the run's issue/task identifiers
|
|
from its context, so historical duplicate receipts cannot multiply run contexts
|
|
in server memory. Existing duplicate events do not require deletion or migration.
|
|
|
|
## Codex resume usage snapshot
|
|
|
|
The native runner retains a bounded local `harness.diagnostic` event with code
|
|
`codex_resume_usage_snapshot`. It identifies `thread/tokenUsage/updated` as
|
|
`resume_usage_snapshot`, retains the reported thread and completed-turn IDs,
|
|
and records cumulative usage counters. It does not include provider credentials
|
|
or message content. The event establishes the accounting baseline; it is not a
|
|
new billable usage receipt or a user-facing provider warning. Other provider
|
|
identity checks remain in force.
|
|
|
|
## AI subscription contention
|
|
|
|
A fresh task execution cannot enter this wait. A run that already entered this
|
|
wait writes an informational `lifecycle` event to the local run log. Its
|
|
payload contains only `retryScheduled`, a boolean that reports
|
|
whether the scheduler created a retry.
|
|
The message distinguishes an automatic retry from work that is no longer eligible.
|
|
This pre-provider wait records `ai_connection_busy` on the cancelled run and does
|
|
not consume the provider-failure retry allowance. The event contains no credentials
|
|
and creates no Telemetry or OpenTelemetry export.
|