Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to TeaAgent are tracked here.

## Unreleased

### Deprecated

- **`--git-sandbox`** (`teaagent agent run`) is deprecated. It has had no effect
since the git sandbox became automatic in git repositories. The flag is still
accepted, but passing it prints a deprecation warning to stderr. It will be
removed in a future release. Sandbox runtime behavior is unchanged.

- **Dependency bumps** (one PR per bump, each reproduced on current `main`
with `uv lock --upgrade-package`; supersedes the stalled Dependabot PRs):
`actions/setup-python` 7, `github/codeql-action` 4.37.3, `pytest` 9.1.1,
Expand Down
217 changes: 217 additions & 0 deletions docs/adr/0032-run-event-taxonomy.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,220 @@ PRE_COMPACT # Hook observability: before context compaction; public
```

The M0 spike covers RUN_STARTED, ITERATION_STARTED, TOOL_CALL_REQUESTED, TOOL_CALL_COMPLETED, TOOL_CALL_FAILED, RUN_COMPLETED, RUN_FAILED. Extended events are added in later phases as gates migrate.

---

## Amendment A1 (Accepted 2026-10-27): Background Lifecycle Events

> **Amendment status: Accepted by the owner on 2026-10-27; implementation pending.**
> This amendment does not change the Status of ADR 0032 above, which remains
> "Accepted and Implemented". The owner's answers to the A1.7 questions are
> recorded there; the payloads and tests below already reflect them.

### A1.1 Context

BG-001 §3.5 (`docs/specs/background-lifecycle-acceptance-spec-2026-07-11.md`)
requires the two background lifecycle transitions to join the ADR 0032 taxonomy.
Today they exist only as mutable fields on the background JSON record
(`submitted` at `start()`, `stopped_at` / `stop_signal` at `stop()`). Those
fields are overwritten on every read or stop, so the record cannot prove when a
background run was submitted or how often and when it was stopped. A repeated
`stop` converges on the first record and leaves no history. The
`BackgroundRunStore` writes nothing to any audit chain today.

The background child is an ordinary `teaagent agent run` process. It mints its
own `run_id` at startup, and the background argv builder
(`build_agent_run_command`) never passes one in. The parent that submits or
stops a background run has no run of its own. Any emit site must account for
both facts.

### A1.2 Decision

**Event names.** Two new `RunEventType` members, with serialized values that
match the existing activation test
`tests/lifecycle/test_background_lifecycle_spec.py::test_background_event_taxonomy_activates`:

| Member | Serialized `event_type` |
| --- | --- |
| `RunEventType.BACKGROUND_SUBMITTED` | `background_submitted` |
| `RunEventType.BACKGROUND_STOPPED` | `background_stopped` |

**Payloads.** All payload values are JSON-safe. Unknown keys are not permitted.

`background_submitted` is emitted once, after the record file is persisted:

| Key | Type | Meaning |
| --- | --- | --- |
| `background_id` | str | uuid4 hex, as in the record |
| `run_id` | null | Always `null` at submit: the child mints its run id later. Backfilled only in the record, never here. |
| `pid` | int | Pid of the detached child |
| `started_at` | str | ISO-8601 UTC, same value as the record |
| `command_digest` | str | `sha256` hex of `json.dumps(command)` (the full argv, including the task text). The digest is one-way, so the task text is not written to the chain. |
| `label` | str or null | Record label, if any |

`background_stopped` is emitted once, on the first transition in `stop()`,
after the stop record is persisted:

| Key | Type | Meaning |
| --- | --- | --- |
| `background_id` | str | As above |
| `run_id` | str or null | The child run id if the record had backfilled it at stop time, otherwise `null` |
| `pid` | int | Pid that was signalled (or found dead) |
| `stop_signal` | `"SIGTERM"` or `"SIGKILL"` | The signal recorded by `stop()` (see §6 of BG-001 for the accepted race) |
| `stopped_at` | str | ISO-8601 UTC, same value as the record |
| `exit_code` | int or null | Exit code known to `get()` at stop time, else `null` |
| `exit_code_derived` | bool | `true` only when `exit_code` is the U-P1-17 stand-in (a completed run's `0`). Never persisted to the record. |

Rules that follow from these payloads:

- `alive` is not a payload key. It is always false after a stop, and the record already says so.
- A stop on an already-dead process still emits (BG-001 §3.6: the stop is recorded without raising).
- A repeated `stop` on a stopped record returns early and emits nothing. The first stop's event is the only one.

**Emitting process.** The parent CLI process that performs the transition:

- `background_submitted`: the process that calls `BackgroundRunStore.start()`
(`agent run --background`, the automation tick, the deprecated ultrawork
wrapper).
- `background_stopped`: the process that calls `BackgroundRunStore.stop()`.
This is a different process from the child, and the child may have been
SIGKILLed before it could write anything.

**Destination (the choice this amendment makes).** A dedicated, append-only,
hash-chained file in the same tenant-scoped directory as the records:
`<BackgroundRunStore.dir>/lifecycle.jsonl`, written through
`AuditLogger(path=...)`. The envelope `run_id` is the synthetic value
`background:<background_id>`. Rationale:

1. The schema requires a non-empty `run_id` on every event, and no parent run
exists. The synthetic namespace gives each background record one stable,
known-at-submit identifier that the envelope can carry.
2. The submit event must survive even if the child never starts, and the stop
event must survive the child being SIGKILLed. A file the parent owns
satisfies both. A child-owned log does not.
3. The file is a new state boundary. It keeps background lifecycle out of
every run's chain and out of the workspace-wide chain, so run evidence
folds (`build_evidence_from_events`) are unaffected.
4. Concurrent writers (submit in one process, stop in another) are already
handled by `AuditLogger.record`, which reads the chain tip under the file
lock (the 2026-10-23 tip-resync fix). This amendment relies on that fix
and does not add a second lock.

**Ordering and failure.** Persist the record first, then emit. The record
remains the operational source of truth, and an emit failure cannot make
`stop()` report failure. In compliance mode a failed emit raises. Outside
compliance mode the failure is logged at ERROR with the category and the
`background_id`, and the gap is accepted and visible (open question Q4).

**What does not change.** `BackgroundRunRecord` fields (the BG-001 pinned
protocol), the record file format, `background list/show`, the cockpit, and
`exit_code` semantics. The derived `orphaned` state is not an event.

### A1.3 Schema change

- `RunEventType` (`teaagent/runner/_events.py`): add the two members. Add
matching entries to `_RUN_EVENT_TO_AUDIT_EVENT_TYPE`. The taxonomy-closure
check in `scripts/validate_event_spine_wiring.py` requires a lossless mapping
for every member and fails otherwise.
- `docs/audit-event.schema.json`: **no change.** The schema has no `enum` for
`event_type` (it is `{"type": "string", "minLength": 1}`). The two new names
are valid as written, so the schema-sync gate (G6) is satisfied by
confirming the schema is unchanged. The owner's brief expected enum
additions there; they do not exist, and this amendment does not add one.
- `docs/audit-events.md` lists event types. It needs a two-row addition in the
same change, and it is the only doc table to update.

### A1.4 Compatibility

- Additive. No existing event type is renamed, removed, or re-shaped.
- Readers that ignore unknown `event_type` values are unaffected. The schema
accepts any non-empty string.
- Run-level readers (`run_store`, evidence folds, receipts) never open
`lifecycle.jsonl`, so they see no new events.
- Rollback: stop emitting and revert the enum members and mapping entries.
`lifecycle.jsonl` stays on disk. It is a valid chain, and nothing reads it.
No migration is needed.
- Downgrade: an older build ignores the file entirely.

### A1.5 Rejected alternatives

1. **Emit into the child run's log** (`RunStore.run_path(run_id)`). Rejected.
At submit there is no `run_id`, and the child may never start. At stop the
child is already dead, and the stopper writing into the child's chain mixes
two owners in one chain. It also breaks the run's own evidence and receipt
boundary.
2. **Child emits `background_submitted` via a new argv flag** (for example
`--background-id`). Rejected. It couples the argv builder to the event,
makes the submit record depend on the child reaching its startup code, and
misattributes a parent decision to the child.
3. **Keep deriving the events from the record** (status quo). Rejected by
BG-001 §3.5. The record is mutable, `stop` is idempotent by overwrite, and
history is lost.
4. **Write into the workspace-wide or `pending-*.jsonl` run audit.** Rejected.
The shared workspace chain would couple background lifecycle to unrelated
writers. `RunStore.audit_logger(None)` creates a new file per call, so it
gives no stable chain.
5. **A new `background.*` event namespace or a separate schema file.** Rejected.
It breaks ADR 0032's single taxonomy and the schema's free-string
convention.
6. **Full command in the payload.** Rejected for the default. The spec (§6)
and this amendment record a digest only; no readable command summary is
written (owner decision Q3, 2026-10-27).

### A1.6 Exact test list (to be written with the implementation; none in this change)

Existing test to activate (its `skipif` is removed once the members exist):

1. `tests/lifecycle/test_background_lifecycle_spec.py::test_background_event_taxonomy_activates`

New file `tests/test_background_lifecycle_events.py`:

2. `test_start_emits_background_submitted_after_record_persisted`
3. `test_submitted_payload_keys_are_exact` (keys == the table in A1.2)
4. `test_submitted_run_id_is_null_at_submit`
5. `test_command_digest_is_sha256_of_argv_and_excludes_task_text`
6. `test_submitted_payload_has_no_readable_command` (digest only, Q3)
7. `test_stop_emits_background_stopped_on_first_transition`
8. `test_repeated_stop_emits_no_second_event`
9. `test_stop_on_dead_process_still_emits_background_stopped` (BG-001 §3.6)
10. `test_stopped_exit_code_derived_true_only_for_completed_stand_in` (U-P1-17)
11. `test_stopped_exit_code_null_for_reaped_failed_run` (U-P1-17 regression)
12. `test_envelope_run_id_is_background_namespace_and_non_empty`
13. `test_envelope_run_id_never_collides_with_minted_run_ids`
14. `test_lifecycle_chain_verifies_strict_with_submit_and_stop_from_two_loggers`
(same pattern as `tests/test_audit_chain_tip_resync.py`)
15. `test_readonly_store_emits_nothing`
16. `test_record_persist_failure_emits_nothing`
17. `test_emit_failure_compliance_mode_raises`
18. `test_emit_failure_outside_compliance_logs_and_returns`
19. `test_tenant_scoped_lifecycle_path_under_tenant_dir`
20. `test_run_evidence_fold_unchanged_without_background_events` (golden)

Existing gates that must stay green:

21. `scripts/validate_event_spine_wiring.py` (taxonomy closure)
22. `scripts/check_audit_schema_conformance.py` and
`tests/test_audit_schema_conformance.py` (schema unchanged)
23. `tests/lifecycle/test_background_lifecycle_spec.py` (all other tests)
24. `tests/test_background_run.py`, `tests/test_background_unified.py`,
`tests/test_background_exit_code_fallback.py`

### A1.7 Owner decisions (2026-10-27)

- **Q1 Destination — decided: dedicated `lifecycle.jsonl`.** The events go to
the hash-chained `<BackgroundRunStore.dir>/lifecycle.jsonl`, not the child
run's log.
- **Q2 Synthetic envelope id — follows from Q1.** `background:<background_id>`
is the envelope `run_id` of the dedicated chain as drafted. Not asked
separately; raise it at implementation review if a different id is wanted.
- **Q3 Command info — decided: digest only.** `command_digest` (sha256 of the
argv) is the only command field; there is no `command_summary`.
- **Q4 Emit failure — decided: persist then emit; log the gap, no retry.** Outside
compliance mode a failed emit is logged at ERROR and not re-emitted (no
duplicates); compliance mode still raises.
- **Q5 Placement — decided: amend ADR 0032** (this section), not a new ADR.
- **Q6 Spec wording — follows from Q1.** BG-001 §3.5 says the events are
"consumed by audit like all spine events"; with the separate chain, that
wording is edited in the implementation change.
- **Q7 Readers — out of scope.** Whether `background list/show` or the cockpit
shows lifecycle history is not decided here.
2 changes: 1 addition & 1 deletion docs/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ See the [Backwards Compatibility](integration-guide.md#backwards-compatibility)

**Session** — a saved conversation history (`ChatSession`) that persists multi-turn context across REPL invocations. Sessions are stored in `.teaagent/sessions/`. See [data-formats.md § Session JSON Schema](data-formats.md#session-json-schema).

**Git sandbox** — in a git repository the agent runs in an isolated branch by default, and `--git-sandbox` has no effect. Interactive runs ask first (Enter or `y` enables it, `n` runs in the local workspace for that run, `always` saves `git_sandbox_consent=always`); non-interactive runs and runs with `git_sandbox_consent=always` enable it automatically. Changes are discarded on failure or promoted to the main branch on success. Use `teaagent runs commit <run_id>` to merge a sandbox run.
**Git sandbox** — in a git repository the agent runs in an isolated branch by default, and `--git-sandbox` is deprecated with no effect (passing it prints a warning to stderr; it will be removed in a future release). Interactive runs ask first (Enter or `y` enables it, `n` runs in the local workspace for that run, `always` saves `git_sandbox_consent=always`); non-interactive runs and runs with `git_sandbox_consent=always` enable it automatically. Changes are discarded on failure or promoted to the main branch on success. Use `teaagent runs commit <run_id>` to merge a sandbox run.

---

Expand Down
2 changes: 1 addition & 1 deletion docs/api/cli-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Alias: `run` and `ask` are identical.
| `--max-estimated-cost-cents N` | int | 0 (no cap) | Cost cap in cents |
| `--clarify` | flag | false | Run ambiguity scoring before model call |
| `--allow-destructive` | flag | false | Allow write/patch/shell tools |
| `--git-sandbox` | flag | no effect | Accepted for compatibility only. In a git repository the sandbox is enabled automatically (prompted when interactive, automatic when non-interactive or `git_sandbox_consent=always`) |
| `--git-sandbox` | flag | no effect | **Deprecated.** Prints a deprecation warning to stderr and has no effect; it will be removed in a future release. In a git repository the sandbox is enabled automatically (prompted when interactive, automatic when non-interactive or `git_sandbox_consent=always`) |
| `--git-sandbox-auto-stash` | flag | false | Auto-stash dirty state before sandbox |
| `--parallel N` | int | — | Run N parallel approaches (tournament) |
| `--approach HINT` | string | — | Hint for parallel approach (repeatable) |
Expand Down
8 changes: 4 additions & 4 deletions docs/generated/docs-aging-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ Regenerate: `python3 scripts/report_docs_aging.py`

## Corpus Cost (G5 Signal)

**Total docs:** 676 (baseline 582 at diagnosis, delta +94)
**Live corpus (non-archive):** 378
**Total docs:** 677 (baseline 582 at diagnosis, delta +95)
**Live corpus (non-archive):** 379
**Archive-tier docs (total):** 298
**Working-tier docs unreferenced by INDEX.md:** 324
**Working-tier docs unreferenced by INDEX.md:** 325

Dead-weight candidates (working tier, not linked from INDEX.md):

Expand All @@ -100,7 +100,7 @@ Dead-weight candidates (working tier, not linked from INDEX.md):
- `adr/0017-backend-adapter-interfaces.md`
- `adr/0018-async-from-sync-pattern.md`
- `adr/0019-phase-4-federated-swarm-consensus.md`
- ... and 304 more
- ... and 305 more

### Archive-tier growth (files added per month, last 6 months)

Expand Down
Loading
Loading