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
16 changes: 16 additions & 0 deletions packages/code/src/cli.golden.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,22 @@ beforeAll(async () => {
'the record is set up',
);
delete process.env.MNEMA_RUN;

// ── The run that DID something, read while it is still the latest one.
//
// Every other reading of a run in this transcript lands on the second one, which is
// opened below and stays empty — so without this pair the file would pin `wrote` in
// its empty case only, and a clause that never shows a value is a clause a change
// could empty without moving a byte here. Read HERE, between the two runs, because
// `resume` answers with the latest and the second one is a line away.
//
// Nothing volatile crosses: the run has ENDED, so it carries no age and no idleness
// — which is also the case the phrase treats differently, `wrote` riding an ended run
// where the durations do not.
section('reads', 'the run that did something');
await mnema('reads', 'resume', '--actor', anchor);
await mnema('reads', 'resume', '--actor', anchor, '--json');

const second = await mnema(
'writes',
'run',
Expand Down
94 changes: 83 additions & 11 deletions packages/code/src/cli.reads.golden.txt
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,73 @@ $ mnema verify
| census [key-without-tail] public <backup-key-fingerprint>: committed public key has no tail on disk — the tail may have been dropped (a botched merge), never written (an empty tail is not versioned), or removed
| private: no record here — nothing has been written to this tree on this machine, so there is nothing to rule on

### the run that did something
$ mnema resume --actor <anchor>
| <anchor-short> last run <run-first> (ended) — set the record up · wrote 8 task.transitioned, 5 decision.transitioned, 5 knowledge.linked, 3 decision.recorded, 3 skill.transitioned, 2 memory.captured, 1 handoff.recorded, 1 observation.recorded, 1 skill.created, 1 task.created
| 0 run(s) still open
$ mnema resume --actor <anchor> --json
| {
| "actor": "<anchor>",
| "lastRun": {
| "id": "<run-first>",
| "agent": "agent-alpha",
| "who": "<anchor>",
| "open": false,
| "startedAt": "<at:01>",
| "wrote": [
| {
| "kind": "task.transitioned",
| "count": 8
| },
| {
| "kind": "decision.transitioned",
| "count": 5
| },
| {
| "kind": "knowledge.linked",
| "count": 5
| },
| {
| "kind": "decision.recorded",
| "count": 3
| },
| {
| "kind": "skill.transitioned",
| "count": 3
| },
| {
| "kind": "memory.captured",
| "count": 2
| },
| {
| "kind": "handoff.recorded",
| "count": 1
| },
| {
| "kind": "observation.recorded",
| "count": 1
| },
| {
| "kind": "skill.created",
| "count": 1
| },
| {
| "kind": "task.created",
| "count": 1
| }
| ],
| "goal": "set the record up",
| "outcome": "the record is set up",
| "endedAt": "<at:31>",
| "lastFactAt": "<at:30>",
| "thisSession": false
| },
| "focus": {
| "actor": "<anchor>",
| "openRuns": []
| }
| }

### a populated project
$ mnema search
| 13 record(s):
Expand Down Expand Up @@ -1217,7 +1284,7 @@ $ mnema antipatterns --json
| }
$ mnema status --actor <anchor>
| <anchor-short> — where things stand.
| last run <run-second> (open) — read the record back · open <duration> · nothing recorded in it
| last run <run-second> (open) — read the record back · open <duration> · nothing recorded in it · wrote nothing
| 1 run(s) still open
|
| 3 live task(s):
Expand All @@ -1242,7 +1309,8 @@ $ mnema status --actor <anchor> --json
| "agent": "agent-beta",
| "who": "<anchor>",
| "open": true,
| "startedAt": "<at:31>",
| "startedAt": "<at:32>",
| "wrote": [],
| "goal": "read the record back",
| "thisSession": false,
| "ageSeconds": <seconds>
Expand All @@ -1255,7 +1323,8 @@ $ mnema status --actor <anchor> --json
| "agent": "agent-beta",
| "who": "<anchor>",
| "open": true,
| "startedAt": "<at:31>",
| "startedAt": "<at:32>",
| "wrote": [],
| "goal": "read the record back",
| "thisSession": false,
| "ageSeconds": <seconds>
Expand Down Expand Up @@ -1312,10 +1381,10 @@ $ mnema status --actor <anchor> --json
| }
$ mnema focus --actor <anchor-short>
| <anchor-short> — 1 open run(s):
| <run-second> agent-beta — read the record back · open <duration> · nothing recorded in it
| <run-second> agent-beta — read the record back · open <duration> · nothing recorded in it · wrote nothing
$ mnema focus --actor <anchor>
| <anchor-short> — 1 open run(s):
| <run-second> agent-beta — read the record back · open <duration> · nothing recorded in it
| <run-second> agent-beta — read the record back · open <duration> · nothing recorded in it · wrote nothing
$ mnema focus --actor <anchor> --json
| {
| "actor": "<anchor>",
Expand All @@ -1325,15 +1394,16 @@ $ mnema focus --actor <anchor> --json
| "agent": "agent-beta",
| "who": "<anchor>",
| "open": true,
| "startedAt": "<at:31>",
| "startedAt": "<at:32>",
| "wrote": [],
| "goal": "read the record back",
| "thisSession": false,
| "ageSeconds": <seconds>
| }
| ]
| }
$ mnema resume --actor <anchor>
| <anchor-short> last run <run-second> (open) — read the record back · open <duration> · nothing recorded in it
| <anchor-short> last run <run-second> (open) — read the record back · open <duration> · nothing recorded in it · wrote nothing
| 1 run(s) still open
$ mnema resume --actor <anchor> --json
| {
Expand All @@ -1343,7 +1413,8 @@ $ mnema resume --actor <anchor> --json
| "agent": "agent-beta",
| "who": "<anchor>",
| "open": true,
| "startedAt": "<at:31>",
| "startedAt": "<at:32>",
| "wrote": [],
| "goal": "read the record back",
| "thisSession": false,
| "ageSeconds": <seconds>
Expand All @@ -1356,7 +1427,8 @@ $ mnema resume --actor <anchor> --json
| "agent": "agent-beta",
| "who": "<anchor>",
| "open": true,
| "startedAt": "<at:31>",
| "startedAt": "<at:32>",
| "wrote": [],
| "goal": "read the record back",
| "thisSession": false,
| "ageSeconds": <seconds>
Expand Down Expand Up @@ -1491,8 +1563,8 @@ $ mnema switch
| edit-asks-a-person on the pause before a file is written where the record asks that a person look first — the rules go on arriving, and nothing stops
$ mnema usage
| 2 run(s):
| <run-second> agent-beta <at:31> → open no transcript
| <run-first> agent-alpha <at:01> → <at:30> no transcript
| <run-second> agent-beta <at:32> → open no transcript
| <run-first> agent-alpha <at:01> → <at:31> no transcript
|
| Read from <sandbox>/home/.claude/projects — 0 host session(s) there record work in this project.
| These counts come from Claude Code's own transcripts on this machine, not from the record: nothing here is signed, `mnema verify` does not cover it, and the host deletes a transcript on a retention it decides. Which host session belongs to which run is this reading inferring it from two clocks, not a fact the record states. Nothing was written to produce it.
Expand Down
15 changes: 14 additions & 1 deletion packages/code/src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,13 @@ const DECISION_VERDICTS = andListed(actionsRequiring('decision', 'note'));
* missing MEANS something (a run that recorded nothing at all), and a reader left to
* infer that from silence would infer whatever it already believed.
*
* `wrote` is said here too, though it is not one of the asker-relative fields the rest
* of this text is about: it is on EVERY run reported, open or ended, and an agent that
* does not know a field exists does not read it. It is the one thing here that says
* what a session DID rather than how it stands, which is what "where was I" actually
* asks — and the empty array is spelled out for the same reason the absent
* `idleSeconds` is, except that here the emptiness is the answer rather than a gap.
*
* And it says what none of this proves. An open run is not evidence of a live
* session: nothing in the record says a process is running, so a run left behind by
* a session that was killed is indistinguishable from one an agent is idle inside.
Expand All @@ -257,7 +264,13 @@ const OPEN_RUN_CONTRACT =
'it started) and, when anything has been recorded in it, `idleSeconds` (since its ' +
'last recorded fact); NO `idleSeconds` means the run has recorded nothing at all. ' +
"Both compare this machine's clock with the writer's, so a run written on another " +
'machine reports whatever those two clocks differ by. None of this says a ' +
'machine reports whatever those two clocks differ by. EVERY run reported — open or ' +
'ended — also carries `wrote`: what was written IN it, as one `{kind, count}` entry ' +
'per kind of fact recorded there, commonest first. An EMPTY array means the run ' +
'recorded nothing (it is never absent, so an empty one is an answer and not a gap), ' +
'and the run’s own start and end are not counted — only the work. The `kind` is the ' +
'catalog’s own, so `search` takes it verbatim; the facts themselves are not listed, ' +
'and `audit_timeline` on an entity is what serves those. None of this says a ' +
'run is dead — nothing in the record speaks about a process — so an old idle run ' +
'may be abandoned or may be a session waiting; closing one is `mnema run end <id>`.';

Expand Down
70 changes: 65 additions & 5 deletions packages/code/src/presentation/runs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
*
* `focus` lists the open runs, `resume` names the last one and `status` says where the
* actor left off as one half of a wider answer, and all three need the same things said
* the same way: how long a run has been open, how long since it recorded anything, what
* the LAST one was, how many are still open, and what a run IS for the reader who has
* none. Three readings wording that separately is three wordings, and the second one to
* change would be the one nobody noticed.
* the same way: how long a run has been open, how long since it recorded anything, WHAT
* was written in it, what the LAST one was, how many are still open, and what a run IS
* for the reader who has none. Three readings wording that separately is three wordings,
* and the second one to change would be the one nobody noticed.
*/

import type { Resume } from '@mnema/copilot';
import type { WrittenInRun } from '@mnema/core';
import { oneLine } from '../one-line.js';
import { fact } from './detail.js';
import type { Line } from './line.js';
Expand Down Expand Up @@ -58,18 +59,27 @@ export const NO_RUNS_HINT: readonly Line[] = [
* the rule `wiring/on-one-line.ts` states and the reason the three halves are joined
* rather than nested: a fragment carrying its own punctuation, collapsed, loses the
* space it opens with, and `for a — g` becomes `for a— g`.
*
* WHAT WAS WRITTEN RIDES IT WHETHER THE RUN IS OPEN OR ENDED, which is the one part of
* this phrase that is NOT under the age's rule. An age beside an ended run would read
* as time still passing; a tally beside one reads as what that session did, and the
* reader of an ENDED run is exactly the person asking "where was I" — the question this
* phrase exists for. Asserted in `cli-e2e.test.ts` — "`resume` says what was written in
* the run, ended or open".
*/
export function lastRunPhrase(run: {
readonly id: string;
readonly open: boolean;
readonly goal?: string;
readonly ageSeconds?: number;
readonly idleSeconds?: number;
readonly wrote: readonly WrittenInRun[];
}): string {
return (
`last run ${run.id} (${run.open ? 'open' : 'ended'})` +
(run.goal === undefined ? '' : ` — ${oneLine(run.goal)}`) +
(run.open ? runAgeSuffix(run) : '')
(run.open ? runAgeSuffix(run) : '') +
wroteSuffix(run)
);
}

Expand Down Expand Up @@ -129,6 +139,56 @@ export function runAgeSuffix(run: {
return ` · ${age} · ${idle}`;
}

/**
* WHAT was written in a run — the tally per kind, commonest first, appended to the
* run's OWN line.
*
* The order and the entries are the projection's (`core`'s `run.ts`); this only words
* them. It is the answer to the complaint that put this clause here: every reading of a
* run reported the CONTAINER — an id, a goal, two durations — and none of them said
* what was put in it, so a session that recorded a decision and one that recorded
* nothing but a memory printed the same line.
*
* IT IS UNCUT, and that is the projection's ceiling kept rather than a second decision
* made here. The entries are over the event catalog, a closed union, so the clause is
* bounded by the number of kinds however long the session ran — which is why there is
* no `+N more` and no total beside it, the shape the four lists of `status` need
* because a record holds entities without limit.
*
* WHAT THAT BOUND COSTS, measured rather than assumed: the golden transcript pins one
* run that every write of the fixture was pinned to — 10 kinds, 30 facts — and its
* clause is 10 entries and about 210 characters (`cli.reads.golden.txt`, "the run that
* did something"). That is the worst shape a real record produces, and it wraps on a
* narrow terminal rather than truncating. The trade is deliberate: a cut here would be
* a second observable decision to document and to keep in step with the projection's
* order, in exchange for a line that is already the LONGEST one this file can word.
*
* "wrote nothing" IS SAID, and it is said even beside {@link runAgeSuffix}'s own
* "nothing recorded in it", which for an OPEN run is the same fact from the other axis.
* The two are left to agree rather than one being dropped: dropping this half when the
* other happens to be present would make the clause conditional on the run being open,
* which is the rule written in two places — and the case that needs it most is the
* ENDED run, where `runAgeSuffix` does not run at all and silence would be the only
* answer a reader got.
*
* THE SITE THIS CLAUSE DELIBERATELY DOES NOT REACH is `mnema usage`, which is the
* other reading in this package that lists runs (`presentation/usage.ts`). It was found
* by asking who reads `listRuns`, not by anybody's list, and it is left alone because
* it answers a different question: what a session COST, joined to the host's
* transcripts, in a fixed-width table whose closing statement says the numbers are not
* the record's. What a run wrote is the record's, and putting it in that table would
* mix the two halves the report exists to keep apart.
*
* Nothing here goes through `oneLine`: every value on this clause is the record's own
* — a kind is one of the catalog's literals and a count is a number — so there is no
* text an actor typed for a newline to hide in. That is the classification
* `tests/the-line-a-reading-words-is-one-line.test.ts` holds for this line.
*/
export function wroteSuffix(run: { readonly wrote: readonly WrittenInRun[] }): string {
if (run.wrote.length === 0) return ' · wrote nothing';
return ` · wrote ${run.wrote.map((w) => `${w.count} ${w.kind}`).join(', ')}`;
}

/**
* A duration in seconds as the two largest units that matter — `3d 4h`, `2h 14m`,
* `41m 3s`, `9s`. Two and not three: the third digit never changes a decision, and
Expand Down
13 changes: 11 additions & 2 deletions packages/code/src/wiring/focus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import type { Command } from 'commander';
import { asId, itemLine } from '../presentation/items.js';
import { NO_RUNS_HINT, runAgeSuffix } from '../presentation/runs.js';
import { NO_RUNS_HINT, runAgeSuffix, wroteSuffix } from '../presentation/runs.js';
import { here } from './context.js';
import { writeLines } from './io.js';
import { onOneLine } from './on-one-line.js';
Expand Down Expand Up @@ -58,6 +58,14 @@ export function registerFocus(program: Command, wiring: Wiring): Declared {
// stays the rule a reader counts by. `thisSession` is NOT printed: a read
// opens no run, so it is false in every line here, and a constant is noise
// rather than honesty (`--json` carries it, being the faithful object).
//
// And WHAT each one wrote, from the same module, because this reading needs
// it for the reason the durations were added: what tells two leftover runs
// apart is what happened inside them, and until this clause a session that
// recorded a decision and one that recorded nothing at all differed only by
// a number of seconds. The clause is `wroteSuffix`'s and not this file's —
// `resume` and `status` print the same words through `lastRunPhrase`, and a
// second wording here is the drift that module exists to prevent.
io.out(
render(
itemLine([
Expand All @@ -70,7 +78,8 @@ export function registerFocus(program: Command, wiring: Wiring): Declared {
// and a collapse would eat the space this one opens with.
onOneLine`${run.agent}` +
(run.goal !== undefined ? onOneLine` — ${run.goal}` : '') +
runAgeSuffix(run),
runAgeSuffix(run) +
wroteSuffix(run),
]),
),
);
Expand Down
Loading