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
42 changes: 42 additions & 0 deletions architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -5477,6 +5477,48 @@ Freedom is vendored from a pinned unpublished commit under
`packages/cli/src/repl/vendor/freedom/`, as a pristine copy and a patched copy
whose every difference belongs to one named patch. See its `PROVENANCE.md`.

### One chronology, and the authority that answers it

The Sessions reading is one list, not two appended: every Prompt this process
observes takes a slot when it is scheduled and keeps it through publication, so
a turn that becomes durable stays the same mounted node in the same place, and
concurrent Prompts that publish out of order do not reorder each other. The
slot, its order and its live key are process-local — no route, model, Journal
record or public Core Api carries one.

Filtering is by the conversation key a provider actually issued; an authored
`<Session>` name is not one. Selecting or clearing changes `route.session` and
nothing else, and background Agent work never changes it.

A pending request is a fact on the turn that is waiting. Arrival mounts it and
does nothing else. Activating it records that request's opaque key in
process-local state — never in the location — and opens the Sessions-only
`+permission` drawer; the reducer decides and `program.ts` alone holds the
authority that answers, so the drawer closes only when the request is really
settled and a stale, unknown or unoffered action settles nothing. A retained
permission audit is an inert reading, and a historical prefix has no live
requests to answer.

Both readings are windowed at the layout region that will place them, rather
than described whole and clipped: only what the window holds is described, so
only that is mounted, focusable, drawn and pointable. A window moves from the
position its frame is drawing rather than from the number stored, because a
resize changes what a window holds and a delta added to a stale number would
spend a press on state nobody can see. The canonical location is bounded the
same way and for the same reason: in a narrow frame it shares one region with
every control on the screen, so it takes at most three rows and says what it is
not showing. Their window controls and
the two surface controls stay outside the moving window. A narrow frame mounts
one routed outlet and both surface controls, so the outlet the route did not
select is absent everywhere — `mounted()`, the frame, the target map — while the
way to it is not.

Whether a pointer may activate a row is decided by what the row is, not by how
its key is spelled: a control answers Enter, so a pointer on it asks for the
same thing, and a line has nothing to activate. Keys carry provider session keys
and field names, so a rule about spelling would have taken the pointer away from
a conversation or a field whose name happened to look like something else.

### From a frame to a terminal, and back

Below the tree, one more direction:
Expand Down
72 changes: 72 additions & 0 deletions packages/cli/src/repl/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,50 @@ export interface ReplLivePermission {
readonly choices: readonly ReplLiveChoice[];
}

/**
* Where one observed Prompt sits, and what it became.
*
* A slot outlives the live turn it began as. Publication changes where a turn's
* facts come from — the record, rather than this process's observation — and it
* is the same turn a person was already looking at, so the slot keeps one
* position and one identity across that change. Without it a reader has only
* two disjoint lists and has to guess which durable row replaced which live one.
*
* `order` is observation order, which is the order Prompts were scheduled in;
* `durable` is the name the record was journaled under, once the append that
* replaced this turn has been accounted for. Everything here is process-local:
* no slot, key or order reaches a location, the model or the Journal.
*/
export interface ReplAgentSlot {
/** The live key this Prompt was observed under, and stays mounted as. */
readonly key: string;
/** Where this Prompt sits among the ones this process observed. */
readonly order: number;
/** The durable name its record was journaled under, or none while it is live. */
readonly durable: string | undefined;
/**
* The facts this turn had when it published, or none while it is still live.
*
* Kept so the row can never blank: the append is accounted for here and the
* record is projected by whoever owns the transition, and a reader that had
* only the two lists would show nothing for this turn in between.
*/
readonly last: ReplLiveTurn | undefined;
}

/** Everything this process knows about live Agent work right now. */
export interface ReplAgentReading {
/** Live turns in the order their Prompts were observed. */
readonly turns: readonly ReplLiveTurn[];
readonly requests: readonly ReplLivePermission[];
/**
* Every Prompt this process observed, live or since published.
*
* In observation order. A reader presents these rather than concatenating the
* live turns with the retained ones, because a turn that has published is
* still in the same place it was.
*/
readonly slots: readonly ReplAgentSlot[];
}

/**
Expand Down Expand Up @@ -211,6 +250,14 @@ interface LiveTurn {
failure: string | undefined;
}

/** Mutable bookkeeping for one observed Prompt's place in the reading. */
interface Slot {
readonly key: string;
readonly order: number;
durable: string | undefined;
last: ReplLiveTurn | undefined;
}

interface LiveRequest {
readonly key: string;
readonly turn: string;
Expand Down Expand Up @@ -238,6 +285,15 @@ function frozenTurn(turn: LiveTurn): ReplLiveTurn {
});
}

function frozenSlot(slot: Slot): ReplAgentSlot {
return Object.freeze({
key: slot.key,
order: slot.order,
durable: slot.durable,
last: slot.last,
});
}

function frozenRequest(request: LiveRequest): ReplLivePermission {
return Object.freeze({
key: request.key,
Expand Down Expand Up @@ -302,6 +358,8 @@ export function useReplAgent(mode: PermissionMode): Operation<ReplAgentKernel> {
const changes = createSignal<ReplAgentReading, never>();
const turns: LiveTurn[] = [];
const requests: LiveRequest[] = [];
/** One per Prompt this process observed, kept after publication. */
const slots: Slot[] = [];
/** The live turn core began in a scope, until that scope's prompt claims it. */
const begun = new Map<Scope, LiveTurn>();
/**
Expand All @@ -325,6 +383,7 @@ export function useReplAgent(mode: PermissionMode): Operation<ReplAgentKernel> {
let reading: ReplAgentReading = Object.freeze({
turns: Object.freeze([]),
requests: Object.freeze([]),
slots: Object.freeze([]),
});
let keys = 0;
let failure: Error | undefined;
Expand All @@ -339,6 +398,7 @@ export function useReplAgent(mode: PermissionMode): Operation<ReplAgentKernel> {
reading = Object.freeze({
turns: Object.freeze(turns.map(frozenTurn)),
requests: Object.freeze(requests.map(frozenRequest)),
slots: Object.freeze(slots.map(frozenSlot)),
});
}

Expand Down Expand Up @@ -388,6 +448,9 @@ export function useReplAgent(mode: PermissionMode): Operation<ReplAgentKernel> {
failure: undefined,
};
turns.push(turn);
// Its place, taken when the Prompt was scheduled rather than when it
// finished: a turn that publishes first did not thereby happen first.
slots.push({ key: turn.key, order: slots.length + 1, durable: undefined, last: undefined });
announce();
return turn;
}
Expand Down Expand Up @@ -652,6 +715,14 @@ export function useReplAgent(mode: PermissionMode): Operation<ReplAgentKernel> {
),
);
}
// The live facts are gone, and the record holds them now — but this is
// the same turn, in the same place. The slot says which record that is,
// by the durable name the journal wrote it under.
const slot = slots.find((candidate) => candidate.key === turn.key);
if (slot !== undefined) {
slot.durable = event.description.name;
slot.last = frozenTurn(turn);
}
retire(turn);
project();
},
Expand Down Expand Up @@ -685,6 +756,7 @@ export function useReplAgent(mode: PermissionMode): Operation<ReplAgentKernel> {
requests.length = 0;
failures.length = 0;
turns.length = 0;
slots.length = 0;
begun.clear();
publishing.clear();
// Projected, not announced. A reader still holding this owner sees a
Expand Down
Loading
Loading