Skip to content

✨ Slice A of #854: retain a safe Agent permission audit and project Agent conversations - #857

Open
taras wants to merge 2 commits into
mainfrom
agent/issue-854-projection
Open

taras wants to merge 2 commits into
mainfrom
agent/issue-854-projection

Conversation

@taras

@taras taras commented Sep 28, 2026

Copy link
Copy Markdown
Owner

Why

A REPL that shows Agent work has to read it from the Journal, and the Journal did
not hold enough: a <Prompt> recorded what it was asked and what it answered,
but not the permission decisions taken on the way. Nothing could show what a turn
was allowed to do, and nothing could group turns into the conversations they
belong to. This is the retained-truth half of #854 — the surface that presents it
comes later.

What changes

Before: a retained Prompt records its input, output and status. Permission
decisions exist only while the turn runs. A REPL location can name an execution,
a scope, a history position and a drawer.

After: a retained Prompt also records the permission decisions it was given, as
safe data — tool call id, title, kind, the offered options and the outcome —
and nothing else. The projector reads retained Prompts into frozen turns with
their owning scope, their chronology and the conversation each belongs to. A
location can name one conversation (?session=<key>) and a live permission
drawer (+permission, on the Sessions surface only).

How it works

<Prompt> turn → observed permission decisions → PromptRecord.permissions
  → projectRepl() → frozen turns grouped by sessionKey → route resolves ?session / +permission

The observation is installed inside the turn's own scope in runPrompt(), which
is the only place that can see a decision and still be the turn that owns it.
Audits join the record when the turn settles, in occurrence order rather
than completion order.

Nothing live crosses the durable boundary: the audit is built from copies of the
safe fields, so a provider's rawInput, its session key and its cwd never
reach the journal — proved by a canary and by mutating every source object the
instant a request settles.

Chronology comes from PromptRecord.sequence, which is prompt scheduling, not
append order. Conversations are grouped by the provider's own sessionKey; a
turn that never reached a provider has no key, stays out of every group and
appears only in the All list.

Review guide

Start with: packages/core/src/agent/journal.ts

Then review:

  1. packages/core/src/agent/journal.ts — the record's new member, its
    serialization, and what it refuses
  2. packages/core/src/agent/function-components.ts — where the observation is
    installed (14 lines inside the turn's scope)
  3. packages/cli/src/repl/model.ts — retained turns, chronology, groups
  4. packages/cli/src/repl/route.ts — ?session and +permission
  5. specs/acp-client-spec.md

Look carefully at:

  • That only safe fields are copied, and that nothing retains a caller-owned
    object: the durable value and the serialized bytes are both checked.
  • Backward compatibility: a record written before this change parses with
    permissions undefined.

What must stay true

  • No live object or raw provider input reaches the journal — enforced by copying
    named safe fields and checked by the rawInput canary and the
    mutate-on-settle row.
  • Old records still parse — checked by a legacy record with no permissions.
  • Chronology is sequence, not append order — checked by swapping two records'
    sequence values and leaving append order alone.
  • A conversation is the provider's sessionKey, never a guess — checked by one
    agent in two conversations, two agents in one, and an unassigned failure that
    joins no group.
  • +permission is live-only and Sessions-only — checked by refusals at a frozen
    position, on the wrong surface, and with no request waiting.

How to verify it

deno task test packages/core/tests/agent-function-components.test.ts \
  packages/cli/tests/repl-model.test.ts packages/cli/tests/repl-route.test.ts
  • Zero, one and three audits round-trip exactly, and fourteen malformed audits
    each refuse the whole record — fails if an audit is dropped or read loosely.
  • A malformed sequence, status, sessionKey, audit or source position
    refuses the projection with no partial model — fails if hostile data is shown.
  • ?session round-trips canonically beside the existing query members, and an
    absent key refuses and leaves navigation untouched — fails on an ad-hoc session
    path or an empty unknown view.

Scope

Included

  • The safe permission audit on PromptRecord, and its observation
  • Retained Agent turns, chronology and conversation groups in the REPL model
  • ?session and the +permission drawer in the route grammar
  • test-weights.json from run 36417683621 at a403cc57

Intentionally unchanged

Generated or mechanical changes

  • test-weights.json is the artifact of Measure test weights run
    36417683621 at a403cc57, committed unchanged. No shard recalibration.
  • packages/cli/src/repl/application.ts is mechanical: the new route and model
    members on the initial route, the refused view and the empty model.

Risks and limitations

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and test results.

Part of #854.

A completed Prompt now retains a closed account of the permission requests
answered while its turn ran, and the REPL can read Agent turns and
conversations out of a hostile Journal and filter them from its canonical
location. Nothing live runs yet: no Agent provider reaches `xmd repl`, no
observer, no pending request, no form and no new command option.

The audit is assembled field by named field from the request as it arrives and
gains the decision when it is made, so `rawInput`, the Session, provider
callbacks and errors cannot cross the durable boundary, and mutating what the
caller kept afterwards changes nothing. Members are in arrival order rather
than completion order, and a request that raised has no member. The list is
closed: an option kind or outcome this build cannot read refuses the whole
prompt record. Observing decides nothing — the installed policy still answers.

The projection reads `agent_prompt` through core's own record parser, orders
the chronology by the sequence each record states rather than by append order,
groups conversations by the provider's session key alone, and leaves a turn
that reached no provider in the chronology and in no conversation. Every value
is copied out of the event graph and frozen, and one turn is one object however
it is reached. `ReplRoute` gains `session`, canonically last in the query, and
the Sessions-only `+permission` drawer, which carries no key. The resolver's
`asking` boolean becomes one closed live-availability value, and a historical
prefix ignores all of it.

Slice A of #854.
The artifact of run 36417683621, unchanged. This slice adds one test file and
changes what several others run, and a weight is only true of the runner that
measured it.
@taras
taras added this pull request to stack #858 September 28, 2026 22:24

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant