paperclip/doc/run-log-events.md

157 lines
7.8 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> }`.
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.
## 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.
## Sandbox performance batches
When full sandbox diagnostics are enabled by `OTEL_EXPORTER_OTLP_ENDPOINT`, the
host writes `sandbox.performance.batch` system events after measured execution.
These rows stay in the instance database; they are neither first-party Telemetry
events nor a replacement for inspecting actual OpenTelemetry exports. No batch
is produced with the endpoint unset.
The payload schema is `paperclip.sandbox-performance.v1`, with a hashed
`runHash`, `records` (at most 250 per event), and the run's `dropped` count.
Each record contains a fixed operation name, span id, optional parent/trace id,
start time, duration, operation outcome, and closed safe attributes. Host times
use epoch milliseconds and monotonic durations. Records with
`clock: "remote_relative"` instead contain offsets from a remote command's
start and must not be placed on the host timeline as absolute timestamps.
The root has numeric retained-record and dropped-record counts.
The default buffer holds 20,000 records per run; additional records increment
the drop count. A missing root or a nonzero drop count means the local record
set is incomplete. Event persistence happens in bounded batches after the
operation timings end, not synchronously for each file. A sink failure stops
batch persistence without changing the original task outcome. A successful
task alone therefore does not prove that all timing records were saved.
## 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.
## Native Process Rotation
`native.session.process_rotation` records a controller-initiated close of a
settled warm session before opening the next run. Its system-stream payload
contains `reason` (`run_scoped_github_capability` or `configuration_changed`),
`previousRunId` (nullable), `runId`, `companyId`, `agentId`, `nativeSessionId`,
and `runnerInstanceId`. It contains no token, environment, path, or credential
value. The event records rotation intent after the prior process closes; the
new run must still succeed to establish successful continuation.
A subsequent run uses a fresh process for its own GitHub capability while
preserving the durable conversation. Warm qualification accepts a changed
process fingerprint only with a matching system rotation event for that exact
run transition and a process start inside the new run. Unexpected restarts,
configuration changes, and conversation, runner-instance or sandbox changes
remain failures. Same-process warm reuse remains required without a rotation.