Skip to content

Limit how often a node runs with maxRuns instead of edge limits - #128

Merged
Epsirom merged 1 commit into
mainfrom
chrok/blissful-hopper-09c273
Oct 25, 2026
Merged

Epsirom merged 1 commit into
mainfrom
chrok/blissful-hopper-09c273

Conversation

@Epsirom

@Epsirom Epsirom commented Oct 25, 2026

Copy link
Copy Markdown
Owner

Change

Part of RSI Mode (#118). Replaces the per-edge limit added in #124 with an optional maxRuns on every node type. It lands before #127 (arbitrary cycles), which no longer needs to prefer limited edges when it picks closing edges, and before 0.6, so edge limits were never released.

People think "review at most 3 times", not "this edge fires n-1 times", and with arbitrary cycles it is hard to tell which edge closes a cycle or to bound overlapping cycles at a shared node with one edge's limit. A node limit has none of those problems.

Behavior

  • maxRuns counts the executions of a node's lineage that start; skipped ones do not count. Unlimited when omitted or Infinity. maxRuns: 3 on a node in a cycle allows three rounds.
  • When an edge reaching a settled head would rerun it past maxRuns, the clone is still created but stays pending with the new wait reason limit. Once its inputs are current, core emits limit_reached (limit: "maxRuns", the waiting executionId) and lists it in the new BraidSnapshot.limitedExecutionIds and graph_waiting.limitedExecutionIds, so a run with nothing else active waits and Pi/DSH wake the owner with the waiting reminder.
  • Raising maxRuns with a normal update/upsertNodes edits the pending rerun in place and it starts.
  • Releasing it with resume (or resume in an update) without raising ends the rerun: it is removed (execution_removed), the node keeps its last result, and nothing downstream reruns for it. In a cycle this mirrors the old "release without raising ends the loop": the decision chose the closing edge, so its exits stay inactive. If the same update raises maxRuns, the rerun starts instead.
  • An owner's replacement of a started node is never blocked; the limit only caps automatic reruns (cycle rounds and downstream cascades).
  • Under onLimit: "fail" the run fails with the new code RUN_LIMIT.

Removed

Edge.limit, the rule that a limit must sit on a cycle-closing edge, the edge_limit hold source, limit_reached.edge, and EDGE_LIMIT. Cycle validation is otherwise unchanged. removeEdges now accepts only from, to, and choice.

Pi and DSH

  • Node schemas accept maxRuns; edge schemas drop limit. Tool guidance, onLimit text, and braid_update/braid_resume descriptions explain raising or releasing a waiting rerun.
  • The waiting reminder lists a rerun at its limit as a WaitingItem of kind limited (source maxRuns) and says how to raise or release it. formatEdgeLimit and edgeLimit on completion notices are removed. Pi's OutstandingJob and the single-shot drain warning include limited reruns.
  • Status text shows maxRuns= on nodes, the limit wait reason, and reruns at their limit. The Pi live view restores the previous head when a rerun is released. The DSH panel drops edge limits and shows a node's maxRuns.
  • /braid:review bounds its rounds with maxRuns: 3 on its writer node instead of an edge limit and still fails at the cap.

Docs

Execution control (cycles, wait reasons, graph_waiting, Limits), README graph schema and options, resource limits, the 0.6 migration in docs/compatibility.md (maxIterations n becomes maxRuns n on a node in the cycle), ROADMAP scope table, and both adapter READMEs.

Compatibility

Not breaking against released versions: edge limits were never released. Callers of 0.5 still migrate maxIterations as described in the 0.6 guide, now to maxRuns.

Validation

npm run verify passes (core 303, Pi 194, DSH 68, package smoke). Edge-limit tests are replaced by maxRuns tests: raising in place and releasing to end a cycle, RUN_LIMIT, downstream reruns versus the owner's replacement, skipped runs not counting, releasing while an input still reruns, removing a node and releasing its rerun in one update, and validation. Pi and DSH tests cover the waiting reminder, schemas, /braid:review, and the live-view restore.

  • Relevant tests and type checks pass.
  • User-facing docs are updated where needed.
  • The title reads as a release-note line and the category label is correct.
  • Breaking changes, if any, include migration guidance in the docs.

Replace Edge.limit with an optional maxRuns on every node type. A rerun past
it is still cloned but waits pending (wait reason "limit", limit_reached,
limitedExecutionIds) until the owner raises maxRuns, which edits it in place,
or releases it, which removes it and ends the rerun. The owner's replacement
is never blocked; onLimit "fail" fails with RUN_LIMIT. Pi and DSH schemas,
reminders, status text, and docs follow.
@Epsirom Epsirom added the enhancement New feature or request label Oct 25, 2026
@Epsirom
Epsirom merged commit 92acbb9 into main Oct 25, 2026
18 checks passed
@Epsirom
Epsirom deleted the chrok/blissful-hopper-09c273 branch October 25, 2026 14:33
Epsirom added a commit that referenced this pull request Oct 25, 2026
Follow-up to #128. Part of RSI Mode (#118).

`maxRuns` is the most runs a node's lineage can reach, and it never
resets, including per round of an enclosing cycle (decided in #127). The
owner adjusts it by updating the node. Before this change, that worked
only while a rerun was waiting at the limit. On a running or settled
head, any upsert replaced it: raising `maxRuns` ahead of time cancelled
a running execution and reran it, and reran a settled one.

## What changes

- An upsert whose definition differs from the current one **only in
`maxRuns`** keeps a started head. A running head keeps running, and a
settled one keeps its result and any hold. Later reruns (clones) use the
new limit, so the limit can be raised or lowered at any time.
- Any other change, including an unchanged upsert, still replaces a
started head as before. A pending head (including a rerun waiting at its
limit) is still edited in place.
- The started head's own `node` keeps the definition it ran with;
`snapshot().graph` shows the new `maxRuns`.
- Docs: `docs/execution-control.md` (live updates, limits),
`GraphUpdate.upsertNodes` doc, `docs/compatibility.md`, and the Pi/DSH
READMEs and tool descriptions (`maxRuns`, `braid_update`).

## Tests

`test/limits.test.ts` covers:
- raising `maxRuns` while the head runs: no cancellation, and later
reruns stop at the new limit
- changing only `maxRuns` on a settled, held head: no replacement, and
the hold is kept
- any other change still replaces the head

`npm run verify` passes (core 305, Pi 194, DSH 68, package smoke).

## Overlap with #127

#127 edits the cycle paragraphs of the same READMEs and docs. This PR
puts its sentences elsewhere to avoid conflicts, but whichever merges
second may need a trivial rebase.
Epsirom added a commit that referenced this pull request Oct 25, 2026
… new cycle (#127)

Closes #114. Part of RSI Mode (#118). Builds on #128, where `maxRuns`
replaced edge limits.

## What changes

- **Validation accepts any cycle.** Unconditional, nested, and
overlapping cycles, cycles with several entries or exits from any node,
and decisions without an exit choice are no longer rejected.
- **Closing edges only decide where each round starts.** Braid picks
them per strongly connected part: it keeps the other edges as ordinary
dependencies, and an edge that would complete a cycle closes it instead.
Preference for closing: edges into an entry (a node an outside edge
enters), then a decision's choice edges, then edges back to nodes listed
earlier. Graphs accepted before keep their closing edge, except a rare
cycle without an entry where a decision inside it has a choice edge back
to an earlier-listed node; its round start can move. That is acceptable
because the cycle rules are unreleased since 0.5;
`docs/compatibility.md` notes it. `closingEdges(graph)` returns them
all.
- **Exits per round.** Edges that leave a cycle wait while any of its
nodes reruns. For a cycle closed by a decision's choice edge they still
wait for the final round. For an unconditional closing edge they fire in
every round: downstream nodes outside the cycle run with each finished
round (inactive ones wait for a later round). Round clones are created
at the end of the scheduling pass, so exits are admitted before the next
round takes the slots; #128's `maxRuns` check still applies when a clone
is admitted.
- **Overlapping cycles** that reach a shared node together merge: the
first closing edge clones it, the second attaches to the clone as
another input.
- **Bounds.** Cycles are bounded by `maxRuns` on their nodes (#128):
total runs, never reset per round of an enclosing cycle (tested and
documented).
- **Cycle notices.** Each submission or update that creates cycles emits
`cycles_created` with each new cycle (`GraphCycle`: `nodeIds` starting
where its rounds start, `edges` ending with its closing edge, `stops` =
the decision choice edges on it, `limitedNodeIds` = its nodes with a
finite `maxRuns`; at most 20, with `truncated`). `newCycles(graph,
previous?)` computes the same list. Pi and DSH `braid` / `braid_update`
results show it, and flag a cycle with nothing that stops or bounds it,
so an accidental `test → fix` next to `fix → test` is noticed.
- Pi and DSH tool descriptions, guidance, and READMEs describe the new
rules. Pi's `display.ts` gets only the event-text case the exhaustive
switch requires.

## Docs

`docs/execution-control.md` (Cycles, Cycle notices), README graph schema
and out-of-scope list, ROADMAP (#114 checked, scope row), and a 0.6
migration subsection in `docs/compatibility.md`.

## Compatibility

Not labelled breaking: existing callers need no changes. TypeScript code
with an exhaustive `switch` over `ExecutionEvent` needs a case for
`cycles_created`.

## Decided: `maxRuns` never resets

`maxRuns` is the most runs a lineage can reach, including in a nested
cycle across rounds of the enclosing one; it does not reset per round.
The owner raises or lowers it by updating the node; since #129, an
upsert that only changes `maxRuns` keeps a started head instead of
replacing it.

## Tests

New `test/cycles.test.ts` covers:
- an unconditional cycle with an exit that runs each round, ended at
`maxRuns`
- a nested cycle, and `maxRuns` counting across outer rounds
- overlapping cycles merging at a shared node
- cycle reports for submissions and updates, and the report cap

Validation and closing-edge tests are updated, and Pi and DSH tests
cover the notices. `npm run verify` passes on `main` with #128 and #129
(core 309, Pi 195, DSH 69, package smoke).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant