Skip to content

🐛 Keep xmd repl usable after repeated component calls - #969

Merged
taras merged 1 commit into
mainfrom
agent/issue-941-repeated-component-calls
Oct 20, 2026
Merged

taras merged 1 commit into
mainfrom
agent/issue-941-repeated-component-calls

Conversation

@taras

@taras taras commented Oct 20, 2026

Copy link
Copy Markdown
Owner

Why

Fixes #941. One entry calls the same Markdown component once in each of two <Spawn> branches:

<All>
  <Spawn><Planner session="first" /></Spawn>
  <Spawn><Planner session="second" /></Spawn>
</All>

Both calls complete and save their replies, and then the REPL cannot read the entry. The projector looked up the owner of every effect inside Planner.md by path alone, found two admissions of that path, and refused the whole projection. Every later EntryOutput and Submit projects the same history first, so a finished entry made the REPL unusable.

What changes

Before: any effect whose source was admitted more than once in one entry refused with "names a source this entry admitted more than once", however the two calls were arranged.

After: a path-positioned effect belongs to the one admission of that path it could have come from — recorded earlier, on the effect's own coroutine or an ancestor of it. Two calls in two branches are two scopes (Planner-1, Planner-2), each holding its own turns, questions and bindings. A cold reopen reads the saved work without asking a provider or writing a record. The refusal stays for histories where that rule still leaves two candidates.

How it works

effect at path P → admissions of P → keep those recorded before it, on its coroutine or above → one owner | "not part of" | "more than once"

This is the rule generated fragments already used (generatedOwnerOf). Each admission now records the coroutine and journal position it was written at, which an import_component record has always carried, and both attributions narrow through one couldOwn.

What must stay true

  • Nothing is chosen by the latest or nearest admission — the same call repeated on one branch, and a call made inside another call, both still refuse "more than once" (RS2, and the M3 control that already existed).
  • Work a call is not above is not the call's — a sibling branch's effect, or the parent's own after both branches called, refuses "is not part of" (RS2).
  • Reading saved work performs nothing — the cold half of R1 reopens the execution with a provider that answers nothing and compares the bytes.

How to verify it

deno task test packages/cli/tests/repl-model.test.ts packages/cli/tests/repl-agent-execution.test.ts
  • RS1 and R1 prove each call's own work projects under it, from a doctored journal and through real execution with <All>/<Spawn> and two sessions, and fail against the previous ownerOf (checked by restoring it).
  • R1 also proves the next entry is admitted after the repeated-call entry settles, and that a cold reopen asks nothing and rewrites nothing.
  • RS2 proves the four refusals above still refuse, so the repair cannot attach work to a guessed call.

Scope

Included: the path attribution rule in packages/cli/src/repl/model.ts, the Advisor.md fixture, and the tests above. Nothing changes in what the runtime records, in agent-session identity, or in the generated-fragment rule beyond sharing its narrowing.

Intentionally unchanged: the same component called twice sequentially on one coroutine still refuses. No record bounds where the first call's work ends, and taking the later call would be choosing by recency.

Risks and limitations

  • Already-written histories need no migration: the records they hold carry the coroutine and order this reads, so the execution from the issue projects under this rule as saved.
  • Which of two concurrent calls becomes -1 is the order their admissions were journaled in, which concurrency does not fix. R1 therefore checks attribution by each record's branch rather than by that order.

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.

A path-positioned effect now belongs to the one admission of that path it
could have come from: recorded earlier, on its own coroutine or an ancestor
of it. This is the rule generated fragments already used; each admission
records where it was written, and both attributions narrow through one
`couldOwn`. One component called in two `<Spawn>` branches is two scopes
with their own turns, a finished entry stays readable, and the next entry is
admitted. The refusal stays where two candidates remain, so nothing is chosen
by the latest or nearest admission.
@github-actions

Copy link
Copy Markdown

PR #969: 🐛 Keep xmd repl usable after repeated component calls

4 files, +286 / -31

Scope

✅ PR scope looks good.

Structural

🟡 Interface declarations with no consumers: GeneratedOwner.
Symbol Declared at Refs in diff Why flagged
GeneratedOwner packages/cli/src/repl/model.ts:1364 1 referenced ≤1× within the added diff (pre-existing usages not counted)

Oxlint structural signals:

  • no-unnecessary-type-assertion ×2: packages/cli/src/repl/model.ts

Slop

✅ Slop indicators look low.

Static Analysis

Oxlint: 5 diagnostics across 1 file (4 rules)
Density: 0.017 violations/added-line

no-unnecessary-type-assertion (2): packages/cli/src/repl/model.ts
no-array-sort (1): packages/cli/src/repl/model.ts
no-unnecessary-type-conversion (1): packages/cli/src/repl/model.ts
no-unnecessary-template-expression (1): packages/cli/src/repl/model.ts

Correctness

No extraneous code patterns detected.

@taras
taras merged commit 6501e66 into main Oct 20, 2026
44 checks passed
@taras
taras deleted the agent/issue-941-repeated-component-calls branch October 20, 2026 17:48
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.

Keep xmd repl usable after repeated component calls

1 participant