Repository navigation
Resume REPL entries without repeating completed file operations - #902
Conversation
… 1/N) Start `xmd repl` inside a Git checkout and what an entry does to files there is retained: the `<File>` reads and writes, the `<File.Delete />` deletions and the directories `<Dir>` ensures. Leave an entry at a question, come back to the printed location, and the work it already did is not done again — the question is offered again, the binding a completed read produced is still the text it read, and the write after the question happens for the first time when you answer. A *new* entry reading that same path reads what is there now. Only live new work touches the environment. Answering a completed operation performs no path canonicalization, no ambient discovery, no lease acquisition, no permission lookup and no filesystem call, so a continuation works in a checkout somebody has since changed or removed. Nothing is inventoried, predicted or compared: the bounded comparison beside this change shows the cost unchanged between a checkout holding nothing else and one holding a thousand unrelated files. Rebased onto the landed U1–U5, which moved three things this feature used to carry itself. Core owns `<File>`, `<File.Delete />` and `<Dir>`, so recording is ordinary ancestor-max `Files` middleware over the one document filesystem and a generated fragment's admitted operations are retained exactly as an authored one's are — there is no separate captured-access door any more. `Execution.init` owns lifetime, so the recording is established inside the scope the invocation already owns, once per executing entry, and a passive view initializes nothing. Generated selection is the landed names-and-forms one, untouched here. Under the accepted C1 composition the host's own selection is one CLI-private Context whose default is the provider-neutral Git Plugin. Deno and the compiled binary bind a configured native variant before any command is dispatched; the shared command path reads the value and imports no adapter; Node and Bun leave the default and retain nothing. The native variant delegates the neutral Plugin's whole install and adds ordinary initialization for command `run`, while the selected REPL host registers the recorder for an entry. A command captures that inert value into its child-host settings before any document code runs, so a test root — which activates no bundled Plugin at all — still hands its `host="run"` child the configured one, and a document that replaces the Context afterwards does not get to choose its children's host. The repository installer arguments, `repl-repositories.ts` and the installation-array transform are gone. A record's name is the digest of the record version, the operation kind and the complete request as the document wrote it. Equal work in two entries is one name, which is allowed: each entry executes against its own stream segment, and that segment rather than a label inside the record is what decides whose history an entry may continue. The history admission that reads these records travels with the Git Plugin's existing contribution for the command. A shared Git directory still admits one live entry at a time through one kernel lock on a hashed sidecar outside every checkout, taken by the first new mutation and held until the entry is torn down. A completed read acquires nothing: an observation is not a new exclusive writer.
Addresses the Architect's R1 and the Planner's B1–B3 on `a0306e5a`. Both read-lease probes reproduced before anything changed: the Architect's live read came back with the contention sentence, and the Planner's observed one hold where there should be none. **R1/B1.** `routed()` took the checkout's lease for every new in-checkout operation, reads included, so a read refused while another entry was working and made anybody else wait while it was only looking. Being retained and being exclusive are different questions: what the lease answers is that one entry at a time *changes* a repository. Only a new write, deletion or directory ensure acquires it now. A read is still routed, still recorded, still permission- checked, and a completed one still answers before any of that. The regression lives in the lifetime suite beside the other resource rows: one entry holds the lease at a question, a second entry reads the same file, and the read succeeds, is retained, and opens no descriptor — observed at the native function the advisory lock opens with. Restoring the unconditional hold fails it. **B2.** The configured Plugin copies the helper assembly at construction — scalars and the nested launcher environment — and initializes executions from that copy. The caller's object is neither mutated nor frozen; what an execution gets is what this host selected, not whatever the caller's reference says later. **B3.** The benchmark measures what it said it measured. Ordinary mode now initializes the production ordinary composition inside its own timed region, so both live columns are initialized executions. The lease column counts lock descriptors actually opened, at `Deno.openSync`, with the persistent sidecar left exactly where it is — the old column inferred acquisition from a file it had deleted first. Appends are counted apart from retained records, which is how the continuation column can say it appends the engine's closing event and no local result. The `globs` and `checks` columns say what they are, and the report states plainly that this harness does not observe `node:fs` and so cannot claim comprehensive traversal coverage. Its first numbers stand on their own; the R6 report is preserved unchanged. The REPL specification no longer says reads are ordinary throughout — a read inside the checkout is retained, and searches, temporary directories and outside-checkout reads keep their ordinary scope — and it describes exclusion as applying to a new mutation rather than to every entry that needs a checkout. The architecture says the same, and the exact contention sentence is unchanged.
Four statements this change had left untrue, found while preparing the publication of #902 and fixed rather than deferred. Section 9.6 described a repository installer as a `runXmd` parameter, with the argument in the sample and a paragraph about what Node and Bun pass instead. Under the accepted C1 composition there is no such parameter: the bundled Plugin is one CLI-private Context whose default is the provider-neutral value, a runtime-named entrypoint binds a configured native variant before any command is dispatched, and the enumeration of how the commands differ now says what each one initializes — a test root capturing the selected value for its `host="run"` child, and `xmd repl` initializing none of it because an entry's recording is registered by the selected REPL host. `architecture.md` listed that same parameter beside the service installer and the upgrade assembly in the sentence about where standard input comes from. Two runtime exclusions described the withdrawn design: one named a record owner this feature no longer has, and one named an entry-host factory the entrypoint no longer supplies. `repl-profile.ts` goes back to the base. Its interface renaming and its third paragraph were for an input the amendment withdrew, so the file carried prose about a member it does not declare. No behavior changes. Prose, one revert, and the enumerations they belong to.
2b47512 to
ecd0863
Compare
| // caller that kept a reference and changed a field afterwards would be | ||
| // configuring every later execution, including a child's, from outside the | ||
| // selection the command already made. The caller's own object is neither | ||
| // mutated nor frozen — this is a copy, and the copy is what is used. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // mutated nor frozen — this is a copy, and the copy is what is used. |
| // The neutral value's own install, whole: this variant adds to the | ||
| // bundled Plugin rather than reimplementing it, so the vocabulary, the | ||
| // commands it declares for and the admissions it contributes are the ones | ||
| // that package decides. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // that package decides. |
| spawnChild(command, args, { stdio: ["ignore", "pipe", "pipe"] }), | ||
| ); | ||
| // The child's own closing fact, not a second pair of listeners watching for | ||
| // it: one closing per child, removed by the one cleanup that owns it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // it: one closing per child, removed by the one cleanup that owns it. |
| if (!pane.ok) { | ||
| // An unreadable screen used to read as an empty one, which this file then | ||
| // reported as the REPL never drawing. A pane that cannot be captured is a | ||
| // pty that has gone, and that is the fact worth printing. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // pty that has gone, and that is the fact worth printing. |
| yield* removed(join(checkout, "drafts", "obsolete.md")); | ||
| // And the write after the question has not happened: by now the prefix is | ||
| // through its last authored operation, so this one is absent because the | ||
| // entry is waiting rather than because it has not got there yet. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // entry is waiting rather than because it has not got there yet. |
| // is answered rather than on the surface behind it. | ||
| yield* showing(first, "waiting for an answer", ACT_MS); | ||
|
|
||
| // Left through [exit], with the question still open. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Left through [exit], with the question still open. |
| // offers is the answer: a keystroke in this drawer reaches the field a | ||
| // *pointer* last selected rather than the line holding focus, so an enum is | ||
| // what a pointer alone can settle. Reported as a non-blocking observation; | ||
| // it is not this feature's behavior. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // it is not this feature's behavior. |
| // each execution's journal read, and this is the trusted path that | ||
| // already declares for the command. It holds no native state and names | ||
| // no runtime: a Node session that retained nothing has nothing here to | ||
| // refuse. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // refuse. |
PR #902: ✨ Continue a REPL entry without redoing the files it already wrote (#181, 1/N)53 files, +6749 / -263 Scope🔴 PR has 7012 lines changed. Split into focused PRs. 🟡 7012 lines changed. PRs under 400 receive more thorough review. 🟡 53 files changed. Are all changes related? 🟡 Changes span 11 directories. 🟡 PR mixes config and source changes. 🟡 New abstraction files: packages/git/src/deno/retained-repository/provider.ts. Verify 3+ consumers. Structural🟡 13 console statements. Oxlint structural signals:
Slop
Oxlint slop signals:
Static AnalysisOxlint: 53 diagnostics across 9 files (12 rules) no-console (21): packages/cli/src/cli.ts, scripts/bench-repl-file-dir.ts CorrectnessNo extraneous code patterns detected. |
Two CI failures, each one a gate the local Deno matrix does not have. **The Node typecheck.** `pnpm exec tsc --project tsconfig.node.json` is the only place `lib: ES2022` is enforced, and it reported five errors: the lifetime proof names `Deno.ChildProcess` and `Deno.Command`, and the local proof's concurrent row called `toSorted`, which is ES2023 and compiles under Deno alone. The lifetime proof joins the three tests already excluded from that project for reaching `Deno` directly — its subject is a kernel-released advisory lock and a second process taking the same sidecar — and the sort is copied before sorting, the way `module-partition.test.ts` already does it. **Bun.** The C1 fixture was a row inside the shared `testing-execution-host.test.ts`, which runs everywhere, and it imports the configured native Plugin — `@executablemd/git/deno`, whose graph reaches `node:sqlite`. Bun has no such module, so locally the whole file fails to load and on CI the child's repository work failed instead. Reproduced both ways before moving anything. The proof is now its own file, excluded for Node and Bun beside the other Deno-only repository subjects, and the portable half — that a test root hands its `host="run"` child the value the host selected, reinstalled in the child's own scope after its working directory — stays where it was and still runs on all three runtimes. No production code changes. Verified: the Node typecheck gate exit 0; the trimmed shared suite 21 passed under Bun, 21 under Node and 21 under Deno; the moved proof 1 passed under Deno; runtime exclusions and the manifest rows pass; lint and check exit 0.
| // here, in the runtime scope the selected REPL host owns, because only a | ||
| // runtime-named module may name the native adapter — and registering it | ||
| // acquires nothing, so a session that only reads history discovers no | ||
| // checkout and takes no lease. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // checkout and takes no lease. |
| // here, in the runtime scope the selected REPL host owns, because only a | ||
| // runtime-named module may name the native adapter — and registering it | ||
| // acquires nothing, so a session that only reads history discovers no | ||
| // checkout and takes no lease. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // checkout and takes no lease. |
| // The bundled Plugin this host runs with: the configured native variant, | ||
| // selected before any command is dispatched. The shared command path reads | ||
| // the value and imports no adapter, and an ordinary execution installs it | ||
| // in its own scope. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // in its own scope. |
| // The bundled Plugin this host runs with: the configured native variant, | ||
| // selected before any command is dispatched. The shared command path reads | ||
| // the value and imports no adapter, and an ordinary execution installs it | ||
| // in its own scope. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // in its own scope. |
| yield* removed(join(checkout, "drafts", "obsolete.md")); | ||
| // And the write after the question has not happened: by now the prefix is | ||
| // through its last authored operation, so this one is absent because the | ||
| // entry is waiting rather than because it has not got there yet. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // entry is waiting rather than because it has not got there yet. |
| yield* removed(join(checkout, "drafts", "obsolete.md")); | ||
| // And the write after the question has not happened: by now the prefix is | ||
| // through its last authored operation, so this one is absent because the | ||
| // entry is waiting rather than because it has not got there yet. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // entry is waiting rather than because it has not got there yet. |
Why
Continuing a REPL entry currently repeats its file work before returning to an unanswered question. That can overwrite edits made while the entry was paused, delete files restored by hand, and change the text bound by an earlier read.
This is the first increment of #181: retain an entry's completed file operations inside the Git checkout where it started.
What changes
Before: an entry writes
note.md, deletesobsolete.md, then waits at a question. Reopening its saved location repeats the write and deletion.After: completed operations return their recorded results. If you edited
note.mdor restoredobsolete.mdbetween runs, those changes survive. The question is offered again, and work after it executes when you answer.In Deno and compiled
xmd repl, this covers<File>text reads and writes,<File.Delete />, and the directories<Dir>ensures inside the starting checkout. A completed read binds the text it originally returned; a new entry reads the current file. Admitted generated operations use the same recording route and retain their existing permission ceiling.A new write, deletion or directory ensure takes an exclusive checkout lease until the entry ends. A competing mutation is refused immediately. Reads take no lease and can proceed while another entry holds one.
How it works
Replay checks the existing journal position before live path canonicalization, checkout discovery, permissions or filesystem work. Routing is remembered per durable coroutine, preserving nested and concurrent execution; each entry owns its journal segment. Ordinary middleware answers, transformations and refusals are recorded as the composed operation's result.
Deno and compiled entrypoints select a configured Git Plugin through the CLI-private
BundledRunPluginContext. Ordinary runs initialize their repository provider throughExecution.init; the REPL host registers entry recording. Isolated run children receive the selected Plugin captured before document code executes and initialize it after their own working directory is established. Helper configuration is copied when the Plugin is constructed.Review guide
Start with:
packages/git/src/deno/retained-repository/files.ts.Then review:
retained-repository/{records,admission}.tsfor record validation and history admission.deno/retained-repository/{local,provider,leases}.tsfor journal ordering, coroutine routing, failure handling and resource ownership.packages/cli/src/{run-profile,deno-repositories,cli,testing-host}.tsfor host selection and isolated children.What must stay true
How to verify it
testing-execution-host-native.test.ts; portable child-host tests remain in the shared suite.Representative commands:
The bounded benchmark compares initialized executions over equal fixtures. At 0 / 1,000 unrelated files, medians were ordinary 69.4 / 63.4 ms, entry 144.8 / 133.7 ms, and completed continuation 47.4 / 44.1 ms. Continuation observed no document reads, writes, deletes, ensures, Git calls or lock opens; it appended one engine closing event and no local result. These samples establish no performance budget or comprehensive traversal coverage.
Architect and Planner accepted
6d82e41b. Subsequent commits align documentation and isolate native test dependencies, with reported Node typecheck and focused Deno/Node/Bun checks passing. Those feedback reviews do not replace Product Owner review, delivery verification or merge approval.Scope
Included: entry-owned File/Dir recording in an existing checkout, native mutation leases, host/child wiring, specifications and public source/compiled journeys.
Ordinary runs, workflows, Node/Bun REPL behavior and operations outside the checkout keep their existing behavior. Searches, path checks and temporary directories remain ordinary. Managed Repository/Worktree selection, local Git operations and external workers belong to later increments.
New abstractions
BundledRunPlugincarries the host-selected Plugin without importing native adapters into shared command code.useCommonDirectoryLeaseowns an entry's mutation lease through the existing advisory lock.Generated or mechanical changes
Obsolete repository-installer forwarding is removed from CLI entrypoints. Shared PTY support drives source and compiled journeys. Runtime exclusions and Node typecheck configuration keep native-only proofs on their supported host.
Risks and limitations
The REPL does not inventory, validate or restore checkout state. Unacknowledged work may run again; new operations depend on the live environment. Reopening from a different working directory currently fails closed with a journal divergence diagnostic. The lease excludes participating XMD entries and processes, not editors or arbitrary Git tools.
Scope confirmation