mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
2641 lines
228 KiB
HTML
2641 lines
228 KiB
HTML
<!doctype html>
|
||
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Parked Until Resolved: one model for dependencies in Chord v2</title>
|
||
<style>
|
||
|
||
:root{--paper:#fbf8f2;--ink:#2a2723;--muted:#7b7468;--rule:#e3dbcc;--accent:#b4472f;--blue:#2e6a8a;--sand:#c2b18c;--soft:#f4eee2}
|
||
*{box-sizing:border-box}
|
||
html{background:#efe9dd}
|
||
body{margin:0;color:var(--ink);font:19px/1.68 'Iowan Old Style','Palatino Linotype',Palatino,'Book Antiqua',Georgia,serif;
|
||
-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
|
||
.book{max-width:860px;margin:0 auto;background:var(--paper);box-shadow:0 0 0 1px #e1d9c8,0 30px 80px -40px rgba(60,40,10,.35);padding:0 0 4rem}
|
||
.inner{padding:0 90px}
|
||
h1,h2,h3{font-weight:400;line-height:1.15}
|
||
h2{font-size:2.1rem;margin:.2em 0 .3em;letter-spacing:-.01em}
|
||
h3{font-size:1.05rem;text-transform:uppercase;letter-spacing:.14em;margin:2.4em 0 .6em;color:var(--ink);font-weight:600;
|
||
font-family:'Avenir Next','Helvetica Neue',sans-serif}
|
||
p{margin:0 0 1.05em;hyphens:auto;text-align:left}
|
||
code{font:.8em/1 'SF Mono',Menlo,Consolas,monospace;background:#f1ebde;padding:.1em .35em;border-radius:3px}
|
||
pre{font:.74rem/1.55 'SF Mono',Menlo,Consolas,monospace;background:#f4eee2;border-left:3px solid var(--sand);padding:1rem 1.2rem;margin:1.4rem 0 1.8rem;
|
||
overflow-x:auto;border-radius:0 4px 4px 0;white-space:pre;tab-size:2}
|
||
pre code{background:none;padding:0;font-size:1em}
|
||
em{font-style:italic}
|
||
a{color:var(--blue)}
|
||
.cover{min-height:92vh;display:flex;flex-direction:column;justify-content:center;padding:90px;background:
|
||
radial-gradient(circle at 85% 15%,rgba(180,71,47,.13),transparent 40%),radial-gradient(circle at 10% 90%,rgba(46,106,138,.12),transparent 45%),var(--paper);
|
||
border-bottom:1px solid var(--rule)}
|
||
.kicker{font:600 .78rem 'Avenir Next','Helvetica Neue',sans-serif;letter-spacing:.22em;text-transform:uppercase;color:var(--accent)}
|
||
.cover h1{font-size:5.2rem;line-height:.98;margin:.35em 0 .25em;letter-spacing:-.025em}
|
||
.subtitle{font-size:1.45rem;font-style:italic;color:#4a453e;max-width:30em;text-align:left}
|
||
.cover-stats,.stats{display:flex;flex-wrap:wrap;gap:1.2rem 2.4rem;margin:2.2rem 0}
|
||
.stats{margin:1.6rem 0 1.8rem;padding:1.1rem 0;border-top:1px solid var(--rule);border-bottom:1px solid var(--rule)}
|
||
.stat b{display:block;font-size:2.1rem;font-weight:400;line-height:1.1;font-variant-numeric:lining-nums}
|
||
.stat span{font:.78rem/1.3 'Avenir Next','Helvetica Neue',sans-serif;color:var(--muted);letter-spacing:.03em;display:block;max-width:14em}
|
||
.cover-note{font-size:.9rem;color:var(--muted);max-width:34em;text-align:left;margin-top:2rem}
|
||
.toc{padding:4rem 0 2rem}
|
||
.toc ol{list-style:none;padding:0;margin:1rem 0;columns:2;column-gap:3rem}
|
||
.toc li{padding:.3rem 0;border-bottom:1px dotted var(--rule);break-inside:avoid}
|
||
.toc a{color:var(--ink);text-decoration:none}
|
||
.toc a:hover{color:var(--accent)}
|
||
.preface{padding-top:2rem}
|
||
.findings{padding-left:1.4em}
|
||
.findings li{margin:.55em 0;padding-left:.3em}
|
||
.findings li::marker{color:var(--accent);font-weight:600}
|
||
.chapter{padding-top:4.5rem;margin-top:3rem;border-top:1px solid var(--rule)}
|
||
.opener{text-align:center;margin-bottom:2.6rem}
|
||
.numeral{font-size:4.2rem;color:var(--accent);line-height:1;font-variant:small-caps;letter-spacing:.05em}
|
||
.opener h2{font-size:2.5rem;margin:.25em 0}
|
||
.lede{font-style:italic;color:#5a544a;font-size:1.12rem;max-width:30em;margin:0 auto;text-align:center}
|
||
.dropcap::first-letter{float:left;font-size:4.1em;line-height:.82;padding:.06em .09em 0 0;color:var(--accent)}
|
||
.fig{margin:2rem 0 2.2rem}
|
||
.fig.wide{margin-left:-50px;margin-right:-50px}
|
||
.fig svg{display:block;margin:0 auto}
|
||
figcaption{font:.8rem/1.45 'Avenir Next','Helvetica Neue',sans-serif;color:var(--muted);margin-top:.7rem;padding-left:.8rem;border-left:2px solid var(--rule)}
|
||
.fignum{color:var(--ink);font-weight:600}
|
||
.note{font:.86rem/1.55 'Avenir Next','Helvetica Neue',sans-serif;color:#5b5549;background:#f4eee2;border-left:3px solid var(--sand);
|
||
padding:.9rem 1.1rem;margin:1.8rem 0;border-radius:0 4px 4px 0}
|
||
.note.review{border-left-color:var(--accent)}
|
||
.note b.tag{font-weight:600;letter-spacing:.12em;text-transform:uppercase;font-size:.72rem;color:var(--accent);display:block;margin-bottom:.3rem}
|
||
.note code{font-size:.85em}
|
||
table{width:100%;border-collapse:collapse;margin:1.4rem 0 1.8rem;font:.8rem/1.4 'Avenir Next','Helvetica Neue',sans-serif}
|
||
th{text-align:left;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;font-size:.68rem;border-bottom:1.5px solid var(--ink);padding:.45rem .5rem}
|
||
td{padding:.42rem .5rem;border-bottom:1px solid var(--rule);vertical-align:top}
|
||
td code{font-size:.92em}
|
||
table.num td:not(:first-child),table.num th:not(:first-child){text-align:right}
|
||
.epilogue p{font-size:1.04em}
|
||
.appendix{font-size:.92em}
|
||
.appendix ul,.chapter ul{padding-left:1.2em}
|
||
.appendix li,.chapter li{margin:.4em 0}
|
||
.colophon{text-align:center;font:.78rem 'Avenir Next','Helvetica Neue',sans-serif;color:var(--muted);margin-top:4rem;letter-spacing:.05em}
|
||
@media (max-width:760px){.inner{padding:0 22px}.cover{padding:40px 22px}.cover h1{font-size:3.2rem}.fig.wide{margin-left:0;margin-right:0}
|
||
.toc ol{columns:1}p{text-align:left}body{font-size:17px}}
|
||
@media print{html{background:#fff}.book{box-shadow:none}.chapter{break-before:page}.fig{break-inside:avoid}}
|
||
.transcript p b{font:600 .78rem 'Avenir Next','Helvetica Neue',sans-serif;letter-spacing:.08em;text-transform:uppercase;color:var(--accent);margin-right:.3em}
|
||
pre .tk-c{color:#8d8576;font-style:italic}pre .tk-s{color:#5f7a3d}pre .tk-n{color:#a0662a}pre .tk-k{color:#b4472f}pre .tk-t{color:#2e6a8a}pre .tk-f{color:#7a3f6a}
|
||
h4{font:600 .82rem 'Avenir Next','Helvetica Neue',sans-serif;letter-spacing:.06em;margin:1.8em 0 .5em;color:var(--muted)}
|
||
.inner ul,.inner ol:not(.findings){padding-left:1.2em}
|
||
.inner li{margin:.35em 0}
|
||
blockquote{margin:1.4rem 0;padding:.2rem 0 .2rem 1.2rem;border-left:3px solid var(--rule);color:#5a544a;font-style:italic}
|
||
.fig svg text{dominant-baseline:auto}
|
||
.fig .replay{font:600 .7rem 'Avenir Next','Helvetica Neue',sans-serif;letter-spacing:.1em;text-transform:uppercase;color:var(--accent);
|
||
background:none;border:1px solid var(--rule);border-radius:3px;padding:.2rem .55rem;margin:0 0 0 1rem;cursor:pointer;float:right}
|
||
.fig .replay:hover{border-color:var(--accent)}
|
||
.anim.armed .step{opacity:0}
|
||
.anim.armed.play .step{animation:stepIn .45s ease-out both;animation-delay:calc(var(--i) * var(--pace, .45s))}
|
||
.anim.armed.play .step.edge{animation-name:edgeIn}
|
||
@keyframes stepIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
|
||
@keyframes edgeIn{from{opacity:0}to{opacity:1}}
|
||
.steps{margin:2rem 0 2.2rem;border:1px solid var(--rule);border-radius:6px;background:#fffdf8}
|
||
.steps-head{display:flex;align-items:center;gap:.8rem;padding:.6rem 1rem;border-bottom:1px solid var(--rule);
|
||
font:600 .72rem 'Avenir Next','Helvetica Neue',sans-serif;letter-spacing:.12em;text-transform:uppercase;color:var(--accent)}
|
||
.steps-head .title{flex:1}
|
||
.steps-head .count{color:var(--muted);letter-spacing:.06em}
|
||
.steps-head button{font:600 .72rem 'Avenir Next','Helvetica Neue',sans-serif;color:var(--ink);background:var(--soft);border:1px solid var(--rule);
|
||
border-radius:3px;padding:.25rem .65rem;cursor:pointer}
|
||
.steps-head button:disabled{opacity:.35;cursor:default}
|
||
.step-panel{padding:1rem 1.2rem .4rem}
|
||
.step-panel>h4:first-child{margin-top:.2rem;color:var(--ink)}
|
||
.steps.js .step-panel{display:none}
|
||
.steps.js .step-panel.on{display:block}
|
||
.steps:not(.js) .steps-head button,.steps:not(.js) .steps-head .count{display:none}
|
||
.steps:not(.js) .step-panel+.step-panel{border-top:1px dashed var(--rule)}
|
||
.fig figcaption{display:flow-root}
|
||
.fig svg{max-width:100%;height:auto}
|
||
table{max-width:100%}
|
||
@media (max-width:760px){.inner{padding:0 20px}.cover{padding:48px 22px}.cover h1{font-size:3.2rem}.fig.wide{margin-left:0;margin-right:0}table{display:block;overflow-x:auto}.toc ol{columns:1}}
|
||
</style>
|
||
|
||
|
||
</head>
|
||
<body><div class="book">
|
||
<section class="cover">
|
||
<div class="kicker">A design proposal · Chord v2 · slice 7 · 2026-10-29</div>
|
||
<h1>Parked<br>Until Resolved</h1>
|
||
<p class="subtitle">What happens when what an object needs is not there yet, is somewhere else, has gone away or does not fit, told through the scenarios that decide it</p>
|
||
<div class="cover-stats"><div class="stat"><b>5 → 1</b><span>answers today to "the provider is not there"</span></div><div class="stat"><b>0</b><span>deadlines on waiting for a dependency</span></div><div class="stat"><b>300+</b><span>scenarios played through by four reviewers</span></div><div class="stat"><b>6</b><span>decisions left for the owner</span></div></div>
|
||
<div class="cover-note"><p>A proposal, not a decision. It states the dependency model the redesign session drafted on the night of
|
||
2026-10-28/29 from the owner's direction ("park until resolved"; "no difference between local and remote
|
||
resolution: a link going is like a plugin unloading") and five rounds of review by Astra, Fable, Opus and Sol, who
|
||
played more than 300 scenarios from p2's plugins and their own against it. The working document is
|
||
<code>/tmp/model/model.md</code>; the reviews are in <code>/tmp/dep-graph-*.md</code>, <code>/tmp/barrier-*.md</code> and <code>/tmp/model/review*-*.md</code>.
|
||
Code references are to pi <code>main</code> at <code>358f7d46a</code> (<code>packages/chord/src/next</code>). Notes marked <em>review</em> record a point
|
||
the reviewers found or still disagree on; <em>Owner</em> marks a decision that is his.</p>
|
||
</div>
|
||
</section><div class="inner">
|
||
<nav class="toc"><h2>Contents</h2><ol>
|
||
<li><a href="#before-we-begin">Before we begin</a></li>
|
||
<li><a href="#today">I. Five Answers to One Question</a></li>
|
||
<li><a href="#requests">II. Requests That Wait</a></li>
|
||
<li><a href="#states">III. Loading or Waiting</a></li>
|
||
<li><a href="#parked">IV. Parked</a></li>
|
||
<li><a href="#wakes">V. What Wakes Whom</a></li>
|
||
<li><a href="#startup">VI. Starting Up</a></li>
|
||
<li><a href="#reload">VII. Plugins Coming and Going</a></li>
|
||
<li><a href="#cycles">VIII. Finding Cycles</a></li>
|
||
<li><a href="#callers">IX. Callers</a></li>
|
||
<li><a href="#registries">X. Registries</a></li>
|
||
<li><a href="#housekeeping">XI. Keep-alive, Presence, Revocation</a></li>
|
||
<li><a href="#clock">XII. The Clock</a></li>
|
||
<li><a href="#morning">XIII. A Morning, Step by Step</a></li>
|
||
<li><a href="#sc-startup">XIV. Scenarios: Starting Up</a></li>
|
||
<li><a href="#sc-refs">XV. Scenarios: References, Holds and Calls</a></li>
|
||
<li><a href="#sc-providers">XVI. Scenarios: Providers, Versions, Registries</a></li>
|
||
<li><a href="#sc-rollout">XVII. Scenarios: Rollouts and Cycles</a></li>
|
||
<li><a href="#open-questions">Open Questions</a></li>
|
||
<li><a href="#what-changes">Appendix A: What Changes</a></li>
|
||
<li><a href="#glossary">Appendix B: Glossary</a></li>
|
||
<li><a href="#the-working-document">Appendix C: The Working Document</a></li>
|
||
</ol></nav>
|
||
|
||
<section class="preface" id="before-we-begin">
|
||
<h2>Before we begin</h2>
|
||
<p class="dropcap">It is a Tuesday morning and Mario opens a session on his phone. The server starts a worker for it. The worker
|
||
connects to the server, loads its plugins and opens the agent's harness. One of the plugins, code mode, needs the
|
||
harness before it can contribute its tools. Another, search, needs a feed that lives on the server. A third, sharing,
|
||
was removed from the worker's plugin set last night, but the notes view still asks for it. At the same moment the
|
||
browser tab holds the session's notes, which live on the worker that is still starting.</p>
|
||
<p>Every one of these is the same question asked in different places: <em>what I need is not there yet; what now?</em> Chord
|
||
v2 answers it today in five different ways, depending on how the dependency was declared and where its provider
|
||
lives. A missing type on the same host breaks the object for good; the same type missing on another host makes it
|
||
retry every few seconds forever; a missing registry waits for a value; a reload that removes a type ends every
|
||
reference to it. And when two hosts start at once and need each other's objects in a chain that has no cycle at all,
|
||
both wait until a 60-second deadline breaks the knot.</p>
|
||
<p>The owner's direction was short: an object should be <strong>parked until its dependencies resolve</strong>, and there should be
|
||
no difference between a dependency on this host and one on another: a link going away is like the provider's plugin
|
||
being unloaded, and a link coming back is like it being loaded. This book works out what that means, rule by rule,
|
||
and plays it through the scenarios that decide whether it holds.</p>
|
||
<h3 id="eight-things-to-know">Eight things to know</h3>
|
||
<ol class="findings">
|
||
<li><strong>A request outlives its provider.</strong> A reference stands for a request (type, params, principal, version). It ends
|
||
only when its holder ends it, never because the provider is missing, went away or does not fit.</li>
|
||
<li><strong>One resolver for local and remote.</strong> The address decides where a request goes; the provider found there is
|
||
checked; a missing provider here and a missing provider on another host give the same answer.</li>
|
||
<li><strong>Two kinds of not-yet.</strong> Something under way (activating, reconnecting, installing, not published yet) shows as
|
||
<code>loading</code>; nothing under way (not served, no fitting version, a dependency failed) shows as <code>failed</code> with the new
|
||
cause <code>waiting</code>.</li>
|
||
<li><strong>Dependents park.</strong> An object whose dependencies are not all live waits with no timer and no backoff, and is
|
||
woken by events: a dependency changing state, or a name becoming usable on a host.</li>
|
||
<li><strong><code>broken</code> means the object's own fault.</strong> Only its own code and cycles break an object; whatever it waits for is
|
||
<code>waiting</code>.</li>
|
||
<li><strong>Startup needs one short gate, not a barrier.</strong> Requests wait only for the host's facets to be published, which
|
||
is synchronous; that ends the startup deadlock. Whether <code>load</code> then also waits for first outcomes is the owner's
|
||
call.</li>
|
||
<li><strong>One gap in cycle detection needs a choice.</strong> Objects on two hosts that start at the same moment and need each
|
||
other are caught today only by a deadline. A probe can walk the waiting edges and find the ring without failing
|
||
anything (recommended); a narrowed deadline is simpler but polls, misses a ring, or never finds the cycle at all.</li>
|
||
<li><strong>Only callers and own work keep clocks.</strong> Getters, parked calls and an object's own steps keep their deadlines;
|
||
waiting for a dependency has none.</li>
|
||
</ol>
|
||
<h3 id="words-used-throughout">Words used throughout</h3>
|
||
<p>A <strong>host</strong> is one participant (the server, a session worker, a browser tab). A <strong>provider</strong> is what serves a type or
|
||
a registry on a host: a facet registered it with <code>host.serve</code>. A <strong>request</strong> is what a reference stands for: a type
|
||
or registry, its params, the principal it acts as and the consumer's contract version. Its <strong>holder</strong> made it and
|
||
owns it: an object's attempt (<code>self.get</code>, <code>self.registry</code>), a call (<code>ctx.get</code>), outside code (<code>host.get</code>,
|
||
<code>host.hold</code>) or a peer host (a <code>hold</code> arriving over a link). A request is <strong>bound</strong> when it points at an object (here)
|
||
or a hold (over a link), and <strong>unbound</strong> with a reason otherwise. An <strong>attempt</strong> is one try at activating an object:
|
||
its factory, its requests, its natives. A <strong>dependency</strong> is a request an attempt needs live before it may go live. An
|
||
attempt is <strong>parked</strong> while one of its dependencies is not live. A <strong>publication</strong> is the moment a host makes its
|
||
facets' declarations visible: the setups of <code>load</code>, the commit of a reload, the commit of an install. A <strong>wake</strong> is an
|
||
event that makes the resolver look at a request again.</p>
|
||
<p>The running example is a small p3: a server <strong>S</strong> with the session catalog and the search feed; a worker <strong>W</strong> for
|
||
session <code>S1</code> with the harness, code mode, the notes store and the search indexer; and a browser tab <strong>T</strong> that shows
|
||
the notes. The plugins are <code>core</code>, <code>models</code>, <code>codemode</code>, <code>notes</code>, <code>search</code> and <code>sharing</code>.</p>
|
||
</section>
|
||
|
||
<section class="chapter" id="today"><header class="opener"><div class="numeral">I</div><h2>Five Answers to One Question</h2><p class="lede">What Chord v2 does today when a dependency is missing, and why the answers disagree.</p></header>
|
||
<p class="dropcap">Chord v2 was built slice by slice, and each slice answered "the provider is not there" for the case in front of it.
|
||
Slice 1 made <code>self.get</code> of a type nobody serves throw, because a typo in a type name should fail loudly. Slice 3b made
|
||
remote holds survive link drops, so a missing provider on another host became a refused hold that the consumer retries.
|
||
Slice 8 made <code>self.registry</code> a dependency that waits for a value. Each answer was reasonable alone. Together they
|
||
give five outcomes for one situation.</p>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Declared as</th>
|
||
<th>The provider is missing</th>
|
||
<th>Today</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody><tr>
|
||
<td><code>self.get(LocalType)</code></td>
|
||
<td>no facet serves it</td>
|
||
<td><code>self.get</code> throws, so the factory throws: the object is <code>broken</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>self.get(RemoteType)</code></td>
|
||
<td>the worker does not serve it</td>
|
||
<td>the hold is refused <code>broken</code>; the dependent is <code>transient</code> and retried every 1 to 60 seconds, forever</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>self.registry(R)</code></td>
|
||
<td>no plugin serves <code>R</code></td>
|
||
<td><code>transient</code>, woken at <code>R</code>'s first value only</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>self.registry(R)</code>, the reader newer than the provider</td>
|
||
<td></td>
|
||
<td><code>broken</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>a reload removes the type</td>
|
||
<td></td>
|
||
<td>every holder gets <code>gone</code>: ended for good</td>
|
||
</tr>
|
||
</tbody></table>
|
||
<p>The differences are not about the situation. Whether the notes plugin is missing on this host or on the worker, the
|
||
notes view cannot work until someone installs it. Yet in one case the view is broken until a reload, in the other it
|
||
polls the worker every minute for the rest of the session, and the reload that finally brings the plugin back cannot
|
||
revive a reference that got <code>gone</code>.</p>
|
||
<h3 id="the-deadlock-without-a-cycle">The deadlock without a cycle</h3>
|
||
<p>The startup barrier has a worse problem. Sol reproduced it against the current code (<code>/tmp/dep-graph-sol-barrier.log</code>).</p>
|
||
<figure class="fig anim"><svg viewBox="0 0 720 401" width="100%" role="img" aria-label="The startup deadlock today. Nothing in the object graph is cyclic; the two barriers block each other." xmlns="http://www.w3.org/2000/svg" style="font-family:'Avenir Next','Helvetica Neue',Helvetica,Arial,sans-serif;max-width:760px"><defs><marker id="f1m" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#7b7468"/></marker><marker id="f1a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#b4472f"/></marker><marker id="f1b" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#2e6a8a"/></marker></defs><line x1="188" y1="52" x2="188" y2="393" stroke="#c9bfae" stroke-dasharray="3 3"/><rect x="103" y="10" width="170" height="42" rx="4" fill="#fff" stroke="#c9bfae"/><text x="188" y="27" font-size="11" fill="#2a2723" text-anchor="middle">server S</text><text x="188" y="41" font-size="9.5" fill="#7b7468" text-anchor="middle">load: waits for A</text><line x1="532" y1="52" x2="532" y2="393" stroke="#c9bfae" stroke-dasharray="3 3"/><rect x="447" y="10" width="170" height="42" rx="4" fill="#fff" stroke="#c9bfae"/><text x="532" y="27" font-size="11" fill="#2a2723" text-anchor="middle">worker W</text><text x="532" y="41" font-size="9.5" fill="#7b7468" text-anchor="middle">load: waits for BStore</text><g class="step edge" style="--i:1"><path d="M188 72 h34 v14 h-32" fill="none" stroke="#7b7468" marker-end="url(#f1m)"/><text x="230" y="82" font-size="10" fill="#2a2723" text-anchor="start">setups: serve C; start eager A</text></g><g class="step edge" style="--i:2"><line x1="188" y1="118" x2="529" y2="118" stroke="#b4472f" marker-end="url(#f1a)"/><text x="360" y="112" font-size="10" fill="#b4472f" text-anchor="middle">A needs B: hold B</text></g><g class="step edge" style="--i:3"><path d="M532 144 h-34 v14 h32" fill="none" stroke="#7b7468" marker-end="url(#f1m)"/><text x="490" y="154" font-size="10" fill="#2a2723" text-anchor="end">setups: serve B; start eager BStore</text></g><g class="step edge" style="--i:4"><line x1="532" y1="190" x2="191" y2="190" stroke="#b4472f" marker-end="url(#f1a)"/><text x="360" y="184" font-size="10" fill="#b4472f" text-anchor="middle">BStore needs C: hold C</text></g><g class="step edge" style="--i:5"><path d="M188 216 h34 v14 h-32" fill="none" stroke="#7b7468" marker-end="url(#f1m)"/><text x="230" y="226" font-size="10" fill="#2a2723" text-anchor="start">Hold.start waits for S.pendingLoad</text></g><g class="step" style="--i:6"><rect x="26" y="248" width="668" height="23" rx="3" fill="#f4eee2" stroke="#c2b18c"/><text x="360" y="262" font-size="10" fill="#5b5549" text-anchor="middle">S's load waits for A, A for B, B for BStore, BStore for C, C's hold for S's load</text></g><g class="step" style="--i:7"><line x1="16" y1="295" x2="704" y2="295" stroke="#c9bfae" stroke-dasharray="2 4"/><rect x="322" y="285" width="76" height="18" fill="#fbf8f2"/><text x="360" y="299" font-size="9.5" fill="#7b7468" text-anchor="middle" font-weight="600" letter-spacing="1.5">60 S LATER</text></g><g class="step edge" style="--i:8"><path d="M188 323 h34 v14 h-32" fill="none" stroke="#7b7468" marker-end="url(#f1m)"/><text x="230" y="333" font-size="10" fill="#2a2723" text-anchor="start">A's activation deadline fires, A fails</text></g><g class="step edge" style="--i:9"><line x1="188" y1="369" x2="529" y2="369" stroke="#7b7468" stroke-dasharray="4 3" marker-end="url(#f1m)"/><text x="360" y="363" font-size="10" fill="#2a2723" text-anchor="middle">C binds, BStore and B go live, A is retried</text></g></svg><figcaption><button class="replay" type="button">Replay</button><span class="fignum">Figure 1.</span> The startup deadlock today. Nothing in the object graph is cyclic; the two barriers block each other.</figcaption></figure>
|
||
<p><code>C</code> has no dependencies and is registered the moment S's setups ran. Its hold waits anyway, because incoming holds
|
||
wait for <code>pendingLoad</code>, and <code>pendingLoad</code> is the whole of <code>load</code>: the setups and the barrier that waits for every
|
||
host-lifetime object, <code>A</code> included. Two jobs share one promise. Holds only need the first ("are the types
|
||
registered?"); the second ("has every plugin had its first try?") is for the app. Mixing them makes an acyclic chain
|
||
across two hosts wait until a timeout fires.</p>
|
||
<h3 id="timers-standing-in-for-events">Timers standing in for events</h3>
|
||
<p>Both problems have the same root. Where Chord does not know when something will change, it polls or waits for a
|
||
deadline: the identity backoff retries a dependent whose remote provider refused it; the activation deadline breaks the
|
||
startup knot; the same deadline is the only answer to two objects on two hosts that start at the same moment and need
|
||
each other, a cycle the activation chains cannot see (design §6, "Known limitation").</p>
|
||
<p>A timer is the right tool when nothing else can tell Chord that the world changed: a disk that may be back, a worker
|
||
process that may start this time, a silent transport. It is the wrong tool when an event exists or can exist. A
|
||
missing plugin comes back through a reload, which is an event on the host that serves it. A dependency going live is
|
||
an event. A link opening is an event. The model below uses those events and keeps timers only where no event can
|
||
exist.</p>
|
||
</section>
|
||
|
||
<section class="chapter" id="requests"><header class="opener"><div class="numeral">II</div><h2>Requests That Wait</h2><p class="lede">A reference is a request with a holder; it outlives every provider it meets.</p></header>
|
||
<p class="dropcap">The first change is a separation the code makes only halfway today: between what a reference <strong>asks for</strong> and what
|
||
currently <strong>answers</strong> it. When the notes view on W does <code>self.get(NoteStore, {})</code>, it asks for "the NoteStore of this
|
||
host, as the host, at contract version 3". Which object answers is a second question, and the answer can change: no
|
||
object yet, an object activating, a live one, a failed one, none again after a reload, a new one after the next.</p>
|
||
<h3 id="a-request-and-its-holder">A request and its holder</h3>
|
||
<p>A <strong>request</strong> is the asking part: type (or registry), params, principal and the consumer's contract version. Every
|
||
request has exactly one <strong>holder</strong>, which made it and is the only one that ends it.</p>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Holder</th>
|
||
<th>Made by</th>
|
||
<th>Ends when</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody><tr>
|
||
<td>an attempt</td>
|
||
<td><code>self.get</code>, <code>self.registry</code>, a keyed pin <code>ref.notes.at(key)</code></td>
|
||
<td>the attempt ends (its object ended, or a reload replaced it)</td>
|
||
</tr>
|
||
<tr>
|
||
<td>a call</td>
|
||
<td><code>ctx.get</code></td>
|
||
<td>the call settles</td>
|
||
</tr>
|
||
<tr>
|
||
<td>outside code</td>
|
||
<td><code>host.get</code>, <code>host.hold</code>, <code>host.registry</code></td>
|
||
<td><code>release()</code>, or the getter gives up</td>
|
||
</tr>
|
||
<tr>
|
||
<td>a peer host</td>
|
||
<td>a <code>hold</code> arriving over a link</td>
|
||
<td>the peer releases it, or its link or origin closes</td>
|
||
</tr>
|
||
<tr>
|
||
<td>any of them</td>
|
||
<td></td>
|
||
<td>revocation of its principal; its own host closes</td>
|
||
</tr>
|
||
</tbody></table>
|
||
<p>That list is the whole of how a request ends. <strong>A request never ends because its provider is missing, went away or
|
||
does not fit.</strong> Today it does in two places: a <code>self.get</code> of a type nobody serves throws before the request exists,
|
||
and a reload that removes a type sends <code>gone</code>. Both go.</p>
|
||
<h3 id="the-address-decides-where">The address decides where</h3>
|
||
<p>A request goes to one place, and the address decides which. For a <code>local</code> type, the place is this
|
||
host. For a <code>remote</code> type, the consumer computes the address from the params with its own contract's
|
||
<code>hostAddress</code> function (design §3; <code>placement</code> until 2026-10-29): <code>Notes { session: "S1" }</code> is placed on <code>Session { id: "S1" }</code>. If that is this host's
|
||
address, the request goes here; otherwise over the route for that kind, or over the parent link.</p>
|
||
<p>Until 2026-10-29 the code checked something else first: whether the type is registered here. A type placed on this host but not
|
||
registered yet was sent over the parent link, which is wrong twice. Sol's example: the server has address <code>Server</code>,
|
||
the catalog type is placed on <code>Server</code>, and no facet serves it yet. That code, <code>Placement.reference</code> (<code>host/placement.ts</code>, now <code>host/destination.ts</code>), sent the server's
|
||
own request over a parent link the server never had, and when the catalog plugin was installed later, nothing woke
|
||
that request, because it was never registered as waiting here. The rename removed the first half; slice 7 builds the
|
||
wait.</p>
|
||
<figure class="fig wide"><svg viewBox="0 0 720 231" width="100%" role="img" aria-label="The destination by address. The destination follows from the address alone; only then is the provider found there checked." xmlns="http://www.w3.org/2000/svg" style="font-family:'Avenir Next','Helvetica Neue',Helvetica,Arial,sans-serif;max-width:760px"><defs><marker id="f2m" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#7b7468"/></marker><marker id="f2a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#b4472f"/></marker><marker id="f2b" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#2e6a8a"/></marker></defs><text x="16" y="22" font-size="10" fill="#b4472f" text-anchor="start" font-weight="600" letter-spacing="2">WHERE</text><text x="16" y="144" font-size="10" fill="#7b7468" text-anchor="start" font-weight="600" letter-spacing="2">THEN</text><rect x="19" y="36" width="210" height="44" rx="4" fill="#fff" stroke="#b4472f" stroke-width="1.5"/><text x="124" y="55.3" font-size="11" fill="#2a2723" text-anchor="middle">the request's address</text><text x="124" y="69.3" font-size="9.5" fill="#7b7468" text-anchor="middle">from the consumer's own `hostAddress`</text><rect x="255" y="36" width="210" height="44" rx="4" fill="#fff" stroke="#c9bfae"/><text x="360" y="55.3" font-size="11" fill="#2a2723" text-anchor="middle">here</text><text x="360" y="69.3" font-size="9.5" fill="#7b7468" text-anchor="middle">a local type, or this host's address</text><rect x="491" y="36" width="210" height="44" rx="4" fill="#fff" stroke="#2e6a8a" stroke-width="1.5"/><text x="596" y="55.3" font-size="11" fill="#2a2723" text-anchor="middle">a route or the parent</text><text x="596" y="69.3" font-size="9.5" fill="#7b7468" text-anchor="middle">every other address</text><rect x="137" y="158" width="210" height="55" rx="4" fill="#fff" stroke="#c9bfae"/><text x="242" y="176.5" font-size="11" fill="#2a2723" text-anchor="middle">check the provider here</text><text x="242" y="190.5" font-size="9.5" fill="#7b7468" text-anchor="middle">served? fitting version? authorize,</text><text x="242" y="203" font-size="9.5" fill="#7b7468" text-anchor="middle">own host address</text><rect x="373" y="158" width="210" height="55" rx="4" fill="#fff" stroke="#2e6a8a" stroke-width="1.5"/><text x="478" y="176.5" font-size="11" fill="#2a2723" text-anchor="middle">hold over the path</text><text x="478" y="190.5" font-size="9.5" fill="#7b7468" text-anchor="middle">the provider host checks it the same</text><text x="478" y="203" font-size="9.5" fill="#7b7468" text-anchor="middle">way</text><path d="M229 58 L253 58" fill="none" stroke="#7b7468" marker-end="url(#f2m)"/><path d="M124 32 Q360 -12 596 32" fill="none" stroke="#7b7468" marker-end="url(#f2m)"/><path d="M360 80 L242 156" fill="none" stroke="#7b7468" marker-end="url(#f2m)"/><path d="M596 80 L478 156" fill="none" stroke="#7b7468" marker-end="url(#f2m)"/></svg><figcaption><span class="fignum">Figure 2.</span> The destination by address. The destination follows from the address alone; only then is the provider found there checked.</figcaption></figure>
|
||
<p>The rule, from the reviews (Sol B1, Astra F1): the destination never depends on what is registered when the type names an
|
||
address. A request placed here whose provider is missing is <strong>unbound here</strong>, waiting for the name. A version check
|
||
compares the consumer with the provider actually selected, never with a same-name registration on the consumer's own
|
||
host (today <code>#assertRoute</code> does that before choosing the destination, so worker W1 serving <code>Notes</code> v1 rejects a <code>Notes</code> v2 request
|
||
placed on W2, which serves v2).</p>
|
||
<p>Every <code>remote</code> type declares its <code>hostAddress</code> (owner, 2026-10-29, who also renamed <code>placement</code> to it). Until then
|
||
<code>remote()</code> allowed leaving it out, a
|
||
leftover of slice 3a, which built remote objects over one link before host kinds existed; such a type went "here if
|
||
served, otherwise the parent", which needed its own fallback and a re-placement when a tab that loads before it
|
||
connects gets its first parent link (Opus, Sol). Opus and Fable proposed making it required; the owner agreed.</p>
|
||
<h3 id="what-the-check-finds">What the check finds</h3>
|
||
<p>Once placed, the provider found there is checked: served, a fitting version (the current one or an accepted older one,
|
||
with the fingerprint computed from the provider's own history), <code>authorize</code>, and the provider's own host-address check.
|
||
The request is then <strong>bound</strong> (to an object here, or to a hold over a path), or <strong>unbound</strong> with a reason:</p>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>The check finds</th>
|
||
<th>The request</th>
|
||
<th>Shows</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody><tr>
|
||
<td>a fitting provider</td>
|
||
<td>bound</td>
|
||
<td>its object's state</td>
|
||
</tr>
|
||
<tr>
|
||
<td>this host has not published its facets yet</td>
|
||
<td>unbound <code>unpublished</code></td>
|
||
<td><code>loading</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>nobody serves the name at its address</td>
|
||
<td>unbound <code>not_served</code></td>
|
||
<td><code>failed { waiting }</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>no fitting version, a fingerprint that differs at the same version, a host address the provider computes elsewhere</td>
|
||
<td>unbound <code>version</code></td>
|
||
<td><code>failed { waiting }</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>the registry is served, but its first <code>compose</code>, or the reader's version mapping, threw</td>
|
||
<td>unbound <code>compose</code></td>
|
||
<td><code>failed { waiting }</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>the provider host is installing, and the hold does not fit its current code</td>
|
||
<td>unbound <code>installing</code>, at the provider</td>
|
||
<td><code>loading</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>the path has no link (a drop, a route resolving)</td>
|
||
<td>waits on the path</td>
|
||
<td><code>loading</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>authorize</code> refused, or the params are invalid</td>
|
||
<td>refused</td>
|
||
<td><code>failed { broken }</code>, as today</td>
|
||
</tr>
|
||
</tbody></table>
|
||
<p>The reasons that wait for a name (<code>unpublished</code>, <code>not_served</code>, <code>version</code>, <code>compose</code>) are kept in an index under that
|
||
name, on the host that decided them: the consumer's host for a local request, the provider's host for a hold that
|
||
arrived over a link. The provider already keeps such holds today as <strong>refused</strong> (<code>remote/hold.ts</code>), and <code>retry</code> re-runs
|
||
them. <code>installing</code> belongs to the provider host's own install; <code>no_link</code> lives on the path, where the next link holds
|
||
the request again, as today. Every index drops an entry when its request ends, so a released getter, a closed tab or a
|
||
revoked guest leaves nothing behind. A refused request (no authority, bad params) is never indexed: a later
|
||
publication cannot grant authority.</p>
|
||
<h3 id="the-publication-gate">The publication gate</h3>
|
||
<p>There is one short gate. Before a host has run its facets' setups, it does not know what it serves, so it cannot tell
|
||
"not served" from "not registered yet". A worker connects to its parent before it loads (W6), so holds can arrive in
|
||
that window, and p2's web shell holds references before its own <code>load</code> resolved.</p>
|
||
<p>So a request made on a host, or arriving on its links, before the host's <strong>first publication</strong> is not placed at all:
|
||
it is unbound <code>unpublished</code> and shows <code>loading</code>. The publication places it. Without this gate the request would be
|
||
placed by a half-empty registration table (Fable's trace: W1's own <code>Notes</code> sent over the parent, relayed back by the
|
||
server, forever), and a tab would see "not served" during every worker start (Opus's trace).</p>
|
||
<p>The gate is not the barrier. It lasts only until the setups ran, and the setups are synchronous code at the start of
|
||
<code>load</code>: they register types and registries and contribute, and wait for nothing. That is the difference that ends
|
||
the startup deadlock (chapter VI).</p>
|
||
</section>
|
||
|
||
<section class="chapter" id="states"><header class="opener"><div class="numeral">III</div><h2>Loading or Waiting</h2><p class="lede">What a reference shows while it is not live, and why a missing plugin is an answer rather than a spinner.</p></header>
|
||
<p class="dropcap"><code>ref.state</code> keeps its four kinds: <code>loading</code>, <code>live</code>, <code>failed</code>, <code>ended</code>. What changes is one new kind of failure cause.</p>
|
||
<pre><code><span class="tk-k">type</span> <span class="tk-t">RefState</span> =
|
||
| { kind: <span class="tk-s">"loading"</span> }
|
||
| { kind: <span class="tk-s">"live"</span> }
|
||
| { kind: <span class="tk-s">"failed"</span>; cause: <span class="tk-t">FailureCause</span>; retryAt: number | <span class="tk-k">undefined</span> }
|
||
| { kind: <span class="tk-s">"ended"</span>; error: <span class="tk-t">ChordError</span> };
|
||
|
||
<span class="tk-k">type</span> <span class="tk-t">FailureCause</span> =
|
||
| { kind: <span class="tk-s">"transient"</span>; error: <span class="tk-t">ChordError</span> } <span class="tk-c">// the object's own effect failed (an open, a follow): backoff</span>
|
||
| { kind: <span class="tk-s">"broken"</span>; error: <span class="tk-t">ChordError</span> } <span class="tk-c">// the object's own code, a cycle, or authority: retry() or a reload</span>
|
||
| { kind: <span class="tk-s">"waiting"</span>; error: <span class="tk-t">ChordError</span> }; <span class="tk-c">// not the object's fault: woken by an event, never by a timer</span></code></pre>
|
||
<h3 id="the-mapping">The mapping</h3>
|
||
<p>Whether a request that is not live shows <code>loading</code> or <code>failed { waiting }</code> follows from what it waits for:</p>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>The request is</th>
|
||
<th><code>ref.state</code></th>
|
||
<th>Calls on it</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody><tr>
|
||
<td>bound, its object activating, or parked with no failed blocker</td>
|
||
<td><code>loading</code></td>
|
||
<td>park, bounded</td>
|
||
</tr>
|
||
<tr>
|
||
<td>unbound <code>unpublished</code> or <code>installing</code>, or its path has no link</td>
|
||
<td><code>loading</code></td>
|
||
<td>park, bounded</td>
|
||
</tr>
|
||
<tr>
|
||
<td>unbound <code>not_served</code>, <code>version</code> or <code>compose</code></td>
|
||
<td><code>failed { waiting }</code>, no <code>retryAt</code></td>
|
||
<td>reject at once with the cause</td>
|
||
</tr>
|
||
<tr>
|
||
<td>bound, its object failed</td>
|
||
<td><code>failed</code> with the object's own cause and <code>retryAt</code>, as today</td>
|
||
<td>reject at once</td>
|
||
</tr>
|
||
<tr>
|
||
<td>bound, its object live</td>
|
||
<td><code>live</code></td>
|
||
<td>run</td>
|
||
</tr>
|
||
<tr>
|
||
<td>ended by its holder, revoked, or its host closed</td>
|
||
<td><code>ended</code></td>
|
||
<td>reject</td>
|
||
</tr>
|
||
</tbody></table>
|
||
<p>The intuition is "is something under way?": an activation, a reconnect, an install, a load. When something is under
|
||
way, the reference waits and calls park; when nothing is, the reference says what is missing and calls fail fast.
|
||
The reviewers pointed out that "under way" is not something Chord can observe in general (a parent reconnect is the
|
||
app's, a <code>resolve</code> may hang), so the table is the rule and the intuition only explains it.</p>
|
||
<h3 id="a-link-drop-and-an-unload-side-by-side">A link drop and an unload, side by side</h3>
|
||
<p>The owner's point was that a link going away is like the provider's plugin unloading. For the request they are the
|
||
same: it stays, it keeps its last values readable, it binds again when a provider is reachable, and its holder
|
||
notices nothing but the state. They differ in what is under way.</p>
|
||
<div class="steps"><div class="steps-head"><span class="title">The notes view on T, once through a link drop and once through an unload on W</span><span class="count"></span><button class="prev" type="button">Back</button><button class="next" type="button">Next</button></div><div class="step-panel"><h4>Live</h4><p>T's notes view holds <code>Notes { S1 }</code> on W through the server. <code>ref.state</code> is <code>live</code>; the view renders the notes.</p>
|
||
</div><div class="step-panel"><h4>The link drops</h4><p>W's link to the server closes (a crash, a network change). The request on T stays; the path has no link: unbound
|
||
<code>no_link</code>, <code>ref.state</code> <code>loading</code>. The last notes stay readable. A call made now parks, bounded by <code>readyTimeout</code>.
|
||
Calls already sent fail and are never resent: their outcome is unknown.</p>
|
||
</div><div class="step-panel"><h4>The link comes back</h4><p>The route resolves again, the request is held anew on the new link, <code>bound</code> arrives with a fresh snapshot. <code>live</code>.
|
||
Parked calls go out in order.</p>
|
||
</div><div class="step-panel"><h4>The plugin is removed on W</h4><p>A reload on W removes the <code>notes</code> plugin. The provider's object ends as today: its natives close, its callers are
|
||
rejected at once, its handlers drain. The request on T does not end: W sends <code>failed { waiting, not_served }</code> on
|
||
the bound hold, which unbinds it. <code>ref.state</code> is <code>failed { waiting }</code>; the last notes stay readable; new calls fail at once with "notes is
|
||
not served on Session S1".</p>
|
||
</div><div class="step-panel"><h4>The plugin comes back</h4><p>A later reload on W serves <code>Notes</code> again. The publication re-runs W's refused holds for that name: <code>loading</code>, then
|
||
<code>bound</code>. T's view is live again without having done anything.</p>
|
||
</div></div>
|
||
<p>One difference must stay: calls already sent. When a link drops, a sent call's outcome is unknown, so it fails and
|
||
is not resent. When a plugin unloads on the same host, its handlers are drained on the old code (design §21). Location
|
||
decides nothing else.</p>
|
||
<div class="note review"><b class="tag">Review: a third cause kind, or not</b><p>Opus and Fable recommend <code>waiting</code> as above. Astra would keep two kinds and mark event-waited failures as <code>transient</code>
|
||
with no <code>retryAt</code> (today's wire already carries a <code>transient</code> cause without <code>retryIn</code>, <code>remote/remote-object.ts</code>), so
|
||
no kind is added; the cost is that <code>transient</code> then means two things, a backoff under way or an event awaited. Sol would show <code>loading</code> with a diagnostic; that parks every call on a missing plugin for 60
|
||
seconds and spins forever in the UI. The draft recommends <code>waiting</code>. <strong>Owner</strong> decision 3.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="chapter" id="parked"><header class="opener"><div class="numeral">IV</div><h2>Parked</h2><p class="lede">An object whose dependencies are not all live waits, with no timer, and goes on the moment they are.</p></header>
|
||
<p class="dropcap">The owner's instinct was that an object is parked until its dependencies all cleanly resolve. This chapter makes that
|
||
exact: what a dependency is, what the parked object shows, what it keeps, and where parking ends.</p>
|
||
<h3 id="what-counts-as-a-dependency">What counts as a dependency</h3>
|
||
<p>A <strong>dependency</strong> is a request an attempt makes in its factory and needs live before the object may go live:</p>
|
||
<ul>
|
||
<li><code>self.get(Type, params)</code>, local or remote: one rule for both;</li>
|
||
<li><code>self.registry(R)</code>: a registry at the reader's version (chapter X);</li>
|
||
<li>a keyed pin, <code>ref.notes.at(key)</code> on another object's keyed member: the key must be loaded.</li>
|
||
</ul>
|
||
<p>Two things are not dependencies. <code>self.follow(attach)</code> is the attempt's own step: its <code>attach</code> runs Durable's code
|
||
and may fail like an <code>open</code>, with the object's own <code>transient</code> failure and backoff (a follower that wants to recover
|
||
the moment a new harness appears declares <code>self.retryOn</code> on the registry it reads the harness through). And a
|
||
<code>ctx.get</code> in a method is not part of the object's activation at all: it is the call's request.</p>
|
||
<h3 id="the-dependency-check">The dependency check</h3>
|
||
<p>An attempt runs its factory, hydrates its persisted singletons (they are the object's documents, kept across
|
||
attempts, as today), makes its requests, and is <strong>parked</strong> while any dependency is not live (and hydrated). What it
|
||
shows follows its dependencies:</p>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Its dependencies</th>
|
||
<th>The parked object shows</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody><tr>
|
||
<td>any <code>loading</code>, none failed</td>
|
||
<td><code>loading</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>any <code>failed</code></td>
|
||
<td><code>failed { waiting, "waiting for X: <X's cause>" }</code>, no <code>retryAt</code>; the message keeps the nearest blocker and the root cause within the 500-character rule</td>
|
||
</tr>
|
||
<tr>
|
||
<td>one failed with <code>invalid/cycle</code> (a dependent off the ring)</td>
|
||
<td><code>broken</code> with the same error: it would wait on a ring that never heals by itself</td>
|
||
</tr>
|
||
<tr>
|
||
<td>one <code>ended</code> (revoked, or its host closed)</td>
|
||
<td><code>broken</code> with that error: the request cannot come back; <code>retry()</code> or a reload makes new requests</td>
|
||
</tr>
|
||
</tbody></table>
|
||
<p>When every dependency is live, the attempt passes the <strong>dependency check</strong> and runs its own steps: its natives in
|
||
declaration order, its follow attaches, the member checks, its derives. Those are
|
||
bounded by its own <code>readyTimeout</code> deadline, as today. Then it goes live.</p>
|
||
<p>The dependency check is the boundary between waiting and working (Astra's F3). Before it, the attempt holds no
|
||
natives; after it, a dependency that leaves <code>live</code> is treated as after live (R18): the attempt goes on, and its calls
|
||
through that reference meet the reference's state. So a parked attempt never holds a native, and an attempt that
|
||
opened a native is never parked. It is not a rule of its own: it is R18's rule for live objects (a dependency that
|
||
goes away leaves the object live; same-host calls in flight finish, remote ones fail, new calls meet the reference's
|
||
state), applied from the dependency check on. Without that boundary a contributor unloading while the attempt opens its natives
|
||
would leave it half open and parked, or force a seal, a drain and a fresh attempt for a dependency the object may not
|
||
even use at once.</p>
|
||
<figure class="fig anim"><svg viewBox="0 0 720 331" width="100%" role="img" aria-label="Code mode on W parks on the harness and goes on the moment it is live." xmlns="http://www.w3.org/2000/svg" style="font-family:'Avenir Next','Helvetica Neue',Helvetica,Arial,sans-serif;max-width:760px"><defs><marker id="f3m" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#7b7468"/></marker><marker id="f3a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#b4472f"/></marker><marker id="f3b" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#2e6a8a"/></marker></defs><line x1="130.7" y1="52" x2="130.7" y2="323" stroke="#c9bfae" stroke-dasharray="3 3"/><rect x="45.7" y="10" width="170" height="42" rx="4" fill="#fff" stroke="#c9bfae"/><text x="130.7" y="27" font-size="11" fill="#2a2723" text-anchor="middle">CodeMode</text><text x="130.7" y="41" font-size="9.5" fill="#7b7468" text-anchor="middle">host-lifetime, local</text><line x1="360" y1="52" x2="360" y2="323" stroke="#c9bfae" stroke-dasharray="3 3"/><rect x="275" y="10" width="170" height="42" rx="4" fill="#fff" stroke="#c9bfae"/><text x="360" y="27" font-size="11" fill="#2a2723" text-anchor="middle">Harness</text><text x="360" y="41" font-size="9.5" fill="#7b7468" text-anchor="middle">host-lifetime, local</text><line x1="589.3" y1="52" x2="589.3" y2="323" stroke="#c9bfae" stroke-dasharray="3 3"/><rect x="504.3" y="10" width="170" height="42" rx="4" fill="#fff" stroke="#c9bfae"/><text x="589.3" y="27" font-size="11" fill="#2a2723" text-anchor="middle">HarnessModels</text><text x="589.3" y="41" font-size="9.5" fill="#7b7468" text-anchor="middle">registry</text><g class="step edge" style="--i:1"><line x1="130.7" y1="78" x2="357" y2="78" stroke="#7b7468" marker-end="url(#f3m)"/><text x="245.3" y="72" font-size="10" fill="#2a2723" text-anchor="middle">self.get(Harness): loading</text></g><g class="step" style="--i:2"><rect x="26" y="96" width="209.3" height="23" rx="3" fill="#f4eee2" stroke="#c2b18c"/><text x="130.7" y="110" font-size="10" fill="#5b5549" text-anchor="middle">parked: shows loading, no timer</text></g><g class="step edge" style="--i:3"><line x1="360" y1="147" x2="586.3" y2="147" stroke="#7b7468" marker-end="url(#f3m)"/><text x="474.7" y="141" font-size="10" fill="#2a2723" text-anchor="middle">self.registry(HarnessModels): usable</text></g><g class="step edge" style="--i:4"><path d="M360 173 h34 v14 h-32" fill="none" stroke="#7b7468" marker-end="url(#f3m)"/><text x="402" y="183" font-size="10" fill="#2a2723" text-anchor="start">own steps: open Durable</text></g><g class="step edge" style="--i:5"><line x1="360" y1="219" x2="133.7" y2="219" stroke="#b4472f" marker-end="url(#f3a)"/><text x="245.3" y="213" font-size="10" fill="#b4472f" text-anchor="middle">Harness live: a state change</text></g><g class="step edge" style="--i:6"><path d="M130.7 245 h34 v14 h-32" fill="none" stroke="#7b7468" marker-end="url(#f3m)"/><text x="172.7" y="255" font-size="10" fill="#2a2723" text-anchor="start">dependency check passes; open the sandbox</text></g><g class="step edge" style="--i:7"><path d="M130.7 285 h34 v14 h-32" fill="none" stroke="#7b7468" marker-end="url(#f3m)"/><text x="172.7" y="295" font-size="10" fill="#2a2723" text-anchor="start">live; contribute tools</text></g></svg><figcaption><button class="replay" type="button">Replay</button><span class="fignum">Figure 3.</span> Code mode on W parks on the harness and goes on the moment it is live.</figcaption></figure>
|
||
<h3 id="no-clock-while-parked">No clock while parked</h3>
|
||
<p>A parked attempt has no dependency deadline and no backoff. Its <code>readyTimeout</code> deadline is paused while it is
|
||
parked, as eager migrations already pause it (<code>attempt.ts</code>, <code>deadline.pause()</code>), and resumes with the budget left when
|
||
the attempt passes the check. Today a dependency that does not go live within 60 seconds fails its dependent
|
||
<code>transient</code>, which then retries with backoff from 1 to 60 seconds. Both go: the dependent is woken when the dependency
|
||
changes, and the time a dependency takes is bounded where it runs, by that dependency's own steps.</p>
|
||
<h3 id="what-a-parked-attempt-keeps">What a parked attempt keeps</h3>
|
||
<p>It keeps its documents (the object's), its declarations and its requests (R18's holding today), and its seal token.
|
||
It keeps no native. It ends with its object: when its last holder releases it, or, for a host-lifetime object, when
|
||
its plugin is removed or the host closes. A parked object
|
||
with params that nobody holds any more ends and releases its requests, so a whole parked chain goes when its top is
|
||
released.</p>
|
||
<h3 id="broken-is-the-object-39-s-own-fault"><code>broken</code> is the object's own fault</h3>
|
||
<p>After the model, <code>broken</code> means exactly three things: the object's own code failed (a throwing factory, a derive that
|
||
throws at activation, a hydration or <code>migrate</code> that fails, a document fingerprint mismatch, an <code>open</code> that throws
|
||
<code>broken(...)</code>), the object is on a cycle, or it lost its authority (<code>forbidden</code>, a dependency that ended). Everything an object waits for, including a provider that is itself
|
||
broken, makes it <code>waiting</code>. The difference matters for recovery: a broken object needs <code>ref.retry()</code> or a reload of its
|
||
own plugin; a waiting one recovers by itself when what it waits for recovers.</p>
|
||
<h3 id="retry-passes-on">Retry passes on</h3>
|
||
<p>A Retry button on a waiting object should do something. Opus's trace: the tab's <code>Panel</code> waits for the server's <code>Dash</code>,
|
||
which waits for <code>Catalog</code>, which is broken by a factory bug that a configuration fix would heal at the next attempt.
|
||
The user sees "waiting for Dash: waiting for Catalog: …" and presses Retry. Today <code>Panel</code> would re-check, find <code>Dash</code>
|
||
still failed and do nothing.</p>
|
||
<p>So <code>ref.retry()</code> on a <code>failed { waiting }</code> object retries its failed blockers, hop by hop, with the existing <code>retry</code>
|
||
message over links: it retries the blockers that are <code>broken</code> or <code>transient</code> (the only ones a retry can change),
|
||
passes through the <code>waiting</code> ones, and leaves the rest alone. Each retry carries an operation id, so a diamond of
|
||
dependencies retries a shared leaf once. Retry of a <code>broken</code> or <code>transient</code> object starts a new attempt, as today. A
|
||
<code>retryOn</code> value call still retries a failed object as slice 8 decided; while the object is parked <code>waiting</code>, it does
|
||
nothing: its dependencies decide.</p>
|
||
<h3 id="a-reload-of-the-parked-object-itself">A reload of the parked object itself</h3>
|
||
<p>The parked attempt's factory never runs again while it is parked, with one exception: a reload of the object's own
|
||
plugin replaces the parked attempt with the new factory's attempt, as one handover: the new requests are made and
|
||
bound first, then the old ones released (Astra: a dependency held only by this object, unchanged by the reload, must
|
||
not end in between; today's <code>#activate</code> already binds before <code>#releasePrevious</code>). If the new code no longer declares
|
||
the missing dependency, the object can go live at once.</p>
|
||
<h3 id="after-live-nothing-changes">After live, nothing changes</h3>
|
||
<p>R18's second half stays: once an object is live, a dependency that leaves <code>live</code> (a link drop, a removal) leaves the
|
||
object live. Its calls that use the dependency get the dependency's state: they park while it is <code>loading</code> and reject
|
||
while it is <code>failed</code>. Reactivating every live consumer on every dependency loss would turn one worker disconnect into
|
||
closed natives, withdrawn contributions and aborted jobs on the server; the reviewers agreed it should not.</p>
|
||
</section>
|
||
|
||
<section class="chapter" id="wakes"><header class="opener"><div class="numeral">V</div><h2>What Wakes Whom</h2><p class="lede">Two kinds of event replace every poll and every dependency deadline.</p></header>
|
||
<p class="dropcap">A model without timers lives or dies by its wakes: a wake that is lost leaves an object parked forever. So the wakes
|
||
are kept to two kinds of event, each with one owner, and every unbound request and every parked attempt is
|
||
subscribed to exactly the events that can change it.</p>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Event</th>
|
||
<th>Who is told</th>
|
||
<th>Exists today?</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody><tr>
|
||
<td><strong>a target changes state</strong>: an object goes live, loading or failed, or gets a new cause; a hold receives <code>bound</code>, <code>loading</code> or <code>failed</code>, also after a re-hold on a new link</td>
|
||
<td>the requests bound to it, and the attempts that depend on those requests</td>
|
||
<td>partly: <code>dependencyLive</code> covers only "goes live"</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>a name becomes usable on a host</strong>: a publication (<code>load</code>'s setups, a reload's commit, an install's commit) serves a type, a registry or a version; a registry composes or maps a value at a version</td>
|
||
<td>the unbound requests indexed under that name on that host; a refused hold re-runs its start and sends <code>loading</code>, then <code>bound</code> or <code>failed</code></td>
|
||
<td>partly: refused holds and <code>retry</code> exist; the re-run at publication is new; a registry wakes only at its first value</td>
|
||
</tr>
|
||
</tbody></table>
|
||
<h3 id="every-change-not-only-quot-live-quot">Every change, not only "live"</h3>
|
||
<p>Draft 1 woke a parked attempt only when a dependency went live. Astra, Fable and Sol found the same hole: a dependency
|
||
that moves from <code>loading</code> to <code>failed</code>, or from one cause to another, leaves its dependent showing a stale state, and a
|
||
dependent that should now show <code>failed { waiting }</code> keeps parking calls. So a parked attempt re-evaluates on every
|
||
state change of any of its dependencies and updates what it shows. It leaves parking only when all of them are live.
|
||
Its factory never re-runs on a wake.</p>
|
||
<p>Three details keep this correct and cheap:</p>
|
||
<ul>
|
||
<li><strong>Coalesced, after the batch.</strong> Wakes run after the batch that caused them (a publication, a commit, an inbound
|
||
<code>bound</code>), once per object, so a commit that changes three dependencies wakes their dependent once, and no factory
|
||
runs inside another owner's batch.</li>
|
||
<li><strong>Remembered.</strong> A change that arrives while the attempt runs its own steps or tears down is remembered and applied
|
||
after (today's R18 wake memory, generalized).</li>
|
||
<li><strong>Indexed by name.</strong> A publication touches only the unbound requests waiting for the names it serves. A reload of
|
||
<code>notes</code> never looks at requests waiting for <code>search</code>.</li>
|
||
</ul>
|
||
<h3 id="across-a-link-no-new-message">Across a link: no new message</h3>
|
||
<p>The wake across hosts needs no new message kind. Today a provider keeps a hold it could not serve as refused, and a
|
||
<code>retry</code> from the consumer re-runs its start. The model adds one thing: the provider re-runs its refused holds itself,
|
||
at its own publication, for the names that publication serves.</p>
|
||
<figure class="fig anim"><svg viewBox="0 0 720 353" width="100%" role="img" aria-label="The search indexer on W waits for the server's feed, which a reload on S adds." xmlns="http://www.w3.org/2000/svg" style="font-family:'Avenir Next','Helvetica Neue',Helvetica,Arial,sans-serif;max-width:760px"><defs><marker id="f4m" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#7b7468"/></marker><marker id="f4a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#b4472f"/></marker><marker id="f4b" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#2e6a8a"/></marker></defs><line x1="188" y1="52" x2="188" y2="345" stroke="#c9bfae" stroke-dasharray="3 3"/><rect x="103" y="10" width="170" height="42" rx="4" fill="#fff" stroke="#c9bfae"/><text x="188" y="27" font-size="11" fill="#2a2723" text-anchor="middle">worker W</text><text x="188" y="41" font-size="9.5" fill="#7b7468" text-anchor="middle">Indexer, parked</text><line x1="532" y1="52" x2="532" y2="345" stroke="#c9bfae" stroke-dasharray="3 3"/><rect x="447" y="10" width="170" height="42" rx="4" fill="#fff" stroke="#c9bfae"/><text x="532" y="27" font-size="11" fill="#2a2723" text-anchor="middle">server S</text><text x="532" y="41" font-size="9.5" fill="#7b7468" text-anchor="middle">search plugin missing</text><g class="step edge" style="--i:1"><line x1="188" y1="78" x2="529" y2="78" stroke="#7b7468" marker-end="url(#f4m)"/><text x="360" y="72" font-size="10" fill="#2a2723" text-anchor="middle">hold SearchFeed</text></g><g class="step edge" style="--i:2"><line x1="532" y1="110" x2="191" y2="110" stroke="#7b7468" stroke-dasharray="4 3" marker-end="url(#f4m)"/><text x="360" y="104" font-size="10" fill="#2a2723" text-anchor="middle">failed { waiting, not_served }</text></g><g class="step" style="--i:3"><rect x="26" y="128" width="324" height="23" rx="3" fill="#f4eee2" stroke="#c2b18c"/><text x="188" y="142" font-size="10" fill="#5b5549" text-anchor="middle">Indexer shows failed { waiting }; no timer, no retries</text></g><g class="step" style="--i:4"><line x1="16" y1="175" x2="704" y2="175" stroke="#c9bfae" stroke-dasharray="2 4"/><rect x="244" y="165" width="232" height="18" fill="#fbf8f2"/><text x="360" y="179" font-size="9.5" fill="#7b7468" text-anchor="middle" font-weight="600" letter-spacing="1.5">A RELOAD ON S ADDS THE SEARCH PLUGIN</text></g><g class="step edge" style="--i:5"><path d="M532 203 h-34 v14 h32" fill="none" stroke="#7b7468" marker-end="url(#f4m)"/><text x="490" y="213" font-size="10" fill="#2a2723" text-anchor="end">publication serves SearchFeed; re-run refused holds for it</text></g><g class="step edge" style="--i:6"><line x1="532" y1="249" x2="191" y2="249" stroke="#7b7468" stroke-dasharray="4 3" marker-end="url(#f4m)"/><text x="360" y="243" font-size="10" fill="#2a2723" text-anchor="middle">loading</text></g><g class="step edge" style="--i:7"><line x1="532" y1="281" x2="191" y2="281" stroke="#7b7468" stroke-dasharray="4 3" marker-end="url(#f4m)"/><text x="360" y="275" font-size="10" fill="#2a2723" text-anchor="middle">bound (snapshot)</text></g><g class="step edge" style="--i:8"><path d="M188 307 h34 v14 h-32" fill="none" stroke="#b4472f" marker-end="url(#f4a)"/><text x="230" y="317" font-size="10" fill="#b4472f" text-anchor="start">a target changed state: Indexer re-evaluates, passes the check, goes live</text></g></svg><figcaption><button class="replay" type="button">Replay</button><span class="fignum">Figure 4.</span> The search indexer on W waits for the server's feed, which a reload on S adds.</figcaption></figure>
|
||
<p>Two existing messages change: <code>failed</code> carries the new cause kind <code>waiting</code>, and <code>loading</code> and a <code>waiting</code> <code>failed</code>
|
||
carry the provider's attempt id for the cycle probe (chapter VIII). The pinned list of causes and its test change with
|
||
them, and <code>PROTOCOL_VERSION</code> stays 1 until p3 ships.</p>
|
||
<h3 id="registries">Registries</h3>
|
||
<p>A registry reader is woken at every transition of the registry's value at its version to usable: when the registry
|
||
is served, when it composes for the first time, when an older reader's mapping succeeds again after it failed. Today
|
||
<code>RegistryValueSource</code> wakes only at the very first good value (<code>#valued</code>), which a registry whose provider is removed
|
||
and added again would never pass a second time.</p>
|
||
</section>
|
||
|
||
<section class="chapter" id="startup"><header class="opener"><div class="numeral">VI</div><h2>Starting Up</h2><p class="lede">The barrier did two jobs with one promise; separated, the deadlock disappears.</p></header>
|
||
<p class="dropcap"><code>load</code> does three things today: it runs every facet's setup, it publishes them (types served, registries composed
|
||
once, contributions in their tables), and it waits until every host-lifetime object has had its first outcome. The
|
||
promise of the whole thing, <code>pendingLoad</code>, is also what incoming holds and <code>host.hold</code> wait for. That is the startup
|
||
deadlock of chapter I.</p>
|
||
<h3 id="publication-first">Publication first</h3>
|
||
<p>The model splits <code>load</code> at the publication:</p>
|
||
<ol>
|
||
<li><strong>Setups.</strong> Every facet's setup runs. Setups only declare: <code>host.serve</code>, <code>self.contribute</code>, <code>host.references</code>.
|
||
They are synchronous and wait for nothing.</li>
|
||
<li><strong>Publication.</strong> All declarations become visible at once, in one batch: types served, registries served and
|
||
composed once, contributions in their tables. Nothing activates inside that batch.</li>
|
||
<li><strong>Wakes.</strong> After the batch, every request that waited for the publication (unbound <code>unpublished</code>) is placed, and
|
||
every unbound request for a name the publication serves is re-evaluated.</li>
|
||
<li><strong>First outcomes.</strong> The host-lifetime objects activate. Whether <code>load</code> waits for them is the barrier question below.</li>
|
||
</ol>
|
||
<p>Requests wait for step 2 only. Since the setups wait for nothing, a host always reaches step 2, and a request from
|
||
another host can never wait for this host's activations. Chapter I's knot comes apart:</p>
|
||
<figure class="fig anim"><svg viewBox="0 0 720 356" width="100%" role="img" aria-label="The same startup as in chapter I, under the model. No deadline is needed." xmlns="http://www.w3.org/2000/svg" style="font-family:'Avenir Next','Helvetica Neue',Helvetica,Arial,sans-serif;max-width:760px"><defs><marker id="f5m" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#7b7468"/></marker><marker id="f5a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#b4472f"/></marker><marker id="f5b" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#2e6a8a"/></marker></defs><line x1="188" y1="42" x2="188" y2="348" stroke="#c9bfae" stroke-dasharray="3 3"/><rect x="103" y="10" width="170" height="32" rx="4" fill="#fff" stroke="#c9bfae"/><text x="188" y="30" font-size="11" fill="#2a2723" text-anchor="middle">server S</text><line x1="532" y1="42" x2="532" y2="348" stroke="#c9bfae" stroke-dasharray="3 3"/><rect x="447" y="10" width="170" height="32" rx="4" fill="#fff" stroke="#c9bfae"/><text x="532" y="30" font-size="11" fill="#2a2723" text-anchor="middle">worker W</text><g class="step edge" style="--i:1"><path d="M188 62 h34 v14 h-32" fill="none" stroke="#7b7468" marker-end="url(#f5m)"/><text x="230" y="72" font-size="10" fill="#2a2723" text-anchor="start">setups, publication: C served</text></g><g class="step edge" style="--i:2"><line x1="188" y1="108" x2="529" y2="108" stroke="#b4472f" marker-end="url(#f5a)"/><text x="360" y="102" font-size="10" fill="#b4472f" text-anchor="middle">A needs B: hold B</text></g><g class="step edge" style="--i:3"><path d="M532 134 h-34 v14 h32" fill="none" stroke="#7b7468" marker-end="url(#f5m)"/><text x="490" y="144" font-size="10" fill="#2a2723" text-anchor="end">setups, publication: B served</text></g><g class="step edge" style="--i:4"><line x1="532" y1="180" x2="191" y2="180" stroke="#b4472f" marker-end="url(#f5a)"/><text x="360" y="174" font-size="10" fill="#b4472f" text-anchor="middle">BStore needs C: hold C</text></g><g class="step edge" style="--i:5"><line x1="188" y1="212" x2="529" y2="212" stroke="#7b7468" stroke-dasharray="4 3" marker-end="url(#f5m)"/><text x="360" y="206" font-size="10" fill="#2a2723" text-anchor="middle">bound C (S has published; C activates and goes live)</text></g><g class="step edge" style="--i:6"><path d="M532 238 h34 v14 h-32" fill="none" stroke="#7b7468" marker-end="url(#f5m)"/><text x="574" y="248" font-size="10" fill="#2a2723" text-anchor="start">BStore live, B live</text></g><g class="step edge" style="--i:7"><line x1="532" y1="284" x2="191" y2="284" stroke="#7b7468" stroke-dasharray="4 3" marker-end="url(#f5m)"/><text x="360" y="278" font-size="10" fill="#2a2723" text-anchor="middle">bound B</text></g><g class="step edge" style="--i:8"><path d="M188 310 h34 v14 h-32" fill="none" stroke="#7b7468" marker-end="url(#f5m)"/><text x="230" y="320" font-size="10" fill="#2a2723" text-anchor="start">A live; S's first pass done</text></g></svg><figcaption><button class="replay" type="button">Replay</button><span class="fignum">Figure 5.</span> The same startup as in chapter I, under the model. No deadline is needed.</figcaption></figure>
|
||
<h3 id="the-barrier-a-or-b">The barrier: (a) or (b)</h3>
|
||
<p>What is left of the barrier is the app's question "has every plugin had its first try?". R17 needs it: p3's base code
|
||
resumes the harness after <code>load</code>, so the first turn sees the tools of every plugin that loaded. Two shapes are on the
|
||
table.</p>
|
||
<p><strong>(a) Chord keeps it.</strong> <code>load</code> resolves when every host-lifetime object of this <code>load</code> has a <strong>first outcome</strong>: it is
|
||
live, failed by its own cause, or parked with no blocker that is a local object still without its first outcome. In
|
||
code, per object:</p>
|
||
<pre><code><span class="tk-k">const</span> firstOutcome = (o: <span class="tk-t">Obj</span>): boolean =>
|
||
o.live || o.ownFailure || (o.parked && o.blockers.<span class="tk-f">every</span>((b) => !b.local || <span class="tk-f">firstOutcome</span>(b)));</code></pre>
|
||
<p>So <code>CodeMode → Harness</code>, both local, waits until the harness had its outcome: code mode is parked on a local object
|
||
that is still in its first attempt, so code mode is not done. A wait on another host, or on a dependency that failed,
|
||
ends the first pass: the barrier never waits for another host, a retry, a later contribution or a reload. The signal
|
||
is separate from the readiness dependents wait for (Sol: resolving that for a parked object would let its dependents
|
||
open natives too early), and it is counted once per object, never reopened by retries. An object of the cohort that
|
||
ends before its first outcome (its plugin removed, the host closing) settles its entry as ended, and <code>host.close</code>
|
||
settles a <code>load</code> that is still waiting (Astra).</p>
|
||
<p><strong>(b) The app keeps it.</strong> <code>load</code> resolves after the publication. Chord adds <code>host.startup</code>, a watchable of the
|
||
host-lifetime objects and their first outcomes with causes; base code decides when to resume (Fable). The tracker is
|
||
the same; only who awaits it changes.</p>
|
||
<p>Three of the four reviewers recommend (a): the activation graph is Chord's, and every app that wants R17's behaviour
|
||
would otherwise build the same cohort tracker. Fable recommends (b): the policy belongs to the app, and p2 already has
|
||
three different ones. <strong>Owner</strong> decision 1.</p>
|
||
<h3 id="what-the-barrier-does-not-promise">What the barrier does not promise</h3>
|
||
<p>Neither shape can promise that every tool is there for the first turn, and the model does not pretend it can. Sol
|
||
reproduced it against today's code (<code>/tmp/barrier-sol-first-outcome.log</code>): object A's <code>open</code> fails on its first
|
||
attempt (A's first outcome); B goes live and contributes; the registry changes and <code>retryOn</code> wakes A for a second,
|
||
slow <code>open</code>; <code>load</code> resolves with only B's contribution in the table. Tools found in <code>onLive</code> (MCP) arrive later by
|
||
design. What a first turn really needs (a model, an environment, a reconciled working directory) is p3's explicit
|
||
admission check, as p2's <code>whenRegistered</code> and its harness prerequisites are today.</p>
|
||
<div class="note"><b class="tag">What p2 does today</b><p>p2 does not wait for all host objects before the first turn. Its worker (<code>apps/p2/src/hosts/worker.ts</code>) loads with
|
||
<code>awaitHostObjects: false</code>, waits for the harness, resumes Durable, tells its parent <code>control.ready()</code> and only then
|
||
awaits <code>host.ready()</code>. It has to: its subagent tools and the search indexer need server types, and the server admits
|
||
the worker's link only after <code>control.ready()</code>. A barrier before that point would deadlock, exactly as in chapter I
|
||
(Astra and Sol, independently).</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="chapter" id="reload"><header class="opener"><div class="numeral">VII</div><h2>Plugins Coming and Going</h2><p class="lede">Removals, versions and rollouts across hosts, as events the requests wait for.</p></header>
|
||
<p class="dropcap">Slice 7 brings reload and rollout, and with them the cases where a provider disappears on purpose. Today's design
|
||
handles them with their own mechanisms: <code>gone</code> when a type is removed, a final <code>contract_mismatch</code> when a version is
|
||
dropped, <code>updating</code> with a deadline during a rollout. Under the model all of them are one thing: a request becomes
|
||
unbound with a reason, and the event that fixes the reason wakes it.</p>
|
||
<h3 id="a-type-removed-and-served-again">A type removed, and served again</h3>
|
||
<p>A reload that removes a type ends its objects as today: natives close with reason <code>end</code>, callers of running calls are
|
||
rejected at once, handlers drain. What changes is what happens to the requests that pointed at those objects.</p>
|
||
<ul>
|
||
<li><strong>Over a link,</strong> the provider sends <code>failed { waiting, not_served }</code> on the bound hold. A <code>failed</code> on a bound hold
|
||
unbinds it (Fable: one message, one rule; the consumer applies it as today's <code>#startOver</code> plus <code>failed</code>). The
|
||
provider detaches the hold's sources and drops its unsent pending ops and any queued <code>bound</code>. Two things it must not
|
||
do (Sol): free the ack slots of <code>bound</code> and <code>state</code> messages already sent, which stay counted until their <code>ack</code> or
|
||
the link's loss, or resend calls, which fail and free their slots once.</li>
|
||
<li><strong>On the same host,</strong> a <code>Binding</code> whose target ended by removal becomes unbound and accepts a second <code>bind</code>. Its
|
||
member sources re-bind by name (one frame from the last value, as on a restart today), keyed pins re-pin, parked
|
||
calls flush or reject, presence lists it again. This is the one genuinely new piece of machinery in the model
|
||
(Fable, Sol): today <code>bind</code> runs once per binding, and the ended object is forgotten.</li>
|
||
</ul>
|
||
<p>When a later commit serves the name again, the publication wakes every request indexed under it, on every host that
|
||
holds one, and they bind as if nothing had happened.</p>
|
||
<h3 id="a-version-no-longer-accepted">A version no longer accepted</h3>
|
||
<p>A reload can also keep the type and stop accepting an old version (it drops <code>accepts[1]</code>). Draft 1 ended the provider
|
||
object then; Sol and Astra caught it: the object, its documents, its jobs and its holders on the current version are
|
||
fine. Only the requests on version 1 become unbound <code>version</code>. The object's own plugin reload still restarts it if it
|
||
has natives (§21), as for any reload.</p>
|
||
<h3 id="a-type-that-moved">A type that moved</h3>
|
||
<p>A plugin can move a type from the worker to the server by changing its <code>hostAddress</code>. A consumer's request carries the
|
||
address its own code computed, so the provider appearing on the server cannot wake it: the request is not waiting
|
||
there. The old host answers <code>not_served</code>; the consumer's own reload, which brings the new <code>hostAddress</code>, makes a new
|
||
request. The model adds no redirect: moving state between hosts is a deliberate migration, not something Chord guesses.</p>
|
||
<h3 id="rollouts-across-hosts">Rollouts across hosts</h3>
|
||
<p>A rollout installs a revision on the server first, then on the workers, then on the tabs (design §21). Between those
|
||
installs, hosts run different code. The model needs only one rule for that: <strong>each host knows its own install, and
|
||
nothing more.</strong></p>
|
||
<ul>
|
||
<li>A provider host that is <strong>installing</strong> keeps holds that do not fit its current code as unbound <code>installing</code>: the
|
||
consumer sees <code>loading</code>, its calls park. When the install ends, by its commit or by its rejection, the host
|
||
re-evaluates them against the code it now runs (Astra, Sol: a rejected install must not leave them asleep).</li>
|
||
<li>A provider host that is <strong>queued</strong> but not installing yet answers a mismatch with <code>failed { waiting, version }</code>
|
||
until its install commits and wakes the request. That is a visible blip of at most one install; Opus judged it
|
||
acceptable, and avoiding it would need revision fields in every answer.</li>
|
||
<li><strong>Installs never wait for activations.</strong> An install that waited for its objects to go live would deadlock a rollout
|
||
that runs in the other order: the worker's objects wait for the server's new types, the server's install waits for
|
||
the worker.</li>
|
||
<li>A host whose <strong>own</strong> install was rejected fails its own requests at once with the reason (design §21).</li>
|
||
</ul>
|
||
<div class="steps"><div class="steps-head"><span class="title">The sharing plugin's contract goes from v1 to v2; the server installs first</span><span class="count"></span><button class="prev" type="button">Back</button><button class="next" type="button">Next</button></div><div class="step-panel"><h4>Before</h4><p>S and W run revision R. W's <code>ShareStatus</code> holds the server's <code>Sharing</code> v1; T's tab holds W's <code>ShareBadge</code>. All <code>live</code>.</p>
|
||
</div><div class="step-panel"><h4>S installs R+1</h4><p>R+1 makes <code>Sharing</code> v2, with no <code>accepts[1]</code>. While S stages it, S still runs R: W's v1 hold fits the running code
|
||
and stays <code>live</code>. A hold that arrived now from a host already on R+1 (a v2 hold) would not fit R: it would wait
|
||
<code>installing</code> (<code>loading</code>, calls park) until the install ends.</p>
|
||
</div><div class="step-panel"><h4>S commits R+1</h4><p>The install ends. S re-evaluates: v1 is not served. W's hold becomes <code>failed { waiting, version: asked 1, served 2 }</code>.
|
||
<code>ShareStatus</code> stays live (R18 after live); its calls that use sharing reject at once with the cause.</p>
|
||
</div><div class="step-panel"><h4>W installs R+1</h4><p>W's new code bundles <code>Sharing</code> v2. Its own reload's handover makes the new request at v2 and releases the old one. The
|
||
new hold binds; <code>ShareStatus</code> is fully back. T's <code>ShareBadge</code> never noticed anything but one stale value.</p>
|
||
</div></div>
|
||
<h3 id="old-calls">Old calls</h3>
|
||
<p>Calls that were running when the reload came follow §21 unchanged: a pure swap lets them finish on their old handles;
|
||
a native restart drains them first; a removal rejects their callers at once and drains their handlers. Chord never
|
||
resends a call.</p>
|
||
</section>
|
||
|
||
<section class="chapter" id="cycles"><header class="opener"><div class="numeral">VIII</div><h2>Finding Cycles</h2><p class="lede">Without the deadline, the one gap left in cycle detection needs an answer: a narrowed deadline, or a probe.</p></header>
|
||
<p class="dropcap">A provider that never appears keeps its dependents parked, and that is visible: they say what they wait for. A cycle is
|
||
different: A waits for B, B for A, every provider exists, and still neither can go live first. Today Chord has two detectors and a deadline. On one host, <code>self.get</code> follows the waiting edges of
|
||
activating objects. Across hosts, a hold carries the activation chain it was made in, and a provider refuses a hold
|
||
whose chain contains the object being activated. The gap is documented (design §6): when A on the server and B on a
|
||
worker start at the same moment, the holds carry <code>[A]</code> and <code>[B]</code>, neither finds the other, and both fail at the 60-second
|
||
deadline. The model removes the deadline, so the gap must close.</p>
|
||
<h3 id="two-answers">Two answers</h3>
|
||
<p>The owner decided this gap once already. On 2026-10-24, after the slice 4b reviews, he made simultaneous starts across
|
||
hosts "a documented limitation: they time out transient and retry; no probe message." That decision assumed the
|
||
activation deadline. The model removes the deadline for dependency waits, so the decision needs one of two
|
||
continuations, and this one is his (<strong>Owner</strong> decision 6):</p>
|
||
<ul>
|
||
<li><strong>(A) The 4b decision, narrowed.</strong> One deadline stays: an attempt parked on a dependency held over a link, whose
|
||
target attempt is known and still activating or parked, fails <code>transient</code> after its <code>readyTimeout</code> and retries with
|
||
backoff; its next attempt usually starts after the other side, and the activation chain in its hold finds the cycle.
|
||
Working it through showed the catch. A target that is <em>parked</em> is the common case of a chain across hosts waiting
|
||
for a missing plugin: a tab's view on a worker object that waits for an uninstalled type. Those dependents would time
|
||
out and poll again, as today, so "local = remote" would stop at the link. Narrowing the deadline to targets that are
|
||
<code>loading</code> avoids the polling but misses a real ring: A and B on two hosts wait on each other, and B also on a missing
|
||
X. B is <code>failed { waiting }</code>, so A is too; when X appears, both stay <code>failed { waiting }</code> on each other, with no
|
||
deadline to break it and no chain to see it. Worse (Fable, round 6): an object's activation chain is fixed
|
||
when it is created and its instance id survives retries, so a retried attempt sends the same hold with the same
|
||
chain; two simultaneous starts time out and retry in lockstep and are never found. The deadline would poll without
|
||
finding what it is for.</li>
|
||
<li><strong>(B) The probe (recommended, pending review)</strong> described in the rest of this chapter: no deadline at all, one new
|
||
message kind, and the most intricate rules in the model. Five review rounds found a counterexample in each of the
|
||
first four drafts; the version below answers all of them, with one choice left in rule 5. Slice 7
|
||
would prove it with tests before the old deadline goes.</li>
|
||
</ul>
|
||
<p>The rest of the chapter describes (B): it is the answer that keeps one rule on both sides of a link, and its review
|
||
history shows what a complete answer takes.</p>
|
||
<h3 id="why-a-growing-chain-is-not-enough">Why a growing chain is not enough</h3>
|
||
<p>Draft 1 proposed letting chains grow: an activating object that receives a hold with a chain sends the grown chain on
|
||
to its own dependencies. Astra and Sol showed two flaws. Chains are cut at 32 entries today, so a ring of 33 objects
|
||
started at once is never found: the chain that reaches each object is always missing that object's own id. And a
|
||
chain records ancestry, not the current wait graph: once A stops waiting for B, B's chain still names A, and a later
|
||
wait of B on A looks like a cycle that is not there.</p>
|
||
<h3 id="b-the-probe">(B) The probe</h3>
|
||
<p>The model uses an edge-chasing probe (the idea of Chandy, Misra and Haas), for the rings that cross a link; on one host the <code>self.get</code>
|
||
walk stays, synchronous and exact, as today (Opus).
|
||
Five rounds of review shaped it; the rules below are the result.</p>
|
||
<ol>
|
||
<li><p><strong>Attempts, edges, tokens.</strong> Every attempt has its own id, minted per attempt: a retry or a reload is a new
|
||
attempt (today the id is per instance and survives retries). A <strong>wait edge</strong> runs from a parked attempt to the
|
||
activating or parked attempt it waits for, through a bound request. Unbound waits and registry reads are never
|
||
edges: they are not deadlocks, they end when a provider appears. Every edge carries an <strong>edge token</strong>: a number from a counter the
|
||
attempt keeps, never reused, replaced by a fresh one on any interruption or change of that edge (Astra, Sol: two
|
||
requests to one object are two edges): a new target attempt (also when a local
|
||
target retries without a re-bind), any <code>loading</code> on a bound or pending hold (a relay re-holding upstream included),
|
||
an unbinding, a rebinding, a re-hold after a reconnect. An attempt is parked for one interval only (once past its
|
||
dependency check it never parks again), and attempt ids are never reused. A provider tells its consumers which attempt they wait for on every answer that is not <code>bound</code>:
|
||
<code>loading { hold, attempt }</code>, and a <code>waiting</code> <code>failed { hold, cause, attempt }</code>.</p>
|
||
</li>
|
||
<li><p><strong>A search per edge change.</strong> Whenever a parked attempt gets a new edge, or a new token on one, it starts a
|
||
search: <code>probe { kind: "search", round, initiator, trail }</code>, with the next number of a counter the attempt keeps,
|
||
its own attempt id, and the trail of <code>(attempt, edge token)</code> hops crossed so far, starting with its
|
||
own. A ring closes at its last edge,
|
||
and that edge's source starts the search that finds it (Sol's simplification after Astra's counterexample: a ring
|
||
that forms only after a reconnect, with unchanged attempts, is otherwise missed). Searches are coalesced per batch.
|
||
On the same host a probe is a call on the target attempt, forwarded through a queue rather than recursion; over a
|
||
link it is a new message, <code>probe { hold, to, ... }</code>, consumer to provider, relayed by hold id with the origin.
|
||
<strong>Probes are honoured on parent and route links and dropped on child links</strong> (<code>accept</code>), whatever <code>trusted</code> says
|
||
(Opus): a tab learns attempt ids from <code>loading</code> and could otherwise break an object with a forged probe, while a
|
||
worker whose transport is untrusted must still take part. Clients serve no remote types, so no real cycle runs
|
||
through one.</p>
|
||
</li>
|
||
<li><p><strong>Forwarding.</strong> An attempt that receives a search drops it if <code>to</code> is not its current attempt or if it is not
|
||
parked. If the initiator is itself, the search came back. If the attempt is already in the trail, it drops it: a ring
|
||
that does not contain the initiator is found by its own members' searches (Opus). Otherwise it forwards it along
|
||
every wait edge, appending itself and the edge's token to the trail, but only if its round number is higher than the one it stored for
|
||
that initiator; then it stores it. A late, older round is dropped even if this attempt never saw it: its initiator
|
||
has moved on (Astra, Opus). The seen map holds one number per initiator, up to 256 initiators, and goes with the
|
||
attempt.</p>
|
||
</li>
|
||
<li><p><strong>The check.</strong> The initiator accepts a returned search while it is still parked, whatever round it carries (Astra:
|
||
a re-search must not invalidate an older search still under way); the check decides. It verifies its own hop, then
|
||
sends a
|
||
<code>probe { kind: "check", round, trail }</code> along the recorded trail: each hop passes it on only if it is still that
|
||
attempt, still parked, still holding the recorded edge token, pointing at the next hop's attempt. The check follows the
|
||
trail by position, ignores the seen map and dies on any mismatch. When it returns, the initiator verifies its own hop
|
||
once more. If the check comes
|
||
back, the initiator fails <code>broken invalid/cycle</code>, naming the path from the trail, and today's propagation breaks
|
||
the other activating objects on the ring and the dependents off it.</p>
|
||
</li>
|
||
<li><p><strong>No silent loss.</strong> Astra found the last hole: a search can reach an attempt first by a shortcut and be dropped
|
||
there when it arrives again along the ring itself; if the shortcut breaks before the check, the check dies and
|
||
nothing is left to find the ring. Round 6 tried <code>dead</code> notices sent back to the initiator. Sol and Astra showed
|
||
that reliable notices need recovery state per search that the seen map's eviction may not drop, and that a long-
|
||
parked attempt would otherwise keep for every initiator that ever passed: a leak, unless every search gets a full
|
||
lifecycle with completion and cancellation. So rule 5 is a choice, part of <strong>Owner</strong> decision 6:</p>
|
||
<ul>
|
||
<li><strong>(B1) Re-search with backoff (recommended).</strong> An attempt parked on a remote attempt starts a fresh search again
|
||
after a backoff, 1 second doubling to 60, reset by an edge change, whether it shows <code>loading</code> or <code>failed { waiting }</code>; the timer stops when the attempt leaves parking. No <code>dead</code> messages and no recovery state;
|
||
eviction only costs work; a new search starts within one backoff period, so every stable ring that
|
||
crosses a link is eventually found (no wall-clock bound: delivery is only fair). The
|
||
timer runs as long as the attempt stays parked on a remote attempt: a view on a worker object that waits for a
|
||
missing plugin re-searches every minute until a reload (Fable). The timer sends probes only: it
|
||
never fails an object, never re-holds, never polls a provider. Its cost is one probe per parked remote dependent
|
||
per period, which dies at the leaves of the downstream wait graph.</li>
|
||
<li><strong>(B2) Events only.</strong> Every search is owned until it settles: branches report completion or abort along their
|
||
trails, a dropped probe answers <code>dead { initiator, round }</code> by a reverse route, edges remember the searches they
|
||
carry until those settle, and an initiator that ends or moves on cancels its old search. Complete without any
|
||
timer, bounded by the searches in flight, and the most intricate protocol in Chord; not yet specified to the
|
||
reviewers' satisfaction.</li>
|
||
</ul>
|
||
<p>Sol's correctness reference, enumerating every simple trail without dedupe, never suppresses a ring copy, but its
|
||
work is exponential on chains of diamonds.</p>
|
||
</li>
|
||
</ol>
|
||
<figure class="fig anim"><svg viewBox="0 0 720 414" width="100%" role="img" aria-label="Two objects on two hosts start at the same moment and need each other. The probe finds it; nothing times out." xmlns="http://www.w3.org/2000/svg" style="font-family:'Avenir Next','Helvetica Neue',Helvetica,Arial,sans-serif;max-width:760px"><defs><marker id="f6m" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#7b7468"/></marker><marker id="f6a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#b4472f"/></marker><marker id="f6b" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#2e6a8a"/></marker></defs><line x1="188" y1="52" x2="188" y2="406" stroke="#c9bfae" stroke-dasharray="3 3"/><rect x="103" y="10" width="170" height="42" rx="4" fill="#fff" stroke="#c9bfae"/><text x="188" y="27" font-size="11" fill="#2a2723" text-anchor="middle">A on server S</text><text x="188" y="41" font-size="9.5" fill="#7b7468" text-anchor="middle">attempt a1</text><line x1="532" y1="52" x2="532" y2="406" stroke="#c9bfae" stroke-dasharray="3 3"/><rect x="447" y="10" width="170" height="42" rx="4" fill="#fff" stroke="#c9bfae"/><text x="532" y="27" font-size="11" fill="#2a2723" text-anchor="middle">B on worker W</text><text x="532" y="41" font-size="9.5" fill="#7b7468" text-anchor="middle">attempt b1</text><g class="step edge" style="--i:1"><line x1="188" y1="78" x2="529" y2="78" stroke="#7b7468" marker-end="url(#f6m)"/><text x="360" y="72" font-size="10" fill="#2a2723" text-anchor="middle">hold B (A activating)</text></g><g class="step edge" style="--i:2"><line x1="532" y1="110" x2="191" y2="110" stroke="#7b7468" marker-end="url(#f6m)"/><text x="360" y="104" font-size="10" fill="#2a2723" text-anchor="middle">hold A (B activating)</text></g><g class="step edge" style="--i:3"><line x1="532" y1="142" x2="191" y2="142" stroke="#7b7468" stroke-dasharray="4 3" marker-end="url(#f6m)"/><text x="360" y="136" font-size="10" fill="#2a2723" text-anchor="middle">loading { attempt: b1 }</text></g><g class="step edge" style="--i:4"><line x1="188" y1="174" x2="529" y2="174" stroke="#7b7468" stroke-dasharray="4 3" marker-end="url(#f6m)"/><text x="360" y="168" font-size="10" fill="#2a2723" text-anchor="middle">loading { attempt: a1 }</text></g><g class="step edge" style="--i:5"><line x1="188" y1="206" x2="529" y2="206" stroke="#b4472f" marker-end="url(#f6a)"/><text x="360" y="200" font-size="10" fill="#b4472f" text-anchor="middle">search { to: b1, round: 1, initiator: a1, trail: [a1·t1] }</text></g><g class="step edge" style="--i:6"><line x1="532" y1="238" x2="191" y2="238" stroke="#b4472f" marker-end="url(#f6a)"/><text x="360" y="232" font-size="10" fill="#b4472f" text-anchor="middle">forward: search { to: a1, round: 1, trail: [a1·t1, b1·t1] }</text></g><g class="step edge" style="--i:7"><path d="M188 264 h34 v14 h-32" fill="none" stroke="#7b7468" marker-end="url(#f6m)"/><text x="230" y="274" font-size="10" fill="#2a2723" text-anchor="start">search 1 came back: check the trail</text></g><g class="step edge" style="--i:8"><line x1="188" y1="310" x2="529" y2="310" stroke="#b4472f" marker-end="url(#f6a)"/><text x="360" y="304" font-size="10" fill="#b4472f" text-anchor="middle">check { round: 1, trail }: b1 still waits on a1 with token t1</text></g><g class="step edge" style="--i:9"><line x1="532" y1="342" x2="191" y2="342" stroke="#b4472f" marker-end="url(#f6a)"/><text x="360" y="336" font-size="10" fill="#b4472f" text-anchor="middle">check passes on</text></g><g class="step edge" style="--i:10"><path d="M188 368 h34 v14 h-32" fill="none" stroke="#7b7468" marker-end="url(#f6m)"/><text x="230" y="378" font-size="10" fill="#2a2723" text-anchor="start">check came back: broken invalid/cycle "A → B → A"</text></g></svg><figcaption><button class="replay" type="button">Replay</button><span class="fignum">Figure 6.</span> Two objects on two hosts start at the same moment and need each other. The probe finds it; nothing times out.</figcaption></figure>
|
||
<h3 id="why-a-reported-ring-existed">Why a reported ring existed</h3>
|
||
<p>Draft 4 confirmed a cycle with a second, independent round. Sol showed a schedule in which two rounds pass through two
|
||
different stale paths, each of them half replaced by reloads, and together report a ring that never existed at any
|
||
moment (<code>/tmp/model/sol-probe4.log</code>). The check closes that: it walks the very path the search recorded and demands
|
||
that every edge still carry the token it had when the search crossed it. A token changes on every interruption and is
|
||
never reused, and an attempt is parked for one interval only, so an edge cannot vanish and come back unnoticed (Fable and Opus
|
||
checked the argument in round 5). Every edge of the trail therefore existed from the moment the
|
||
search crossed it until the moment the check crossed it; the search crossed all of them before the check began, so
|
||
when the check began, every edge of the ring existed at once. In Sol's schedule the check dies at <code>B</code>, whose edge now
|
||
points at another attempt.</p>
|
||
<h3 id="why-every-ring-is-found">Why every ring is found</h3>
|
||
<p>Under an eventually stable wait graph and fair delivery (endless edge churn can keep invalidating searches), and with
|
||
rule 5's (B1) or (B2) for a search suppressed behind a path that broke, a ring of parked attempts is closed by its last
|
||
edge. When that edge appears, its source starts a search; every other edge of the
|
||
ring exists at that moment, and every attempt on the ring waits, directly or not, on the source, so none of them can
|
||
go live before the search and its check come back. Rings of any length are found, through local and remote edges
|
||
alike. On a graph without cycles, searches die at the leaves; on a diamond, the seen map drops the second arrival.</p>
|
||
<h3 id="what-it-does-not-promise">What it does not promise</h3>
|
||
<ul>
|
||
<li><strong>A ring broken up just after.</strong> A ring that existed when the check began may be broken up by a reload, a retry, an
|
||
unbinding or a revocation a moment later; it is still reported, because it did exist. The objects it breaks recover
|
||
as every broken object does, through <code>ref.retry()</code> or a reload of their plugin.</li>
|
||
<li><strong>Linear work under every schedule.</strong> With more than 256 initiators passing one attempt at once, an evicted entry lets
|
||
a search be forwarded again, so in an adversarial schedule the work can grow with the number of simple paths (Astra, Sol); never endlessly, since
|
||
a search never revisits an attempt already in its trail.
|
||
The seen map's memory stays bounded. Each edge change costs one search that crosses each downstream edge once,
|
||
and one check per returned search; with (B1), every attempt parked on a remote attempt adds one search per backoff
|
||
period, also while an acyclic chain only waits for a missing plugin.</li>
|
||
<li><strong>Constant message size.</strong> A probe carries its trail, so it grows with the length of the path it crossed; a search
|
||
that returns carries a trail as long as its ring. That is the price of the check; chains were cut at 32, trails are
|
||
not, because the trail is the proof.</li>
|
||
</ul>
|
||
<div class="note review"><b class="tag">Review: the state of (B)</b><p>The check's argument (a reported ring existed at the moment the check began) was confirmed by all four reviewers in
|
||
rounds 5 and 6, given exact edge identity (the tokens). Completeness under an eventually stable graph holds with
|
||
(B1); with (B2) it holds once the search lifecycle is specified, which no draft has done yet. If the owner chooses (B),
|
||
slice 7 keeps today's deadline for activations waiting across links until the probe's tests pass: long rings,
|
||
diamonds, a ring closed after a reconnect with unchanged attempts, Sol's phantom schedule, Astra's broken shortcut,
|
||
Sol's eviction schedule, 300 concurrent initiators, edges added and removed, reloads, relays, forged probes.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="chapter" id="callers"><header class="opener"><div class="numeral">IX</div><h2>Callers</h2><p class="lede">Getters, holds and calls get one rule: wait while something is under way, answer when nothing is.</p></header>
|
||
<p class="dropcap">Objects park; callers do not. A caller is code that wants an answer: a method that needs a reference now, a tool that
|
||
runs inside a turn, a tab that wants to render. The model keeps their deadlines and gives them one rule.</p>
|
||
<h3 id="host-hold"><code>host.hold</code></h3>
|
||
<p><code>host.hold</code> returns a reference at once and never rejects; only invalid params throw, also before <code>load</code>. That was
|
||
already the design; the model removes today's inconsistency: a missing local type throws after <code>load</code> but fails the
|
||
reference before it. Now a missing local type gives <code>loading</code> before the publication and <code>failed { waiting }</code> after
|
||
it, and the reference binds when a provider appears. A UI that wants to wait for something that may never come
|
||
holds it and shows its state.</p>
|
||
<h3 id="host-get-ctx-get-host-registry"><code>host.get</code>, <code>ctx.get</code>, <code>host.registry</code></h3>
|
||
<p>The getters resolve when the reference is live. While it is <code>loading</code>, or <code>failed { transient }</code> (its object's own
|
||
retry is under way), they wait, bounded by their <code>readyTimeout</code>. On <code>failed { waiting }</code> or <code>failed { broken }</code> they
|
||
reject at once. On success, rejection or timeout their temporary wait goes; a rejected getter releases its request.
|
||
<code>host.registry</code> returns the <code>RegistryValue</code>, whose watches and observers are owned as they are today; it has no
|
||
<code>release()</code>.</p>
|
||
<p>Opus's case decided the recommendation. A Durable tool runs <code>chord((ctx) => ctx.get(SharingView, { session }), ctx)</code>
|
||
while sharing is not installed. If getters waited through <code>waiting</code>, the tool, and with it the agent's turn, would
|
||
hang for 60 seconds and then fail; a call on the same reference would fail at once. With the rule above, the getter
|
||
and the call give the same answer at the same moment. The rule has a cost Opus named: a dependent whose only blocker is
|
||
an object recovering by itself (an SSH environment coming back, <code>transient</code> with its own backoff) is <code>waiting</code>, so a
|
||
getter on the dependent rejects at once. Code that wants to ride that out holds the reference instead of getting it.</p>
|
||
<div class="note review"><b class="tag">Owner decision 2: getters on <code>waiting</code></b><p>Recommended: reject at once (Opus; Fable agreed in round 2). The alternative waits through <code>waiting</code> up to
|
||
<code>readyTimeout</code>, which keeps "local = remote" just as well but makes every getter on a missing plugin cost its full
|
||
deadline.</p>
|
||
</div>
|
||
<h3 id="calls">Calls</h3>
|
||
<p>Calls on a <code>loading</code> reference park in order, each bounded by the reference's <code>readyTimeout</code> from the moment it was
|
||
made (never renewed by a reconnect or a reload). Calls on a <code>failed</code> reference reject at once with the cause. A call
|
||
already sent when its link drops, or when its object ends, fails and is never resent; its outcome is unknown, and
|
||
idempotency is the app's (a <code>requestId</code>, as p2's chat does).</p>
|
||
<h3 id="the-rule-in-one-table">The rule in one table</h3>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>The reference</th>
|
||
<th><code>host.hold</code></th>
|
||
<th>getters</th>
|
||
<th>calls</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody><tr>
|
||
<td><code>loading</code></td>
|
||
<td>returns it</td>
|
||
<td>wait, bounded</td>
|
||
<td>park, bounded</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>failed { transient }</code></td>
|
||
<td>returns it</td>
|
||
<td>wait, bounded</td>
|
||
<td>reject</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>failed { waiting }</code></td>
|
||
<td>returns it</td>
|
||
<td>reject at once</td>
|
||
<td>reject</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>failed { broken }</code></td>
|
||
<td>returns it</td>
|
||
<td>reject at once</td>
|
||
<td>reject</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>live</code></td>
|
||
<td>returns it</td>
|
||
<td>resolve</td>
|
||
<td>run</td>
|
||
</tr>
|
||
</tbody></table>
|
||
</section>
|
||
|
||
<section class="chapter" id="registries"><header class="opener"><div class="numeral">X</div><h2>Registries</h2><p class="lede">The same resolver on one host, with versions that can differ inside one revision.</p></header>
|
||
<p class="dropcap">A registry is host-local: its contributions are in-process values, often natives. Slice 8 made <code>self.registry(R)</code> a
|
||
dependency that waits for a value. The model keeps that and folds it into the one resolver.</p>
|
||
<h3 id="a-reader-39-s-request">A reader's request</h3>
|
||
<p>A reader asks for <code>R</code> at the version its own bundle was built against. The request is bound while <code>R</code> is served and
|
||
has a usable value at that version: composed, and for an older reader mapped back by the provider's
|
||
<code>accepts[old].value</code>. Otherwise it is unbound: <code>not_served</code> (no plugin serves <code>R</code> here), <code>version</code> (the provider does
|
||
not accept the reader's version, or the reader is newer), <code>compose</code> (<code>R</code> is served, but its first composition, or this
|
||
version's mapping, threw), or <code>loading</code> before the publication. The reader is woken at every transition of the value
|
||
at its version to usable. S2's <code>broken</code> for a newer reader and S3's backoff go; so does <code>#valued</code>, the latch that woke
|
||
readers only at the very first good value.</p>
|
||
<p>A registry waits for a value, never for a particular contribution: an empty table composes. "Wait until model X is
|
||
there" is the app's admission check, not a registry rule; Durable's own "model unavailable" wait covers the harness.</p>
|
||
<h3 id="versions-inside-one-host">Versions inside one host</h3>
|
||
<p>Draft 1 claimed that a registry's provider and its contributors always run the same version, because a host installs
|
||
a revision atomically. Astra corrected it: a revision is a set of plugin bundles, and a reload of the plugin that owns
|
||
the registry leaves every other plugin's contributors and readers on the version they were built against. So the
|
||
provider's adapters do real work inside one host: <code>accepts[old].contribution</code> maps an older contribution, <code>accepts[old]. value</code> maps the composition back for an older reader. A contribution without an adapter is left out and reported; a
|
||
reader without one is <code>waiting</code> with <code>version</code>.</p>
|
||
<h3 id="the-last-good-value-and-natives">The last good value, and natives</h3>
|
||
<p>Slice 8 decided that a throwing <code>compose</code> keeps the last good composition, so an extension point stays available while
|
||
its owner fixes a bug. Astra found the case where that hands out something dead: the harness contributes its native
|
||
<code>H</code>; a reader is live; the harness ends; the next composition throws for an unrelated reason; <code>.value</code> still returns
|
||
the composition that contains the closed <code>H</code>.</p>
|
||
<p>The model narrows the rule: <code>.value</code> keeps the last good composition only while every contribution it was built from
|
||
is still in the table. When one of them leaves (its contributor ended or was reloaded), the last good value is dropped
|
||
and <code>.value</code> throws until <code>compose</code> succeeds again. And <code>compose</code> represents a missing contributor as a value
|
||
(absent), never by throwing.</p>
|
||
<div class="note review"><b class="tag">Review: drop the last good value, or fence the reads</b><p>Sol would keep the last good value and instead require natives in a registry to be looked up at use through their
|
||
owner, without the guarantee. The draft takes Astra's narrower rule, because it keeps "a registry never hands out a
|
||
native whose contributor ended" without asking every contributor to wrap its natives. It narrows a decision of slice 8,
|
||
so it is the owner's: <strong>Owner</strong> decision 5.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="chapter" id="housekeeping"><header class="opener"><div class="numeral">XI</div><h2>Keep-alive, Presence, Revocation</h2><p class="lede">What a waiting request costs, who counts it, and who may end it.</p></header>
|
||
<p class="dropcap">A parked object and an unbound request are resources: they hold memory, they may keep a worker process alive, and
|
||
they act for a principal. The model says how each of those works while nothing is bound.</p>
|
||
<h3 id="keep-alive-and-presence">Keep-alive and presence</h3>
|
||
<p>A worker stays alive while <code>host.activity</code> shows work: references held over links, calls, jobs, lingering objects. A
|
||
request that arrived over a link counts once in <code>host.activity.references</code> from its arrival to its end, bound or
|
||
unbound. Without that, a session whose tab waits for a plugin that is not installed would see its worker stopped every
|
||
idle period (five minutes in p2) and restarted by the next re-hold, forever (Opus). Presence is different:
|
||
<code>host.references</code> lists only bound references, so "who is looking at these notes" counts viewers, not waiting
|
||
requests.</p>
|
||
<h3 id="revocation">Revocation</h3>
|
||
<p>Revocation ends the requests of a revoked principal, bound and unbound, at every hop. A guest whose grant is revoked
|
||
while their tab waits for a missing plugin does not get a reference later when the plugin comes back: the request is
|
||
gone, and nothing revives an ended request (no publication, no Retry, no new grant). A refused request (no authority)
|
||
is never indexed in the first place.</p>
|
||
<h3 id="gone"><code>gone</code></h3>
|
||
<p><code>gone</code> remains for one thing: revocation. A provider host that closes closes its links and sends nothing (today's
|
||
<code>#close</code>); its consumers see a link drop, <code>loading</code>, and hold again when the host comes back. A reload that removes a
|
||
type sends <code>failed { waiting }</code> instead of <code>gone</code> (chapter VII).</p>
|
||
<h3 id="parked-objects-and-their-jobs">Parked objects and their jobs</h3>
|
||
<p>A parked object with params lives while it is referenced, like any object; when its last holder releases it, it ends
|
||
and releases its requests, so a parked chain goes from the top down. A host-lifetime object parks until its plugin is
|
||
removed or the host closes. Persisted job runs of a parked host-lifetime object are loaded with its documents, before
|
||
it parks; <code>run</code> and <code>finish</code> start only once it is live, and the run count keeps the object as today.</p>
|
||
</section>
|
||
|
||
<section class="chapter" id="clock"><header class="opener"><div class="numeral">XII</div><h2>The Clock</h2><p class="lede">Which timers stay, and why each of them has no event to replace it.</p></header>
|
||
<p class="dropcap">The owner expected the model to remove almost every timeout, "except maybe <code>readyTimeout</code>". That is what it does for
|
||
dependencies. Every remaining timer bounds something no event can end: a caller's patience, an object's own code, a
|
||
silent transport, a process that may start this time.</p>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Timeout or backoff</th>
|
||
<th>After the model</th>
|
||
<th>Why</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody><tr>
|
||
<td><code>readyTimeout</code> of an attempt</td>
|
||
<td>only its own steps (natives, follow attaches, checks, derives); paused while parked</td>
|
||
<td>a slow dependency is bounded where it runs, by its own steps</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>readyTimeout</code> of <code>host.get</code>, <code>ctx.get</code>, <code>host.registry</code></td>
|
||
<td>kept</td>
|
||
<td>a caller must get an answer</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>readyTimeout</code> of a parked call</td>
|
||
<td>kept</td>
|
||
<td>an action must not run long after it was asked for</td>
|
||
</tr>
|
||
<tr>
|
||
<td>identity backoff 1 to 60 s</td>
|
||
<td>only for the object's own <code>transient</code> failures (an <code>open</code>, a follow)</td>
|
||
<td>nothing tells Chord that a disk or a network came back</td>
|
||
</tr>
|
||
<tr>
|
||
<td>dependency backoff, polling of refused holds</td>
|
||
<td><strong>removed</strong></td>
|
||
<td>a dependency's recovery is an event</td>
|
||
</tr>
|
||
<tr>
|
||
<td>dependency deadline for waits across links</td>
|
||
<td>answer (A): kept only for an attempt waiting on a remote attempt that stays activating; answer (B): kept until the probe's tests pass, then <strong>removed</strong></td>
|
||
<td>the simultaneous-cycle gap (chapter VIII)</td>
|
||
</tr>
|
||
<tr>
|
||
<td>probe re-search backoff, only with answer (B1)</td>
|
||
<td>new: 1 to 60 s while parked on a remote attempt</td>
|
||
<td>finds a ring whose search was suppressed; sends probes only, never fails or re-holds</td>
|
||
</tr>
|
||
<tr>
|
||
<td>route backoff per address</td>
|
||
<td>kept</td>
|
||
<td>a failed worker start has no event; it avoids fork storms</td>
|
||
</tr>
|
||
<tr>
|
||
<td>heartbeat 15 and 30 s</td>
|
||
<td>kept</td>
|
||
<td>the only detector of a silent transport</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>closeTimeout</code> (natives, the drain, the routine handover)</td>
|
||
<td>kept</td>
|
||
<td>bounds other code's teardown</td>
|
||
</tr>
|
||
<tr>
|
||
<td>routine and job <code>finish</code> backoffs</td>
|
||
<td>kept</td>
|
||
<td>the object's own code throwing</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>linger</code></td>
|
||
<td>kept</td>
|
||
<td>a policy for warm objects, not a wait</td>
|
||
</tr>
|
||
<tr>
|
||
<td>rollout install timeout (designed, not built)</td>
|
||
<td>a watchdog on the install only</td>
|
||
<td>never a deadline for dependencies</td>
|
||
</tr>
|
||
</tbody></table>
|
||
<p>So <code>readyTimeout</code> stays, but its meaning narrows to two things: how long a caller waits, and how long an object's own
|
||
activation steps may take. Waiting for someone else never counts against it.</p>
|
||
</section>
|
||
|
||
<section class="chapter" id="morning"><header class="opener"><div class="numeral">XIII</div><h2>A Morning, Step by Step</h2><p class="lede">The preface's Tuesday morning, played through under the model from the first click to the afternoon's rollout.</p></header>
|
||
<p class="dropcap">The rules of the earlier chapters meet in one story. Mario opens session <code>S1</code> on his phone; the server S has the catalog
|
||
and the search feed; the worker W for <code>S1</code> has not started. W's plugin set has <code>core</code>, <code>models</code>, <code>codemode</code>, <code>notes</code>
|
||
and <code>search</code>, but not <code>sharing</code>, which was removed last night; the notes view still asks for it. Later in the day a
|
||
rollout brings sharing back, with a new contract.</p>
|
||
<div class="steps"><div class="steps-head"><span class="title">Session S1 on Tuesday</span><span class="count"></span><button class="prev" type="button">Back</button><button class="next" type="button">Next</button></div><div class="step-panel"><h4>1. The tab asks for the notes</h4><p>T calls <code>host.hold(Notes, { session: "S1" })</code>. The address is <code>Session { id: "S1" }</code>, not T's own, so the request goes
|
||
over T's parent, the server. S has a route for <code>Session</code>: it resolves the address by starting W. T's reference shows
|
||
<code>loading</code>; the request waits on S's path until W's link exists.</p>
|
||
</div><div class="step-panel"><h4>2. W connects before it loads</h4><p>W connects to S first (W6), then starts <code>load</code>. The hold for <code>Notes</code> arrives before W has published its facets: it is
|
||
unbound <code>unpublished</code> and waits, still <code>loading</code> for T. Nothing is placed by a half-empty table, so the hold is not
|
||
sent back to S.</p>
|
||
</div><div class="step-panel"><h4>3. W publishes</h4><p>W's setups run, synchronously, and are published in one batch: <code>Notes</code>, <code>NoteStore</code>, <code>Harness</code>, <code>CodeMode</code>, <code>Indexer</code>
|
||
are served; <code>HarnessModels</code> and <code>HarnessExtensions</code> are served and composed once. After that batch, the waiting hold
|
||
for <code>Notes</code> is placed and binds; the host-lifetime objects start.</p>
|
||
</div><div class="step-panel"><h4>4. Code mode parks on the harness</h4><p><code>CodeMode</code> declared <code>self.get(Harness)</code>. The harness is still opening Durable, so <code>CodeMode</code> parks: <code>loading</code>, no timer.
|
||
The harness reads <code>HarnessModels</code>, which has a value (composed at the publication; the model runtime's contribution may arrive
|
||
after), finishes its own steps
|
||
and goes live. That state change wakes <code>CodeMode</code>, which passes its dependency check, opens its sandbox, goes live and
|
||
contributes its tools.</p>
|
||
</div><div class="step-panel"><h4>5. The indexer waits for the server</h4><p><code>Indexer</code> needs the server's <code>SearchFeed</code>. The hold goes over W's parent; S published long ago, so it binds at once,
|
||
<code>SearchFeed</code> is live, and <code>Indexer</code> goes live. Had S been starting at the same moment, the hold would have waited only
|
||
for S's publication, never for S's activations: the startup deadlock of chapter I cannot form.</p>
|
||
</div><div class="step-panel"><h4>6. The first pass</h4><p>With barrier (a), W's <code>load</code> resolves once <code>Harness</code>, <code>CodeMode</code> and <code>Indexer</code> had their first outcomes. <code>CodeMode</code>
|
||
waited for the local harness, so its tools are in the table; base code resumes the harness, and the first turn sees
|
||
them. With (b), base code would read <code>host.startup</code> and decide the same.</p>
|
||
</div><div class="step-panel"><h4>7. The notes view asks for sharing</h4><p><code>Notes</code>'s factory declared <code>self.get(ShareStatus)</code>, a local type of the sharing plugin. Nobody serves it on W: the
|
||
request is unbound <code>not_served</code>, indexed under the name. <code>Notes</code> parks <code>failed { waiting, "waiting for ShareStatus: not served on Session S1" }</code>. T's reference shows the same; a call from T rejects at once with that cause. No backoff, no
|
||
polling, no 60-second wait.</p>
|
||
</div><div class="step-panel"><h4>8. The phone changes networks</h4><p>T's link to S drops. T's reference goes <code>loading</code>: its request stays, its last value stays readable, a call made now
|
||
parks. A call that was in flight fails and is not resent. T reconnects; the request is held again on the new link; W
|
||
answers as before: <code>failed { waiting }</code>.</p>
|
||
</div><div class="step-panel"><h4>9. Sharing comes back, with a new contract</h4><p>In the afternoon a rollout brings the sharing plugin back at revision R+1, where <code>Sharing</code> on the server is v2. S
|
||
installs first and serves <code>Sharing</code> v2. W's install is queued; nothing on W asks for <code>Sharing</code> yet, because the sharing
|
||
plugin is not installed there, so nothing changes for W. (Had W still run a sharing facet built against v1 without an
|
||
adapter on S, its requests would show <code>failed { waiting, version }</code> until W installed: the ordinary window of a
|
||
server-first rollout.)</p>
|
||
</div><div class="step-panel"><h4>10. W installs R+1</h4><p>W's install commits: a publication. It serves <code>ShareStatus</code> again: the index wakes <code>Notes</code>'s request, which binds.
|
||
<code>ShareStatus</code> activates, holds the server's <code>Sharing</code> at v2 (W's new code bundles v2), goes live, and <code>Notes</code> passes its
|
||
dependency check, opens what it needs and goes live. T's reference becomes <code>live</code> without T having done anything.</p>
|
||
</div></div>
|
||
<p>What did not happen is the point. Nothing waited for a deadline; nothing retried on a timer; no reference ended
|
||
because a plugin was missing; no host waited for another host's activations; and every waiting object said what it
|
||
was waiting for.</p>
|
||
</section>
|
||
|
||
<section class="chapter" id="sc-startup"><header class="opener"><div class="numeral">XIV</div><h2>Scenarios: Starting Up</h2><p class="lede">Twenty startups on p2's real hosts, under the model, with the barrier as (a) and as (b).</p></header>
|
||
<p class="dropcap">These scenarios, from Astra, follow p2's real hosts: the server alone, workers started by a tab or by another worker,
|
||
web tabs, the TUI, restarts in the middle. Each row says what the model does, with the barrier in both shapes where
|
||
they differ, and what today's code does. The pattern across all of them: publication never waits for activation, so
|
||
dependency traffic between hosts always moves; what differs between (a) and (b) is only when base code is told that
|
||
the first pass is over, and neither shape promises every tool before the first turn.</p>
|
||
<p>The Rule column cites sections of the working document, reproduced in appendix C.</p>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>#</th>
|
||
<th>Scenario</th>
|
||
<th>Under the model</th>
|
||
<th>Today</th>
|
||
<th>Rule</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody><tr>
|
||
<td>1</td>
|
||
<td>The server starts alone. Catalog, ModelConfig, LoginService and ForwardStore initialize before any session worker exists.</td>
|
||
<td>Their local references are <code>loading</code> during initialization, then <code>live</code> or <code>failed</code> with an own cause. Under (a), <code>load</code> waits for these first outcomes; under (b), it returns after publication. Public admission still needs explicit security prerequisites; a completed first pass is not proof that every service works.</td>
|
||
<td>p2 starts its HTTP listener but gates requests and upgrades behind plugin startup (<code>apps/p2/src/hosts/server.ts</code>). Current next waits for eager objects' first live/failed outcomes (<code>host/host.ts:load</code>).</td>
|
||
<td>§§1–3, 6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
<td>A tab's request reaches the server before its first facet publication. Catalog is declared by a later setup in that same load.</td>
|
||
<td>The request stays <code>loading</code> behind the publication gate. The complete declaration batch places it; Catalog activation then produces <code>bound</code> and the reference becomes <code>live</code>. Neither (a) nor (b) delays that hold until unrelated eager objects finish.</td>
|
||
<td>Current next's <code>Hold.start</code> waits for <code>pendingLoad</code>, which includes every eager object's first outcome (<code>remote/hold.ts</code>). It waits longer than declaration safety requires.</td>
|
||
<td>§§1, 4, 6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
<td>Several server facets set up successfully, but the last setup throws. A tab is already waiting for Catalog.</td>
|
||
<td>No partial declaration batch may activate objects. Load fails and the host closes; local held references become <code>ended</code>, while the tab sees a link drop and remains <code>loading</code>. A replacement server's successful publication can satisfy the tab's retained request.</td>
|
||
<td>Current next closes the host on a throwing setup and activates no objects (<code>host/host.ts:load</code>). Setup failure rejects load directly.</td>
|
||
<td>§§1, 6, 10</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
<td>A session worker's Harness opens slowly. Its CodemodeRuntime startup object declares Harness and contributes tools only after its own sandbox opens.</td>
|
||
<td>Harness and CodemodeRuntime start <code>loading</code>; CodemodeRuntime waits without a dependency timer. Harness becoming <code>live</code> wakes CodemodeRuntime, which opens its sandbox and becomes <code>live</code>. Under (a), this local chain finishes before load returns and resume; under (b), the application must await the same startup outcomes or risk a first turn without those tools.</td>
|
||
<td>p2 resumes after Harness is ready, before <code>host.ready()</code> (<code>apps/p2/src/hosts/worker.ts</code>); CodemodeRuntime depends on Harness (<code>plugins/codemode/worker.ts</code>). Current next's load barrier waits for both first outcomes.</td>
|
||
<td>§§3, 6, 11</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
<td>A tab opens Chat for a stopped session. The server starts that session worker, whose Search Indexer needs server SearchFeed.</td>
|
||
<td>Chat remains <code>loading</code> while the route starts the worker. The worker connects and publishes before its application barrier; SearchFeed can bind immediately on the already-published server. Indexer becomes <code>live</code> when its prerequisites are ready, and Chat receives <code>bound</code> once its own prerequisites are ready. Under (a), a still-remote startup blocker can already count as a parked first outcome; neither option guarantees that every remote-backed feature precedes resume.</td>
|
||
<td>p2 connects IPC first but the parent admits it only after <code>control.ready()</code>; Harness resumes before the later <code>host.ready()</code> (<code>src/hosts/worker.ts</code>). Current next gates incoming holds on whole-host load (<code>remote/hold.ts</code>).</td>
|
||
<td>§§1, 3–4, 6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
<td>A running worker calls ConversationBroker on the server. The broker sends to another session whose worker is stopped.</td>
|
||
<td>Only the second worker starts a new publication and first-pass sequence. Its PeerConversation reference is <code>loading</code> until that view binds; the first worker and broker remain <code>live</code>. Under (a), the new worker's scheduler waits for its first-pass report; under (b), its application must supply that admission rule. This call does not reopen the first worker's startup barrier.</td>
|
||
<td>p2 gets PeerConversation inside the broker's call and starts its worker through the session route (<code>plugins/conversations/server.ts</code>, <code>src/runtime/server.ts</code>). The spawned worker uses p2's Harness-before-host-ready ordering.</td>
|
||
<td>§§3, 6, 8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
<td>Many tabs open the same stopped session while other tabs open several different sessions.</td>
|
||
<td>Requests to one worker share route resolution and provider identities where their principals match. Each tab's reference starts <code>loading</code> and becomes <code>live</code> on its own binding. Both barrier options remain per host; a slow worker cannot hold back every other worker's first turn. The application shows per-session startup status, not one global ready flag.</td>
|
||
<td>Current next shares resolution per route address (<code>host/route.ts</code>). p2's SessionHosts also serializes starts per session (<code>apps/p2/src/runtime/server.ts</code>).</td>
|
||
<td>§§1, 6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
<td>During server startup, an eager Bootstrap object needs a worker's Agent. That worker's eager FeedOwner needs the server's independent Catalog.</td>
|
||
<td>Server and worker publication happen independently of their first-pass reports. Catalog binds while Bootstrap is still <code>loading</code>; that wakes FeedOwner, then Agent and Bootstrap can become <code>live</code>. Both (a) and (b) avoid a host-ready gate on this dependency traffic. The acyclic object graph needs no timeout to make progress.</td>
|
||
<td>Current next can deadlock its whole-host load gates until activation deadlines fail attempts: the Agent hold waits for worker load, while the Catalog hold waits for server load (<code>host/host.ts</code>, <code>remote/hold.ts</code>). This is an architectural example using p2-style server/worker dependencies, not a claim that p2 has a Bootstrap plugin.</td>
|
||
<td>§§1, 4, 6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>9</td>
|
||
<td>The worker has a healthy Harness, but its <code>p2.subagent-tools</code> startup object still waits for server Subagents. The first generation is ready to run locally.</td>
|
||
<td><code>p2.subagent-tools</code> remains <code>loading</code> while its remote reference binds, or <code>failed</code> with <code>waiting</code> if that provider is unavailable. Under (a), the remote wait can settle its first outcome without its contribution; under (b), publication returns even earlier. Server <code>bound</code> later wakes <code>p2.subagent-tools</code> and publishes its tools for future phases. If the first turn requires those tools, application admission must wait explicitly.</td>
|
||
<td>p2 <code>p2.subagent-tools</code> declares both Harness and server Subagents (<code>plugins/subagents/worker.ts</code>). Its worker resumes Harness before admitting the upstream-dependent host objects (<code>src/hosts/worker.ts</code>).</td>
|
||
<td>§§2–4, 6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>10</td>
|
||
<td>In a p3 port, the core serves a Models registry whose empty composition is valid. The optional models contributor is absent when the worker starts.</td>
|
||
<td>The registry is usable, so this absence alone does not park Harness; Harness can become <code>live</code>. Neither (a) nor (b) waits for an uninstalled contributor. The generation's model selection or application admission must report model unavailable and wait for a contribution. Registry publication later wakes that domain-level wait; it is not a missing-provider activation failure.</td>
|
||
<td>p2 instead awaits the <code>HarnessConfig</code> default entry inside Harness's Durable opener (<code>src/runtime/worker.ts:serveHarness</code>). Without the models contribution, that opener does not finish normally.</td>
|
||
<td>§§3, 6, 9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>11</td>
|
||
<td>A session is configured for SSH. Harness's execution opener finds that the environment is invalid or cannot open.</td>
|
||
<td>Harness becomes <code>failed</code> with its own <code>transient</code> cause, or <code>broken</code> if the opener marks the error permanent. Dependents still before their readiness check become <code>failed</code> with <code>waiting</code>. Under (a), this is a first outcome, not a healthy Harness; under (b), load already returned but resume still requires a usable Harness. Effect retry or a configuration-triggered retry can later make it <code>live</code>; no local-execution fallback is implied.</td>
|
||
<td>p2 opens the session machine during Harness activation and fails rather than silently using the local machine (<code>src/runtime/worker.ts</code>). Worker startup failures are surfaced and can require explicit retry (<code>src/runtime/server.ts</code>).</td>
|
||
<td>§§2–3, 6, 11</td>
|
||
</tr>
|
||
<tr>
|
||
<td>12</td>
|
||
<td>Prompt becomes live before its initial local instruction-file read finishes. A user immediately sends the first message.</td>
|
||
<td>Prompt's reference can be <code>live</code> while its file-loading work continues. Under (a), its InputGate contribution is present before a normal local first pass completes; the gate still controls when local input may proceed. Under (b), the application must ensure that contribution exists before admitting input. The model does not turn background file loading into a host-wide barrier.</td>
|
||
<td>p2 contributes InputGates and awaits the initial local load in its prompt sections; remote instructions deliberately load in the background (<code>plugins/prompt/worker.ts</code>). Object readiness is already distinct from instruction readiness.</td>
|
||
<td>§§3, 6, 9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>13</td>
|
||
<td>Search starts an archived session worker only to rebuild its historical index. Indexer has a long scan to perform.</td>
|
||
<td>Once SearchFeed and Harness are usable, Indexer becomes <code>live</code> even while its <code>scanned</code> state is false. Both (a) and (b) leave scan completion to the explicit IndexScan operation. The archived-worker application keeps Harness scheduling paused, waits for the scan result and then stops the worker. A startup report does not override that resume policy.</td>
|
||
<td>p2 passes <code>resume:false</code> for archived index-only starts, exposes <code>IndexScan.scanned</code>, and stops the archived worker after rebuilding (<code>plugins/search/server.ts</code>, <code>plugins/search/worker.ts</code>, <code>src/hosts/worker.ts</code>).</td>
|
||
<td>§§3, 6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>14</td>
|
||
<td>After a worker starts, a tab opens Workspace for a large directory. Its filesystem watcher is slow to initialize.</td>
|
||
<td>The on-demand Workspace reference is <code>loading</code> until the backend is usable, then <code>live</code>. Its background watch may still be starting. Neither barrier option waits for every future Workspace view or directory watch; a component that needs a complete initial scan must use explicit domain readiness.</td>
|
||
<td>p2 deliberately returns the Workspace backend before installing a potentially slow watcher (<code>plugins/workspace/worker.ts</code>). This work is not a universal worker-start prerequisite.</td>
|
||
<td>§§3, 6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>15</td>
|
||
<td>A worker restores durable work that can resume immediately. No new Chat.send call occurs, so there is no incoming-input gate to intercept it.</td>
|
||
<td>Harness opens with scheduling paused and can become <code>live</code> before all startup contributors. Under (a), base code resumes after the first-pass report and required application checks; under (b), base code must await equivalent outcomes itself. Input gates on new messages do not guard this replay. Each phase pins the contributions present when that phase begins.</td>
|
||
<td>p2 explicitly calls Harness.resume before <code>host.ready()</code> (<code>src/hosts/worker.ts</code>). Its Durable registry adapter snapshots the extension table per phase/version (<code>src/runtime/durable.ts</code>).</td>
|
||
<td>§§6, 9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>16</td>
|
||
<td>A previously loaded web tab starts while the server is offline. Its local UI facets can render an offline shell.</td>
|
||
<td>Publication of the tab's local facets does not require the server. Remote references stay <code>loading</code> on the absent path. Under (a), remote blockers can count as parked first outcomes; under (b), declaration publication returns directly. The shell renders, and later connection plus <code>bound</code> messages make the affected views <code>live</code>.</td>
|
||
<td>p2 starts reconnection before facet installation and renders React without awaiting a whole-host ready promise (<code>src/hosts/web/main.tsx</code>). It already supports an offline shell after the page has been loaded.</td>
|
||
<td>§§1–2, 6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>17</td>
|
||
<td>A TUI candidate loads before its launcher activates BridgeLink. A startup UI object declares a server reference, whose outgoing frames are buffered.</td>
|
||
<td>The reference remains <code>loading</code> on a remote dependency. Under (a), that remote wait is a parked first outcome, so candidate preparation can finish; under (b), publication alone finishes. Launcher activation flushes the frames, and <code>bound</code> wakes the UI object. Do not replace this dependency declaration with an own native opener that waits indefinitely for launcher activation.</td>
|
||
<td>p2's setupTui awaits facet load while BridgeLink buffers frames until <code>activate</code> (<code>src/hosts/tui-worker-runtime.ts</code>). Adding an eager remote-dependent host object under the old full-ready load rule could stall candidate preparation until a deadline.</td>
|
||
<td>§§1, 3, 6, 11</td>
|
||
</tr>
|
||
<tr>
|
||
<td>18</td>
|
||
<td>A tab starts a worker, but the process crashes while Harness is still opening. The server launches a replacement.</td>
|
||
<td>The tab's Chat reference remains <code>loading</code> across the lost path and replacement startup. The replacement has its own publication and first-pass cohort; successful binding makes Chat <code>live</code>. Both (a) and (b) must keep transport/start failure distinct from a healthy startup report. A process manager may surface a startup error and require user retry rather than retrying forever.</td>
|
||
<td>p2 records worker startup failures and has an explicit retry path; it does not treat every failed boot as an automatically healing object (<code>src/runtime/server.ts:SessionHosts</code>). Current next's route layer independently retries failed resolution (<code>host/route.ts</code>).</td>
|
||
<td>§§1–2, 6, 11</td>
|
||
</tr>
|
||
<tr>
|
||
<td>19</td>
|
||
<td>The server restarts while a worker is still starting. A detached terminal host already owns running PTYs.</td>
|
||
<td>Tabs see their server and worker references become <code>loading</code>; new hosts publish and bind them again. The application may stop old workers on parent loss, while the detached PTY owner survives. The old server's unfinished first-pass wait settles on close (its entries end). New startup reports must not wait for the detached PTYs to restart.</td>
|
||
<td>p2 workers stop on IPC disconnect, while the terminal driver detaches its socket without killing the detached PTYs (<code>src/hosts/worker.ts</code>, <code>plugins/terminals/detached.ts</code>). Current next settles ended objects through its existing load settlement path.</td>
|
||
<td>§§1, 6, 10</td>
|
||
</tr>
|
||
<tr>
|
||
<td>20</td>
|
||
<td>A startup contributor in a v2 port reads a keyed configuration document from a local store. The store is live, but the first requested key still needs hydration.</td>
|
||
<td>The contributor remains <code>loading</code> until that pin is ready; store-live alone does not make the dependency usable. Under (a), this local key-loading work remains part of the first pass, then the contributor can become <code>live</code> and publish before resume. Under (b), publication returns earlier, so the application must await that readiness if the first turn needs the contribution.</td>
|
||
<td>Current next loads dependency pins before opening the consumer's natives and before its settled promise completes (<code>host/attempt.ts:pinKeys</code>). Its activation deadline currently also bounds this wait. This is a v2-port example, not an existing p2 keyed API.</td>
|
||
<td>§§3, 6, 11</td>
|
||
</tr>
|
||
</tbody></table>
|
||
</section>
|
||
|
||
<section class="chapter" id="sc-refs"><header class="opener"><div class="numeral">XV</div><h2>Scenarios: References, Holds and Calls</h2><p class="lede">Getters, holds, calls, link drops, relays, release and revocation, played through.</p></header>
|
||
<p class="dropcap">Fable's scenarios use p2's names: <code>Catalog</code> and <code>Hosts</code> (the sessions plugin, on the server), <code>Sessions</code> (on the
|
||
server, called from worker tools), <code>SessionTransfer</code> (a worker type the server calls for a fork), <code>Notes { session }</code>
|
||
(a worker type a tab holds), <code>NoteStore</code> (local on the worker), <code>ForwardStore</code> (ports, on the worker) and <code>Forwards</code>
|
||
(radius, on the server), and <code>Harness</code> (host-lifetime, on the worker). S is the server, W1 the worker of session S1, T
|
||
a web tab. The pattern: a request waits where something is under way and answers where nothing is; nothing a holder
|
||
does is needed to recover; and an ended holder leaves nothing behind in any index.</p>
|
||
<p>The Rule column cites sections of the working document, reproduced in appendix C.</p>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>#</th>
|
||
<th>Scenario</th>
|
||
<th>Under the model</th>
|
||
<th>Today</th>
|
||
<th>Rule</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody><tr>
|
||
<td>1</td>
|
||
<td><code>self.get(NoteStore)</code> in <code>Notes { S1 }</code>'s factory on W1; <code>NoteStore</code> is served and activating.</td>
|
||
<td><code>Notes</code> makes its request, it binds to the <code>NoteStore</code> object, <code>Notes</code> parks <code>loading</code>. <code>NoteStore</code> goes live: a target state change wakes <code>Notes</code>, which opens its natives and goes live. No timer ran.</td>
|
||
<td>The same wait, bounded by <code>readyTimeout</code>; past it <code>Notes</code> fails <code>transient</code> and backs off (<code>attempt.ts</code> <code>waitFor</code>).</td>
|
||
<td>§3.1, §3.4, §4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
<td><code>self.get(Catalog)</code> from W1's <code>ForwardStore</code>; the parent link is up, S has published, <code>Catalog</code> is served there.</td>
|
||
<td>The request goes over the parent (<code>hostAddress: () => Server()</code>); S binds the hold; <code>bound</code> arrives; <code>ForwardStore</code> was <code>loading</code>, now goes on and live.</td>
|
||
<td>The same, after S's whole barrier (<code>Hold.start</code> awaits <code>pendingLoad</code>).</td>
|
||
<td>§1, §2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
<td>The same, but <code>Catalog</code>'s plugin is not installed on S.</td>
|
||
<td>S indexes the hold <code>not_served</code> under "p2.catalog" and answers <code>failed { waiting, not_served }</code>; <code>ForwardStore</code>'s reference shows <code>failed { waiting }</code>, the object parks <code>failed { waiting, "waiting for p2.catalog: not served" }</code>. A reload on S serving <code>Catalog</code> re-runs the hold: <code>loading</code>, <code>bound</code>; the reference goes live, the dependent wakes. No backoff, no new hold.</td>
|
||
<td>The hold is refused <code>broken</code>; <code>ForwardStore</code> fails <code>transient</code> and re-holds every 1-60 s, refused each time (<code>remote/hold.ts</code> <code>#refuse</code>, <code>object.ts</code> backoff).</td>
|
||
<td>§1, §3.1, §4, §5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
<td><code>self.get(SessionInfo)</code> on W1 when the core facet is missing (same host, not served).</td>
|
||
<td>The request is unbound <code>not_served</code> on W1 itself; the dependent parks <code>failed { waiting }</code>; a reload serving the type binds it and wakes the dependent.</td>
|
||
<td><code>#assertRoute</code> throws in the factory: the object is <code>broken</code> until <code>retry()</code> or a reload (<code>host.ts</code> <code>reference</code>).</td>
|
||
<td>§1, §3.1, §4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
<td>A tab's <code>NotesView</code> declares <code>Notes { S1 }</code>; S routes <code>Session</code>, <code>resolve</code> forks W1, W1 connects and publishes.</td>
|
||
<td>The hold waits at T on its path (<code>no_link</code> while S resolves: <code>loading</code>), is relayed to W1, waits there <code>unpublished</code> until W1's setups, then binds; <code>bound</code> comes back through the relay. <code>NotesView</code> was <code>loading</code> the whole time and goes live.</td>
|
||
<td>The same chain of waits, each bounded by <code>readyTimeout</code>; a slow worker start past it fails the tab object <code>transient</code> and polls.</td>
|
||
<td>§1, §6.2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
<td><code>host.get(Notes, { session }, ctx)</code> from a tab's page code while W1 is starting.</td>
|
||
<td>The getter waits while the reference is <code>loading</code>, up to its <code>readyTimeout</code>, then rejects with the cause and releases its request. It resolves as soon as <code>bound</code> arrives.</td>
|
||
<td>The same.</td>
|
||
<td>§8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
<td><code>host.get(Catalog, {}, ctx)</code> on W1 when S does not serve <code>Catalog</code>.</td>
|
||
<td>The reference becomes <code>failed { waiting }</code> at the first answer; the getter rejects at once with "not served on server" (recommended; owner's choice to wait instead).</td>
|
||
<td>Rejects at once too (a broken failure), after the refused hold's <code>failed</code>.</td>
|
||
<td>§8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
<td><code>host.hold(Catalog, {}, ctx)</code> on W1 before W1's <code>load</code> ran.</td>
|
||
<td>The reference is returned at once, <code>loading</code> (<code>unpublished</code>); W1's publication places it over the parent; it follows the object for its whole life: <code>loading</code>, <code>live</code>, <code>failed { waiting }</code> if S drops the plugin, <code>live</code> again when it comes back.</td>
|
||
<td>Returned at once; placed once <code>load</code> resolved (<code>host.ts</code> <code>#hold</code> races <code>load</code>); a removal ends it for good with <code>gone</code>.</td>
|
||
<td>§1, §8, §5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>9</td>
|
||
<td><code>host.hold(Missing, {}, ctx)</code> for a local type nobody serves here.</td>
|
||
<td><code>loading</code> before the publication, <code>failed { waiting, not_served }</code> after it; a reload serving it binds the same reference.</td>
|
||
<td>Throws <code>unavailable</code> at once after <code>load</code>; before <code>load</code>, fails the reference broken once it resolved.</td>
|
||
<td>§8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>10</td>
|
||
<td><code>await host.registry(HarnessModels, ctx)</code> by base code when the models plugin is missing.</td>
|
||
<td>Rejects at once with <code>unavailable/undeclared</code> (<code>waiting</code>); code that wants to wait declares the registry in an object or holds. A served registry whose first <code>compose</code> threw rejects the same way (<code>compose</code>).</td>
|
||
<td>Rejects at once (slice 8 build).</td>
|
||
<td>§8, §9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>11</td>
|
||
<td><code>ctx.get(Sessions, {})</code> inside a worker tool through <code>host.root</code> (<code>plugins/sessions/worker.ts</code>) while the parent link is down.</td>
|
||
<td>The request waits on the path (<code>no_link</code>, <code>loading</code>); the getter waits up to <code>readyTimeout</code>; the link comes back, the path re-holds, <code>bound</code>, the getter resolves. The request ends when the tool call settles.</td>
|
||
<td>The same (<code>ctx.ts</code> <code>CallContext.get</code>, <code>path.ts</code> re-hold).</td>
|
||
<td>§1, §8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>12</td>
|
||
<td>A call <code>notes.add(args, ctx)</code> on a reference whose object is <code>loading</code> (W1 restarting).</td>
|
||
<td>The call parks in the reference's queue with its own <code>readyTimeout</code>; <code>bound</code> flushes the queue in order; past the deadline it rejects <code>unavailable/park_timeout</code>.</td>
|
||
<td>The same (<code>host/parked.ts</code>).</td>
|
||
<td>§8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>13</td>
|
||
<td>A call on a reference that is <code>failed { waiting }</code> (the plugin is not there).</td>
|
||
<td>Rejects at once with the cause, no parking: nothing is under way.</td>
|
||
<td>Rejects at once (the reference is failed).</td>
|
||
<td>§2, §8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>14</td>
|
||
<td>A call already sent to W1 when the IPC link drops.</td>
|
||
<td>The call fails <code>unavailable</code> and is never resent (its outcome is unknown); the reference goes <code>loading</code>; calls made after the drop park until the re-hold's <code>bound</code>.</td>
|
||
<td>The same (<code>consumer.ts</code> link close).</td>
|
||
<td>§2, §5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>15</td>
|
||
<td>The parent link drops while <code>ForwardStore</code> is parked <code>loading</code> on <code>Catalog</code> (S restarting).</td>
|
||
<td>The request stays on the path (<code>no_link</code>), <code>ForwardStore</code> stays <code>loading</code>; the reconnect holds it again, S answers <code>loading { attempt }</code> or <code>bound</code>; the dependent wakes on the state change. No deadline on the wait.</td>
|
||
<td>The dependent stays <code>loading</code> until <code>readyTimeout</code>, then <code>transient</code> with backoff (the audit's choice 6).</td>
|
||
<td>§1, §3.3, §4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>16</td>
|
||
<td>A route link to W1 drops while T holds <code>Notes { S1 }</code> through S's relay.</td>
|
||
<td>The relay sends <code>loading</code> downstream, rejects relayed calls in flight <code>unavailable</code>, re-resolves the address with the route backoff, holds again upstream with the kept request; <code>bound</code> is forwarded. The tab's dependents stay <code>loading</code>.</td>
|
||
<td>The same (<code>relay.ts</code>); the route backoff stays.</td>
|
||
<td>§1, §4, §11</td>
|
||
</tr>
|
||
<tr>
|
||
<td>17</td>
|
||
<td>T's own link drops while its hold for <code>Notes { S1 }</code> is still unbound at W1 (<code>not_served</code>).</td>
|
||
<td>S sends <code>closed { origin }</code> upstream; W1 drops the indexed hold with the origin: no leak. T reconnects and holds anew.</td>
|
||
<td>The same for refused holds (<code>provider.ts</code> origin close).</td>
|
||
<td>§1, §10</td>
|
||
</tr>
|
||
<tr>
|
||
<td>18</td>
|
||
<td>A tab closes while <code>NotesView</code> is parked on <code>Notes { S1 }</code>.</td>
|
||
<td>Its last holder releases: a parked object ends at once (it is not live, so no <code>linger</code>); its requests end, the relayed hold is released upstream, the index entries go. Nothing keeps the chain.</td>
|
||
<td>The same (<code>object.ts</code> <code>#idle</code>).</td>
|
||
<td>§1, §3.5, §10</td>
|
||
</tr>
|
||
<tr>
|
||
<td>19</td>
|
||
<td><code>host.revoke({ grant })</code> while a guest's objects on W1 are parked and its tab's holds wait unbound.</td>
|
||
<td>Every request of the principal ends, bound and unbound, at every hop: the parked objects end <code>forbidden/revoked</code>, the tab gets <code>gone</code> for each hold, the child link closes. Nothing revives them; a new login holds anew.</td>
|
||
<td>Bound objects end, holds waiting for <code>load</code> are ended (<code>host.ts</code> <code>revokeWaiting</code>); refused holds are released with the link.</td>
|
||
<td>§10</td>
|
||
</tr>
|
||
<tr>
|
||
<td>20</td>
|
||
<td>A worker with nothing live: a tab holds <code>Notes { S1 }</code> but the notes plugin is missing on W1.</td>
|
||
<td>The unbound hold counts in <code>host.activity.references</code> from its arrival, so the supervisor does not stop W1 while the tab waits; it is not in <code>host.references</code> (presence lists bound references only).</td>
|
||
<td>A refused hold is not counted: the worker stops idle, restarts on the next hold, refuses again.</td>
|
||
<td>§10</td>
|
||
</tr>
|
||
<tr>
|
||
<td>21</td>
|
||
<td>S holds <code>SessionTransfer</code> on W1 for a fork while W1's <code>Harness</code> is parked on the models registry (not served).</td>
|
||
<td>The hold binds to <code>SessionTransfer</code>, whose attempt parks on <code>Harness</code>: <code>loading</code>, then <code>failed { waiting, "waiting for p2.harness: waiting for registry …" }</code>, with the chain in the message; S's <code>ctx.get</code> rejects at once with it. A reload on W1 serving the models plugin wakes the harness, then <code>SessionTransfer</code>; a later <code>ctx.get</code> binds.</td>
|
||
<td><code>SessionTransfer</code> fails <code>transient</code> after <code>readyTimeout</code>, polls; S's getter rejects at its own deadline.</td>
|
||
<td>§3.1, §3.4, §8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>22</td>
|
||
<td><code>ref.retry()</code> from a tab's Retry button on a <code>failed { waiting }</code> reference whose blocker on W1 is <code>broken</code> (a throwing factory).</td>
|
||
<td>The retry passes over the link (<code>retry { hold }</code>) to the parked object, which passes it to its <code>broken</code> blocker: a new attempt; <code>waiting</code> blockers are passed through, <code>not_served</code> ones untouched. If the blocker goes live, the chain wakes.</td>
|
||
<td><code>retry</code> reaches only the object the hold is on; a refused hold is started again.</td>
|
||
<td>§3.7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>23</td>
|
||
<td>Keyed pin: <code>Notes</code> pins <code>store.drafts.at(key)</code> on <code>NoteStore</code>; the key's load fails at the provider.</td>
|
||
<td>The pin is a dependency: <code>Notes</code> parks <code>failed { waiting on p2.note-store.drafts[key] }</code>; the provider's re-bind of that member (a retry) wakes it.</td>
|
||
<td><code>dependency_failed</code> transient with backoff (<code>attempt.ts</code> <code>pinKeys</code>).</td>
|
||
<td>§3, §4</td>
|
||
</tr>
|
||
</tbody></table>
|
||
</section>
|
||
|
||
<section class="chapter" id="sc-providers"><header class="opener"><div class="numeral">XVI</div><h2>Scenarios: Providers, Versions, Registries</h2><p class="lede">Plugins missing, added, removed and broken; versions that do not fit; registries and the harness.</p></header>
|
||
<p class="dropcap">Opus's scenarios cover the provider side: plugins that are missing, arrive by a reload, are removed or break; versions
|
||
that fit through <code>accepts</code> or do not; registries with missing providers, skewed versions and last good values;
|
||
<code>self.follow</code> over a harness that restarts; and reloads of an object's own code. The pattern: what used to be <code>broken</code>
|
||
or polled becomes <code>waiting</code>, woken by the one event that can fix it, and an object's own code is still the only thing
|
||
that breaks it.</p>
|
||
<p>The Rule column cites sections of the working document, reproduced in appendix C.</p>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>#</th>
|
||
<th>Scenario</th>
|
||
<th>Under the model</th>
|
||
<th>Today</th>
|
||
<th>Rule</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody><tr>
|
||
<td>1</td>
|
||
<td>Worker W1 has no drafts plugin. Chat (chat plugin, on W1) declares <code>self.get(DraftStore)</code>.</td>
|
||
<td>Chat's request is unbound <code>not_served</code> and indexed under <code>DraftStore</code>. Chat is <code>failed { waiting, "waiting for DraftStore: not served" }</code>, with no timer. A tab's Panel holding Chat shows the same cause, and its calls reject at once.</td>
|
||
<td><code>#assertRoute</code> throws in the factory, so Chat is <code>broken</code>. Only <code>ref.retry()</code> or a reload recovers it (<code>host.ts</code>).</td>
|
||
<td>§1, §2, §3.1</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
<td>A reload adds the drafts plugin to W1 while Chat waits (1).</td>
|
||
<td>The reload's commit publishes <code>DraftStore</code>. The index wakes Chat's request, which binds; DraftStore activates and goes live. Chat leaves parking, opens its natives and goes live; Panel goes <code>live</code>.</td>
|
||
<td>Chat stays <code>broken</code> unless the reload retries broken objects.</td>
|
||
<td>§4, §5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
<td>Server S has no models plugin. On W1, the models plugin's host-lifetime ModelRuntime declares <code>self.get(ModelConfig)</code> (placed on S).</td>
|
||
<td>S keeps W1's hold as refused, <code>not_served</code>. ModelRuntime is <code>failed { waiting }</code>. W1's <code>load</code> counts it as a first outcome (parked on another host). The harness runs, and its generations report "model unavailable".</td>
|
||
<td>S refuses <code>broken</code> (<code>remote/hold.ts #refuse</code>). ModelRuntime is <code>transient</code> and polls: each backoff tick (1-60 s) makes a new hold across the link and reports again, for the host's life.</td>
|
||
<td>§1, §3, §6.3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
<td>S gets the models plugin by a reload (3, continued).</td>
|
||
<td>S's commit re-runs its refused hold and sends <code>loading { hold, attempt }</code>, then <code>bound</code>. ModelRuntime wakes, opens its native and contributes to <code>HarnessModels</code>. The harness's next generation finds a model.</td>
|
||
<td>The next backoff tick re-holds; recovery waits up to 60 s.</td>
|
||
<td>§4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
<td>A reload removes the ports plugin from S. Tabs hold <code>PortsView</code>; Radius (live) reads the <code>radius.forwards</code> registry that Ports contributed to.</td>
|
||
<td>PortsView's objects end, and their holds become unbound <code>not_served</code>: the provider sends <code>failed</code> on the bound hold, and the tabs show <code>failed { waiting }</code>. Radius gets a table without Ports, composes the empty value and keeps working. A later re-add re-runs the kept holds, which bind again.</td>
|
||
<td>The holds get <code>gone</code> and the references end for good (design §21). A re-add cannot revive them.</td>
|
||
<td>§5, §9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
<td>A reload removes W1's file-index plugin. The live Files object (files plugin) holds <code>self.get(FileIndex)</code> locally.</td>
|
||
<td>Files' <code>Binding</code> becomes unbound <code>not_served</code>. Files stays live (R18). Its member sources keep their last values, and calls through that reference reject with the cause. When file-index is re-added, the binding binds again and the members re-bind by name (one frame).</td>
|
||
<td>The reference ends <code>released</code>; Files can never use FileIndex again without a restart.</td>
|
||
<td>§5 (locally), §3.10</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
<td>After a sessions plugin update, S's Catalog factory throws. W1's Chat waits on Catalog; a tab's Panel waits on Chat. The user presses Retry on Panel.</td>
|
||
<td>Catalog is <code>broken</code>; Chat and Panel are <code>failed { waiting, "waiting for Catalog: …" }</code>. Retry passes on hop by hop (T, then S's Catalog, through W1) and retries Catalog once. If the cause is gone (the config is fixed), Catalog goes live and Chat, then Panel, wake.</td>
|
||
<td>Chat polls by backoff with new holds. Retry on Panel retries only Panel, which is still blocked.</td>
|
||
<td>§3.7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
<td>S's EnvManager (envs plugin) opens an SSH connection that fails because the remote host is offline. W1's environment objects wait on S's env views, which wait on EnvManager.</td>
|
||
<td>Only EnvManager backs off (<code>transient</code>, its own <code>retryAt</code>). Its dependents are <code>failed { waiting }</code> with no timers. When SSH comes back, EnvManager goes live and the chain wakes in order. A <code>host.get</code> on a dependent rejects at once (it is <code>waiting</code>); a hold just shows it.</td>
|
||
<td>Every level runs its own 1-60 s backoff and re-holds across the link at every tick.</td>
|
||
<td>§3.1, §3.3, §8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>9</td>
|
||
<td>A reload of the terminals plugin restarts S's TerminalService (natives: the terminal host process D). Terminals{session} objects are live and depend on it.</td>
|
||
<td>Native restart as in §21: new calls park while it is <code>loading</code>; running calls drain; natives close with reason <code>reload</code>; the factory runs; natives reopen. Its dependents stay live (R18); their calls park while it is <code>loading</code>.</td>
|
||
<td>The same; this is design §21, unchanged.</td>
|
||
<td>§5 (old calls), §3.10</td>
|
||
</tr>
|
||
<tr>
|
||
<td>10</td>
|
||
<td>Worker W1 crashes while a tab views Chat{S1} through S.</td>
|
||
<td>S's relayed hold goes to its path, <code>no_link</code>, so the tab's reference is <code>loading</code> and its calls park, bounded. The route re-resolves (route backoff); the new W1 publishes, the held request binds, and a fresh <code>bound</code> reaches the tab.</td>
|
||
<td>The same, except a dependent that is activating fails at its 60 s deadline in between.</td>
|
||
<td>§1, §2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>11</td>
|
||
<td>A tab on Chat v1 meets W1 serving Chat v2 with <code>accepts[1]</code>.</td>
|
||
<td>The check accepts v1; the hold binds through the mappings (slice 7). The tab is <code>live</code>.</td>
|
||
<td>The same once slice 7 builds <code>accepts</code>.</td>
|
||
<td>§1, §5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>12</td>
|
||
<td>A tab on Chat v1 meets W1 serving Chat v3 without <code>accepts[1]</code>.</td>
|
||
<td>The hold is unbound <code>version</code>, so the tab shows <code>failed { waiting }</code> ("update the page") and its calls reject. The tab's own reload makes a new v3 request and releases the old one. If W1 rolled back to v2, the old hold would bind again in place.</td>
|
||
<td>Refused <code>broken</code>, or <code>gone</code> at the reload that dropped v1: ended for good.</td>
|
||
<td>§1, §5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>13</td>
|
||
<td>A tab reloaded to v2 holds Chat while W1 is installing v2.</td>
|
||
<td>W1 keeps the hold <code>installing</code>: the tab is <code>loading</code> and its calls park, bounded. W1's install commit re-evaluates the hold, which binds v2. If W1's install is rejected, the hold is re-evaluated at that end and becomes <code>failed { waiting, version }</code>.</td>
|
||
<td>Refused <code>broken</code> (<code>contract_mismatch</code>); design §21's <code>updating</code> is not built.</td>
|
||
<td>§5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>14</td>
|
||
<td>A plugin changed Chat's contract without bumping the version: a tab on v1 (fingerprint A), W1 on v1 (fingerprint B).</td>
|
||
<td>Unbound <code>version</code> (a fingerprint that differs at the same version), so <code>failed { waiting }</code>. It is not <code>broken</code>: the consumer did nothing wrong, and its next reload or a fixed provider heals it.</td>
|
||
<td>Refused <code>broken</code>, <code>contract_mismatch</code>.</td>
|
||
<td>§1</td>
|
||
</tr>
|
||
<tr>
|
||
<td>15</td>
|
||
<td>A reload of chat makes Chat v4 accept v3 but drop v2. Tabs hold Chat at v2 and at v3.</td>
|
||
<td>Only the v2 holds unbind (<code>version</code>). The object, its v3 holds, documents, jobs and natives stay.</td>
|
||
<td>Design §21: the version's holds end with <code>gone</code>.</td>
|
||
<td>§5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>16</td>
|
||
<td>S has no web-shell plugin. Files, artifacts and terminals contribute routes to <code>Http</code>; proxy-marker reads <code>Http</code>.</td>
|
||
<td>The contributions wait in the table. Proxy-marker's reader is unbound <code>not_served</code> at once after the publication: <code>failed { waiting }</code>. A reload that adds web-shell serves and composes <code>Http</code>, the index wakes the reader, and it goes live.</td>
|
||
<td>Slice 8 S3: the reader fails <code>transient</code> (<code>unavailable/undeclared</code>) at once and is woken at the first value only.</td>
|
||
<td>§9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>17</td>
|
||
<td>The codemode plugin is reloaded alone with a <code>HarnessExtensions</code> token v3; the core harness serves v2.</td>
|
||
<td>Codemode's v3 contribution is left out and reported (<code>contribution_refused</code>). A reader bundling v3 is unbound <code>version</code>: <code>failed { waiting }</code>. A core reload to v3 makes it usable, which wakes the reader.</td>
|
||
<td>The contribution is refused the same way; the reader is <code>broken</code> (S2).</td>
|
||
<td>§9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>18</td>
|
||
<td>Core is reloaded alone, serving <code>HarnessExtensions</code> v3 with <code>accepts[2]</code> (contribution and value).</td>
|
||
<td>v2 contributions are adapted once per value; v2 readers get the composition mapped back once per composition. A reader at v1 (no mapping) is <code>failed { waiting, version }</code>.</td>
|
||
<td>The same, except the v1 reader is <code>broken</code> (S2).</td>
|
||
<td>§9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>19</td>
|
||
<td>The harness's <code>HarnessExtensions</code> compose throws at the first composition (an owner bug).</td>
|
||
<td>The registry is served but has no usable value, so the Harness's request is unbound <code>compose</code>: <code>failed { waiting }</code>. The barrier counts its first outcome. A reload that fixes compose makes the value usable, and the Harness wakes and activates.</td>
|
||
<td>S3: the Harness fails <code>transient</code> at once, woken at the first good value.</td>
|
||
<td>§1, §9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>20</td>
|
||
<td>Codemode contributes its sandbox (a native) to <code>HarnessExtensions</code>. Later another contributor's entry makes compose throw, and then codemode restarts.</td>
|
||
<td>While compose throws, <code>.value</code> keeps the last good composition. When codemode's contribution leaves, that last good value is dropped (it holds the closed sandbox), and <code>.value</code> throws until compose succeeds again. No reader is handed a closed native.</td>
|
||
<td>Slice 8 keeps the last good value regardless, so a reader could use the closed sandbox.</td>
|
||
<td>§9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>21</td>
|
||
<td>Transcript (on W1) follows the harness through <code>Harnesses.value.observeConversation(...)</code> and declares <code>retryOn(Harnesses)</code>. The harness's Session fails and the harness object restarts.</td>
|
||
<td><code>self.follow</code> is Transcript's own step: S1 ends the observer <code>failed</code>, so Transcript fails <code>transient</code> (its own backoff). When the new harness contributes, <code>retryOn</code> wakes it, and it attaches to the new harness and gets one fresh frame.</td>
|
||
<td>The same (slice 8 S1 and <code>retryOn</code>); unchanged by the model.</td>
|
||
<td>§3 (follow), §3.8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>22</td>
|
||
<td>A reload of chat swaps the live Chat in place (no natives). Its new factory adds a <code>self.get(Sharing)</code>, and the sharing plugin is absent.</td>
|
||
<td>Owner item 4, recommended: Chat parks. Its holders see <code>failed { waiting, "waiting for Sharing: not served" }</code> and its calls reject until sharing appears. The alternative keeps the old code until the dependency resolves.</td>
|
||
<td>Not built (slice 7); design §21 swaps the object and opens its gate.</td>
|
||
<td>§3.9, §13.4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>23</td>
|
||
<td>Chat is parked on DraftStore (1). A chat reload ships code that no longer needs DraftStore.</td>
|
||
<td>One handover: the new attempt's requests bind first, then the old ones are released. Chat activates on the new code and goes live; the old <code>not_served</code> index entry goes.</td>
|
||
<td>Not built; today a broken Chat is retried by the reload (slice 7 plan).</td>
|
||
<td>§3.9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>24</td>
|
||
<td>A reload removes the models plugin from W1 while ModelRuntime is parked on S's ModelConfig (3).</td>
|
||
<td>ModelRuntime (host-lifetime) ends with its plugin. Its requests end, and W1's refused hold at S is released (a <code>release</code>). Nothing stays waiting.</td>
|
||
<td>The type's objects end (§21), and its backoff polling stops with it.</td>
|
||
<td>§3.5, §1</td>
|
||
</tr>
|
||
</tbody></table>
|
||
</section>
|
||
|
||
<section class="chapter" id="sc-rollout"><header class="opener"><div class="numeral">XVII</div><h2>Scenarios: Rollouts and Cycles</h2><p class="lede">Delayed facets across hosts, rollout windows, chains over three hosts, and the probe at work.</p></header>
|
||
<p class="dropcap">Sol's scenarios cover the windows in which hosts run different code: a server facet ahead of its worker facet and the
|
||
reverse, tabs older or newer than the server, installs that are queued, staged or rejected, a type moved between host
|
||
kinds; then chains across three hosts, diamonds and rings, the probe's own corner cases and a forged probe. Row 20 is a
|
||
case round 3 found against the previous draft; the rule that fixes it is in chapter VIII. The pattern: a version that
|
||
does not fit is a request waiting for a publication, never an error that ends it; an install answers only for its own
|
||
host; and a cycle is found by the attempt whose edge closed it.</p>
|
||
<p>The Rule column cites sections of the working document, reproduced in appendix C.</p>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>#</th>
|
||
<th>Scenario</th>
|
||
<th>Under the model</th>
|
||
<th>Today</th>
|
||
<th>Rule</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody><tr>
|
||
<td>1</td>
|
||
<td>The models plugin's server facet upgrades first. Browser and TUI model pickers still use its older Models contract, which the new provider accepts.</td>
|
||
<td>Their references remain usable through the old-version adapters. If an owning native restarts, affected references show <code>loading</code> until its fresh binding is ready. A caller that later installs the new contract releases its old request and makes a new one.</td>
|
||
<td><code>host/host.ts:#assertMatch</code> accepts only exact versions and fingerprints. Accepted-version serving across reload is not built in next; p2 uses its older cluster reload runtime.</td>
|
||
<td>§1, §5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
<td>A worker's new search facet makes SearchIndexer depend on SearchFeed v2. The server still serves v1; its search facet installs later.</td>
|
||
<td>SearchFeed's request is <code>failed { waiting }</code> for the version mismatch. SearchIndexer is also <code>failed { waiting }</code>, without a dependency retry timer. The server's later publication rechecks the request; SearchFeed becomes <code>live</code> and wakes the indexer.</td>
|
||
<td>A remote mismatch is refused as broken; its dependent fails transient and retries with backoff. See <code>host/attempt.ts</code> and <code>remote/hold.ts</code>; p2's real indexer declares this server dependency in <code>plugins/search/worker.ts</code>.</td>
|
||
<td>§2–5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>3</td>
|
||
<td>A new web workspace facet arrives before the selected worker's matching Workspace contract. The user opens the workspace panel during this window.</td>
|
||
<td>The panel's held Workspace reference becomes <code>failed { waiting }</code>; an awaited getter rejects under the recommended getter rule. The old worker object and compatible readers are unaffected. Installing the worker facet wakes the held request and supplies a fresh binding.</td>
|
||
<td>Exact-match refusal reaches the client; a dependent object can enter R18 backoff. Next has no install-aware wake protocol yet; p2's workspace UI and worker facets live in <code>plugins/workspace/</code>.</td>
|
||
<td>§2, §5, §8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
<td>A tab sleeps while the worker upgrades Workspace and accepts its old version. The tab reconnects without updating its code.</td>
|
||
<td>The retained request shows <code>loading</code> while its path is down. Re-holding asks for the tab's original version, and <code>bound</code> supplies the mapped snapshot, returning it to <code>live</code>. No old method index is interpreted as a new one.</td>
|
||
<td>Path re-holding already exists in <code>remote/path.ts</code>, but unequal versions still fail in <code>host/host.ts</code>. Reload-time adapters are slice 7 work.</td>
|
||
<td>§1, §5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
<td>The server publishes a new Models contract without accepting the old TUI version. The TUI launcher downloads its replacement bundle later.</td>
|
||
<td>The old TUI request stays held but shows <code>failed { waiting }</code>. The old runtime releases it when the launcher swaps workers. The replacement runtime makes a new-version request and can become <code>live</code>; the old request is not silently relabeled.</td>
|
||
<td>p2 polls for TUI bundles and swaps sandbox workers in <code>hosts/tui-client.ts</code>. Next does not yet implement reload-time version recovery.</td>
|
||
<td>§1, §5, §8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
<td>The server and tabs already use revision R5 when a route starts a worker from an older plugin set. The worker connects before loading its current facets.</td>
|
||
<td>Business requests stay <code>loading</code> until publication or show <code>failed { waiting }</code> against published incompatible code. The loader installs the desired code set without awaiting activation. Publication then rechecks waiting requests and the actual objects activate; a route must not wait for the worker's activation barrier before admitting its link.</td>
|
||
<td>p2 sends selected worker facets through WorkerInit and uses a separate control-ready sequence. Next has the publication/activation admission deadlock described in <code>host/host.ts:load</code> and <code>remote/hold.ts:start</code>.</td>
|
||
<td>§1, §5–6</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
<td>A tab installs new workspace code while the worker's matching install is queued but has not started.</td>
|
||
<td>The old worker answers <code>failed { waiting }</code>, not <code>loading</code> merely because another host knows an upgrade is planned. A held reference survives; a getter rejects. The worker's own install and its completion re-evaluate the request, so it can later become <code>live</code>.</td>
|
||
<td>Next has no revision/install status handling; it returns an exact-match failure. The model deliberately accepts this rollout-window blip instead of adding revision fields to mismatch messages.</td>
|
||
<td>§2, §5, §8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
<td>The worker's search facet stages a new contract, so an incompatible request is held as installing. Staging fails and the old search code remains published.</td>
|
||
<td>The request was <code>loading</code> during the install. Rejection is an install-end event even without publication, so the request is checked against the old provider and becomes <code>failed { waiting }</code> for the remaining mismatch. Its getter rejects rather than waiting forever for a commit that will not occur.</td>
|
||
<td>Next has no installer. p2's <code>hosts/server.ts:performReload</code> retains published code and bundles when preparation fails, through a different reload protocol.</td>
|
||
<td>§4–5, §8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>9</td>
|
||
<td>A worker's own envs facet install is rejected while it needs a capability from the new code set. A provider elsewhere becoming ready cannot repair that rejected local install.</td>
|
||
<td>Requests blocked by the worker's own rejected install fail promptly with its reason. Published old code is not replaced by half-installed new code. A later successful local install or explicit retry is needed; waiting for a remote activation is not an install strategy.</td>
|
||
<td>Next has no own-install state. Design §21 specifies the prompt own-rejection rule; p2 reports reload refusal while retaining its previous selection.</td>
|
||
<td>§5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>10</td>
|
||
<td>The worker ports tool facet is added before the server ports facet. The tool is registered successfully, but the first invocation looks up server Ports with <code>ctx.get</code>.</td>
|
||
<td>The tool owner remains <code>live</code>: this is a call-time lookup, not a declared factory dependency. The missing Ports request is <code>failed { waiting }</code>, so the invocation reports unavailable and its call-scoped request ends. After the server facet publishes, a later invocation can acquire a live Ports object.</td>
|
||
<td>This is how <code>plugins/ports/worker.ts</code> declares the tool: its callback does <code>ctx.get(Ports)</code>. Next's remote lookup currently receives a broken refusal; no provider-publication recovery is built.</td>
|
||
<td>§1–3, §5, §8</td>
|
||
</tr>
|
||
<tr>
|
||
<td>11</td>
|
||
<td>The server removes Ports first, while old worker tool code and an old browser Ports sheet remain loaded. The worker and browser facets update afterward.</td>
|
||
<td>The browser's held request becomes <code>failed { waiting }</code> without ending permanently. Ordinary live worker owners stay <code>live</code>; their next Ports lookup fails while the provider is absent. Updating those consumers removes obsolete surfaces, or a later provider publication can restore a still-held request.</td>
|
||
<td>p2 normally refuses removal when outside-batch consumers still hold a removed type (<code>hosts/server.ts:refuseHeldRemoval</code>). Next's target design originally ended removed-type references; the retained-request behavior is new.</td>
|
||
<td>§3.10, §5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>12</td>
|
||
<td>Notes(session=7) moves from a server facet to a worker facet. An old tab still computes the server address; a new tab computes worker 7.</td>
|
||
<td>The old request stays at the old address and shows <code>failed { waiting }</code>; publishing Notes on the worker cannot wake it there. The new tab routes to worker 7 and can become <code>live</code>. Updating the old consumer computes a new address and creates a new request; the model adds no redirect.</td>
|
||
<td>Placement is already computed by the caller and checked by the provider in <code>host/placement.ts</code>. Next has no relocation reload implementation.</td>
|
||
<td>§1, §5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>13</td>
|
||
<td>Worker 1 still serves Notes v1 for its own session. Its new code requests Notes v2 placed on worker 2, which already serves v2.</td>
|
||
<td>Resolution chooses worker 2 before checking its version. The request can become <code>live</code> without waiting for worker 1's unrelated registration to upgrade. Worker 2's <code>bound</code> wakes any dependent waiting on that request.</td>
|
||
<td><code>host/host.ts:#assertRoute</code> checks a same-name local registration before <code>Placement.reference</code>, so worker 1's v1 can wrongly reject this v2 request.</td>
|
||
<td>§1</td>
|
||
</tr>
|
||
<tr>
|
||
<td>14</td>
|
||
<td>The models plugin's server facet updates now, but the worker facet updates hours later. Each host has its own native model registry and compatible local contributors.</td>
|
||
<td>Each host composes its own table and its local consumers can stay <code>live</code>. A worker does not wait for the server's registry revision: native registries are host-local. Only an actual remote object contract used between those facets creates a cross-host version wait.</td>
|
||
<td>p2 also has independent server ModelConfig and WorkerModels resources (<code>plugins/models/server.ts</code>, <code>worker.ts</code>). Next's registry adapters already handle local reader/contributor versions; atomic registry replacement is not built.</td>
|
||
<td>§5, §9</td>
|
||
</tr>
|
||
<tr>
|
||
<td>15</td>
|
||
<td>A tab Panel depends on a worker SessionView, which depends on a server Catalog view. Catalog's new facet arrives last.</td>
|
||
<td>Catalog's missing request is <code>failed { waiting }</code>; SessionView and Panel show waiting with the nearest blocker and root cause. The server publication binds Catalog and makes it <code>live</code>. Its state change wakes SessionView; that provider's <code>bound</code> then wakes Panel, with no dependent backoff loops.</td>
|
||
<td>R18 retains dependency references but fails dependents transient and uses identity backoff. <code>host/object.ts:dependencyLive</code> already skips that backoff on a live event; registration-based recovery is missing.</td>
|
||
<td>§1–4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>16</td>
|
||
<td>A server Summary waits for views on two workers. Both views reach a shared local CatalogStore on the server, whose own required provider is absent: a diamond, not a cycle.</td>
|
||
<td>CatalogStore is <code>failed { waiting }</code>, and waiting propagates through the views to Summary. Probes reach the common store, but its unavailable request has no target attempt and no edge back to Summary. It must not report <code>invalid/cycle</code>; adding the missing provider later wakes the chain.</td>
|
||
<td>Current local waiting-edge traversal and trusted activation chains distinguish cycles from shared dependencies. Dependency failures still use R18's transient/backoff path (<code>host/object.ts</code>, <code>host/attempt.ts</code>).</td>
|
||
<td>§3–4, §7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>17</td>
|
||
<td>Thirty-three object attempts on trusted server/worker links form a ring and start together. Each reference resolves to the next attempt in that ring.</td>
|
||
<td>All participants stay pre-activation, showing <code>loading</code> or waiting according to their blockers. With answer (A), the attempts waiting across links time out after <code>readyTimeout</code>, retry with backoff, and the activation chains of the later attempts find the ring. With answer (B): the attempt whose edge closed the ring last starts a search; it returns, a check along its recorded path returns, and the initiator fails <code>broken invalid/cycle</code>; propagation breaks the others and the dependents off the ring. The probe carries its trail of 33 hops; no cap cuts it.</td>
|
||
<td>Concurrent cross-host cycles can escape fixed activation chains and rely on readyTimeout/backoff. <code>host/object.ts:assertNoCycle</code> and design §6 document that limitation; next has no probe.</td>
|
||
<td>§3.1, §7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>18</td>
|
||
<td>A ring is being confirmed when one worker reloads its facet and removes its outgoing dependency, repairing the ring. Some probe returns are already in flight.</td>
|
||
<td>With answer (B): New attempts have new ids, and obsolete target ids are discarded. If the check passed before the repair, the ring is reported (it existed when the check began); if the repair came first, the check dies at the replaced attempt and nothing is reported. Unchanged attempts can therefore become <code>broken invalid/cycle</code> and need <code>ref.retry()</code> or their own plugin reload; provider readiness alone does not retry broken objects.</td>
|
||
<td>Next has no probe or reload implementation. Broken cycle failures already require retry, and <code>host/object.ts:retry</code> starts another attempt.</td>
|
||
<td>§3.6–9, §7</td>
|
||
</tr>
|
||
<tr>
|
||
<td>19</td>
|
||
<td>A malicious tab learns a worker attempt id and sends a forged probe naming that same attempt as its initiator through the server relay.</td>
|
||
<td>With answer (B): The server drops the probe because it arrives on a child link, before any forwarding. The worker object and legitimate references keep their states. A child link never carries cycle evidence.</td>
|
||
<td>There is no probe message today. <code>remote/provider.ts</code> already strips activation chains from untrusted holds rather than trusting a child to declare ancestry.</td>
|
||
<td>§7.2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>20</td>
|
||
<td>A→B→C wait while C's return path to A is disconnected. Sixty-five unrelated initiating rounds pass through C before reconnect makes C→A a known edge.</td>
|
||
<td>With answer (B): Found in round 3 against draft 3 (C could have evicted every round from the ring). Under the model the re-hold gives C a new edge token, so C starts its own search; it crosses A and B and returns to C, the check along its path confirms, and C fails <code>broken invalid/cycle</code>. Foreign rounds and evictions play no part.</td>
|
||
<td>Current re-hold carries an activation chain (<code>remote/remote-object.ts:attach</code>), which can detect a small newly reachable ring; otherwise the dependency deadline remains its fallback. No probe-round cache exists today.</td>
|
||
<td>§7.2–3</td>
|
||
</tr>
|
||
</tbody></table>
|
||
</section>
|
||
|
||
<section class="chapter epilogue" id="open-questions"><header class="opener"><div class="numeral">∴</div><h2>Open Questions</h2><p class="lede">What the owner decides, and what slice 7's spec must still pin down.</p></header>
|
||
<p class="dropcap">The model replaces five answers to "the provider is not there" with one: the request stays, the dependent parks, an
|
||
event wakes it. It ends the startup deadlock with a gate that lasts only for the synchronous setups, finds cycles with a probe
|
||
instead of a deadline (or, if the owner prefers, one narrowed deadline), and keeps timers only where no event can exist. Four reviewers played more than 300
|
||
scenarios against five drafts; the catalog in the chapters before this one is a selection.</p>
|
||
<h3 id="decisions-for-the-owner">Decisions for the owner</h3>
|
||
<ol>
|
||
<li><strong>The barrier</strong> (chapter VI): (a) Chord's <code>load</code> waits for the first outcomes of this host's host-lifetime objects,
|
||
with the local-blocker rule; or (b) <code>load</code> resolves after the publication and the app reads <code>host.startup</code>.
|
||
Recommended (a); Astra, Opus and Sol for (a), Fable for (b).</li>
|
||
<li><strong>Getters on <code>waiting</code></strong> (chapter IX): reject at once (recommended) or wait up to <code>readyTimeout</code>.</li>
|
||
<li><strong><code>waiting</code> as a cause kind</strong> (chapter III): a new kind (recommended; Opus, Fable), <code>transient</code> without <code>retryAt</code>
|
||
(Astra), or <code>loading</code> with a diagnostic (Sol).</li>
|
||
<li><strong>A pure swap whose new factory needs a dependency that is not there:</strong> the object parks (its gate closes, its
|
||
holders see <code>loading</code> or <code>failed { waiting }</code>, calls park or reject; recommended), or it stays on the old code until
|
||
the dependency resolves (old and new code would then overlap without bound, against §21's "a host is at R or R−1").</li>
|
||
<li><strong>The last good value</strong> (chapter X): kept only while every contribution it was built from is still in the table
|
||
(recommended; Astra), or kept as slice 8 decided, with natives in a registry looked up at use through their owner
|
||
(Sol).</li>
|
||
<li><strong>Cycles across hosts</strong> (chapter VIII): (B) the probe, with (B1) a re-search backoff that sends probes only
|
||
(recommended) or (B2) a full search lifecycle with no timer at all; or (A) the slice 4b decision narrowed to one
|
||
deadline, which polls chains across hosts again, misses a ring of waiting attempts, and retries in lockstep without
|
||
finding the cycle it is for.</li>
|
||
</ol>
|
||
<h3 id="for-slice-7-39-s-spec">For slice 7's spec</h3>
|
||
<ul>
|
||
<li>The test list: the startup deadlock resolving without <code>readyTimeout</code>; an early hold placed here, not through the
|
||
parent; remove-then-serve with a live dependent's calls recovering; a 1,000-hold publication wave under the window;
|
||
the probe on long rings, diamonds, edge changes, reloads, relays, reconnects and forged probes; GC of a parked
|
||
three-level chain after its top is released; every index entry gone after release, revocation and link loss.</li>
|
||
<li>The local re-binding of a <code>Binding</code> (member sources, keyed pins, parked calls, presence) and the unbinding of a live
|
||
hold under flow control, both with their race tests.</li>
|
||
<li>The wire: <code>waiting</code> in the <code>failed</code> cause schema, the attempt id on <code>loading</code> and <code>failed</code>, the <code>probe</code> message, the
|
||
pinned list; <code>PROTOCOL_VERSION</code> stays 1 until p3 ships.</li>
|
||
<li>The design changes listed in appendix A, folded into <code>design.md</code> and the decision log in the same commit as the
|
||
owner's decisions.</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<section class="chapter appendix" id="what-changes"><header class="opener"><div class="numeral">A</div><h2>What Changes</h2></header>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Where</th>
|
||
<th>Today</th>
|
||
<th>Under the model</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody><tr>
|
||
<td>R18, activation</td>
|
||
<td>a dependency that does not go live within <code>readyTimeout</code> fails the dependent <code>transient</code>; backoff 1 to 60 s</td>
|
||
<td>the dependent parks with no timer; <code>failed { waiting }</code> when a dependency failed; woken on every state change of a dependency; the deadline paused while parked</td>
|
||
</tr>
|
||
<tr>
|
||
<td>R18, the boundary</td>
|
||
<td>"live" means every dependency was live at the end of activation</td>
|
||
<td>the dependency check is the boundary: after it, a dependency leaving <code>live</code> is treated as after live</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>self.get</code> of a missing <code>local</code> type</td>
|
||
<td>throws in the factory: <code>broken</code></td>
|
||
<td>unbound <code>not_served</code>: <code>failed { waiting }</code>, woken by the publication that serves it</td>
|
||
</tr>
|
||
<tr>
|
||
<td>a remote hold the provider cannot serve</td>
|
||
<td>refused <code>broken</code>; the dependent polls by backoff</td>
|
||
<td>refused and kept at the provider, re-run at its publication; <code>failed { waiting }</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>design §3, the destination</td>
|
||
<td>"served here" decides "here"; <code>placement</code> optional on <code>remote()</code></td>
|
||
<td>the address decides; every remote type declares its <code>hostAddress</code>, renamed from <code>placement</code> (owner, 2026-10-29)</td>
|
||
</tr>
|
||
<tr>
|
||
<td>the start barrier, <code>pendingLoad</code></td>
|
||
<td>holds wait for all of <code>load</code></td>
|
||
<td>holds wait only for the publication; the barrier is (a) or (b)</td>
|
||
</tr>
|
||
<tr>
|
||
<td>S2, a registry reader newer than its provider</td>
|
||
<td><code>broken</code></td>
|
||
<td><code>failed { waiting, version }</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>S3, a registry without a value</td>
|
||
<td><code>transient</code>, woken at the first value only</td>
|
||
<td><code>failed { waiting }</code>, woken at every transition to usable</td>
|
||
</tr>
|
||
<tr>
|
||
<td>slice 8, the last good value</td>
|
||
<td>kept whenever <code>compose</code> throws</td>
|
||
<td>kept only while every contribution it was built from is still in the table</td>
|
||
</tr>
|
||
<tr>
|
||
<td>design §6, cycles</td>
|
||
<td>a walk on one host, chains across hosts, a documented gap closed by the deadline</td>
|
||
<td>the gap of simultaneous starts across hosts: (A) one narrowed deadline, or (B) a probe: a search per edge change, a check along the recorded path, edge tokens, <code>dead</code> notices</td>
|
||
</tr>
|
||
<tr>
|
||
<td>design §7, <code>host.hold</code></td>
|
||
<td>throws for a missing local type after <code>load</code></td>
|
||
<td>always returns a reference</td>
|
||
</tr>
|
||
<tr>
|
||
<td>design §7, getters</td>
|
||
<td>wait through every failure up to <code>readyTimeout</code>, reject on <code>broken</code></td>
|
||
<td>wait through <code>loading</code> and <code>transient</code>; reject at once on <code>waiting</code> and <code>broken</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>design §21, removal</td>
|
||
<td><code>gone</code> to every holder</td>
|
||
<td><code>failed { waiting, not_served }</code>; the request binds again when the name is served</td>
|
||
</tr>
|
||
<tr>
|
||
<td>design §21, a dropped version</td>
|
||
<td><code>contract_mismatch</code>, final</td>
|
||
<td>only that version's requests unbind; the object stays</td>
|
||
</tr>
|
||
<tr>
|
||
<td>design §21, rollout</td>
|
||
<td><code>updating</code> with revision fields and a deadline</td>
|
||
<td>each host knows its own install; <code>installing</code> re-evaluated at the install's end; installs never await activations</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>FailureCause</code></td>
|
||
<td><code>transient</code>, <code>broken</code></td>
|
||
<td>adds <code>waiting</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>the wire</td>
|
||
<td></td>
|
||
<td><code>failed</code> cause <code>waiting</code>; the attempt id on <code>loading</code> and <code>failed</code>; with (B), one new message, <code>probe</code> (<code>search</code>, <code>check</code>, <code>dead</code>)</td>
|
||
</tr>
|
||
<tr>
|
||
<td>unchanged</td>
|
||
<td></td>
|
||
<td>an object's own code reload (§21); R18 after live; <code>retryOn</code> outside parking; <code>self.follow</code>'s R4; the call window and flow control; revocation's semantics; no client providers</td>
|
||
</tr>
|
||
</tbody></table>
|
||
</section>
|
||
|
||
<section class="chapter appendix" id="glossary"><header class="opener"><div class="numeral">B</div><h2>Glossary</h2></header>
|
||
<p><strong>Attempt:</strong> one try at activating an object: its factory, requests, natives; its own id since the model. <strong>Bound:</strong> a
|
||
request pointing at an object here or a hold over a path. <strong>Dependency:</strong> a request an attempt needs live before it
|
||
may go live (<code>self.get</code>, <code>self.registry</code>, a keyed pin). <strong>Dependency check:</strong> the moment all of an attempt's
|
||
dependencies are live; after it the attempt runs its own steps. <strong>Edge generation:</strong> a number of a wait edge, incremented by its
|
||
source whenever the request binds anew. <strong>First outcome:</strong> an object is live, failed by its own cause, or parked with no local
|
||
blocker still in its first attempt. <strong>Holder:</strong> whoever made a request and alone ends it. <strong>Parked:</strong> an attempt
|
||
waiting for a dependency, with no timer. <strong>Probe:</strong> the message that walks wait edges to find cycles: a <code>search</code>, then a <code>check</code> along the recorded path. <strong>Publication:</strong>
|
||
a host making its facets' declarations visible: <code>load</code>'s setups, a reload's commit, an install's commit. <strong>Refused:</strong> a
|
||
request without authority or with invalid params; never indexed. <strong>Request:</strong> what a reference stands for: type or
|
||
registry, params, principal, version. <strong>Round:</strong> the number of one search, from a counter per attempt. <strong>Seen set:</strong> the newest round number an
|
||
attempt has forwarded, per initiator (up to 256). <strong>Unbound:</strong> a request with no provider at the moment, with a reason (<code>unpublished</code>, <code>not_served</code>,
|
||
<code>version</code>, <code>compose</code>, <code>installing</code>, no link). <strong>Waiting:</strong> the cause of a failure that is not the object's own, woken by
|
||
an event. <strong>Wait edge:</strong> a parked attempt waiting for another attempt. <strong>Wake:</strong> an event that makes the resolver look
|
||
at a request again.</p>
|
||
</section>
|
||
|
||
<section class="chapter appendix" id="the-working-document"><header class="opener"><div class="numeral">C</div><h2>The Working Document</h2></header>
|
||
<p>The proposal as the reviewers checked it, <code>/tmp/model/model.md</code>, draft 4. Its section numbers are the ones the
|
||
scenario catalogs cite.</p>
|
||
<p>Status: proposal for the owner, not decided. Built in slice 7 with reload and rollout. Sources: the owner's direction
|
||
("park until resolved"; "no difference between local and remote resolution: a link going is like a plugin
|
||
unloading"), the reviews (<code>/tmp/dep-mechanism-*.md</code>, <code>/tmp/dep-graph-*.md</code>, <code>/tmp/barrier-*.md</code>,
|
||
<code>/tmp/model/review{1,2}-*.md</code>), the code at pi <code>358f7d46a</code>. <strong>Owner</strong> marks an open decision.</p>
|
||
<p>Draft 7 (round 6): (A) also retries in lockstep without finding the cycle (Fable); the probe's recovery after a
|
||
suppressed search becomes a choice between (B1) a re-search backoff for probes only (recommended) and (B2) a full
|
||
search lifecycle without timers, since <code>dead</code> notices alone need recovery state that eviction may not drop (Astra, Sol).
|
||
Causes are compared by value on wakes (Opus).
|
||
Round 7: a returned search is accepted while its initiator is parked, whatever its round (Astra: re-searches must not
|
||
invalidate searches under way); detection is eventual, not within one backoff period; the local walk stays (Opus).</p>
|
||
<p>Draft 6 (round 5): cycles across hosts become an owner choice between (A) the owner's slice 4b decision (simultaneous
|
||
starts across hosts time out, no probe message), narrowed to one deadline, and (B) the probe, completed with edge
|
||
tokens, no revisits, and <code>dead</code> notices back to the initiator (Astra: a check that dies silently can lose the only
|
||
search that survived dedupe). (A) turned out to either poll chains across hosts again or miss a ring of <code>waiting</code>
|
||
attempts, so (B) is recommended, pending its review.</p>
|
||
<p>Draft 5 (round 4, Sol's phantom cycle): the second round is replaced by a check along the recorded trail of
|
||
<code>(attempt, edge generation)</code> hops; a reported ring existed at one moment; probes grow with their path.</p>
|
||
<p>Draft 4 after round 4 (probe only): round numbers per attempt, forwarded only when newer than the stored one; probes
|
||
honoured on parent and route links, dropped on child links; completeness stated under an eventually stable graph; the
|
||
repair caveat broadened to any concurrent edge removal; a new edge generation resets confirmation; a bounded display
|
||
trail.</p>
|
||
<p>Draft 4 changes (round 3): the probe starts a fresh round on every new or changed wait edge (a ring closes at its last
|
||
edge, whose source finds it), keeps one seen entry per initiator, accepts only its current round, and states its
|
||
cost honestly; every host drops probes from untrusted links; local forwarding is iterative; no-placement remote
|
||
requests are re-placed when a host's first parent link connects; a cohort object that ends before its first outcome
|
||
settles the barrier; the last-good refinement is marked for the owner.</p>
|
||
<p>Draft 3 changes (round 2): remote types without placement keep today's fallback (here if served, else the parent);
|
||
hydration stays before parking; no copied <code>retryAt</code>; a dependency <code>ended</code> breaks its dependent; a reload's handover
|
||
binds new requests before releasing old ones; retry passes only to <code>broken</code> and <code>transient</code> blockers, once per
|
||
operation; <code>installing</code> re-evaluated at every install end (commit or rejection); a registry's last good value dropped
|
||
when one of its contributors leaves; the probe with round ids, bounded seen sets, edge-generation triggers, attempt
|
||
ids on every non-live answer and relays dropping probes from untrusted links; one cycle mechanism, local and remote.</p>
|
||
<h3 id="0-the-problem">0. The problem</h3>
|
||
<p>The same situation, "what I need is not there", has five outcomes today, depending only on how it is declared:</p>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Declared as</th>
|
||
<th>Provider missing</th>
|
||
<th>Today</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody><tr>
|
||
<td><code>self.get(LocalType)</code></td>
|
||
<td>no facet serves it</td>
|
||
<td><code>self.get</code> throws, the factory throws: <code>broken</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>self.get(RemoteType)</code></td>
|
||
<td>the worker does not serve it</td>
|
||
<td>the hold is refused <code>broken</code>; the dependent <code>transient</code>, polled 1-60 s forever</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>self.registry(R)</code></td>
|
||
<td>no provider</td>
|
||
<td><code>transient</code>, woken at R's first value only</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>self.registry(R)</code>, reader newer</td>
|
||
<td></td>
|
||
<td><code>broken</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>a reload removes the type</td>
|
||
<td></td>
|
||
<td>holders get <code>gone</code>: ended for good</td>
|
||
</tr>
|
||
</tbody></table>
|
||
<p>And the startup barrier deadlocks an acyclic graph across two hosts (Sol, <code>/tmp/dep-graph-sol-barrier.log</code>): S's eager
|
||
A needs W's B, B needs S's C; W's hold for C waits for S's whole <code>load</code>, which waits for A. Only <code>readyTimeout</code> ends it.</p>
|
||
<h3 id="1-requests-and-one-resolver">1. Requests and one resolver</h3>
|
||
<ul>
|
||
<li><p><strong>A request</strong> is what a reference stands for: type (or registry), params, principal, the consumer's contract version.
|
||
Its holder makes it and owns it: an attempt (<code>self.get</code>, <code>self.registry</code>, keyed pins), a call (<code>ctx.get</code>), outside
|
||
code (<code>host.get</code>, <code>host.hold</code>, <code>host.registry</code>), a link's peer (<code>hold</code>). <strong>It ends only when its holder ends it</strong> (the
|
||
attempt ends, the call settles, <code>release()</code>, a getter gives up, the peer's link or origin closes, revocation, the
|
||
holding host closes), never because its provider is missing, went away or does not fit.</p>
|
||
</li>
|
||
<li><p><strong>The destination by address</strong> (round 1): a <code>local</code> type goes here; a <code>remote</code> type goes to the address its
|
||
<code>hostAddress</code> (computed from the consumer's own contract) gives: here if that is this host's address, whether served
|
||
here or not, else over the route for that kind or the parent (design §3). Every <code>remote</code> type declares
|
||
its <code>hostAddress</code> (owner, 2026-10-29: the optional <code>placement</code> of slice 3a is removed and the field renamed, and with it the fallback "here if
|
||
served, otherwise the parent" and its re-placement at the first parent link). A hold arriving for an address that is not this host's is relayed or
|
||
refused (<code>invalid/host_address</code>) as today; a relay never captures a hold because it serves the same name.</p>
|
||
</li>
|
||
<li><p><strong>Then the selected provider is checked</strong> (served, version or accepted version and fingerprint, <code>authorize</code>, the
|
||
provider's own host-address check). Version checks never look at a same-name registration elsewhere.</p>
|
||
</li>
|
||
<li><p><strong>The publication gate:</strong> a request made on this host, or arriving on its links, before the host's first
|
||
publication (§6) is not placed yet; it shows <code>loading</code> and is placed by the publication. The gate lasts only for the
|
||
synchronous setups, never for activation, so it cannot deadlock.</p>
|
||
</li>
|
||
<li><p><strong>Outcomes of the check:</strong></p>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>The check finds</th>
|
||
<th>Request</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody><tr>
|
||
<td>a fitting provider</td>
|
||
<td>bound (to the object here, or to the hold over a path)</td>
|
||
</tr>
|
||
<tr>
|
||
<td>not published yet</td>
|
||
<td>unbound <code>unpublished</code>: <code>loading</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>nobody serves the name at its address</td>
|
||
<td>unbound <code>not_served</code>: <code>failed { waiting }</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>no fitting version, a fingerprint that differs at the same version, a host address the provider computes elsewhere (code skew)</td>
|
||
<td>unbound <code>version</code>: <code>failed { waiting }</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>the registry is served, but its first <code>compose</code> (or the reader's version mapping) threw</td>
|
||
<td>unbound <code>compose</code>: <code>failed { waiting }</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>the provider host is installing a revision and the hold does not fit its current code</td>
|
||
<td>unbound <code>installing</code> at the provider: <code>loading</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>the path has no link (a drop, a route resolving)</td>
|
||
<td>on the path: <code>loading</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>authorize</code> refused, invalid params</td>
|
||
<td><code>failed { broken, forbidden | invalid }</code>, as today, never indexed</td>
|
||
</tr>
|
||
</tbody></table>
|
||
<p>The unbound reasons that wait for a name (<code>unpublished</code>, <code>not_served</code>, <code>version</code>, <code>compose</code>) are indexed under it on
|
||
the host that decided them: the consumer's host for a local request; the provider's host for a hold that arrived
|
||
over a link (it already keeps such holds as refused: <code>remote/hold.ts</code>). <code>installing</code> is the provider host's own,
|
||
re-evaluated at its install's end; <code>no_link</code> lives on the path, where the next link holds it again. Indexes drop an
|
||
entry when its request ends; GC-tested.</p>
|
||
</li>
|
||
</ul>
|
||
<h3 id="2-states">2. States</h3>
|
||
<p><code>ref.state</code> keeps its four kinds; <code>FailureCause</code> gains one kind, <code>waiting</code>:</p>
|
||
<pre><code><span class="tk-k">type</span> <span class="tk-t">RefState</span> =
|
||
| { kind: <span class="tk-s">"loading"</span> }
|
||
| { kind: <span class="tk-s">"live"</span> }
|
||
| { kind: <span class="tk-s">"failed"</span>; cause: <span class="tk-t">FailureCause</span>; retryAt: number | <span class="tk-k">undefined</span> }
|
||
| { kind: <span class="tk-s">"ended"</span>; error: <span class="tk-t">ChordError</span> };
|
||
|
||
<span class="tk-k">type</span> <span class="tk-t">FailureCause</span> =
|
||
| { kind: <span class="tk-s">"transient"</span>; error: <span class="tk-t">ChordError</span> } <span class="tk-c">// the object's own effect failed (an open, a follow): backoff</span>
|
||
| { kind: <span class="tk-s">"broken"</span>; error: <span class="tk-t">ChordError</span> } <span class="tk-c">// the object's own code, a cycle, or authority: retry() or a reload</span>
|
||
| { kind: <span class="tk-s">"waiting"</span>; error: <span class="tk-t">ChordError</span> }; <span class="tk-c">// not the object's fault: no provider, no fitting version, a</span>
|
||
<span class="tk-c">// dependency failed; retryAt undefined, woken by an event</span></code></pre>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>The request is</th>
|
||
<th><code>ref.state</code></th>
|
||
<th>Calls</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody><tr>
|
||
<td>bound, its object activating or parked with no failed blocker</td>
|
||
<td><code>loading</code></td>
|
||
<td>park, bounded</td>
|
||
</tr>
|
||
<tr>
|
||
<td>unbound <code>unpublished</code> or <code>installing</code>, or its path has no link</td>
|
||
<td><code>loading</code></td>
|
||
<td>park, bounded</td>
|
||
</tr>
|
||
<tr>
|
||
<td>unbound <code>not_served</code>, <code>version</code> or <code>compose</code></td>
|
||
<td><code>failed { waiting }</code></td>
|
||
<td>reject at once</td>
|
||
</tr>
|
||
<tr>
|
||
<td>bound, its object failed</td>
|
||
<td><code>failed</code> with the object's cause, as today</td>
|
||
<td>reject at once</td>
|
||
</tr>
|
||
<tr>
|
||
<td>bound, its object live</td>
|
||
<td><code>live</code></td>
|
||
<td>run</td>
|
||
</tr>
|
||
<tr>
|
||
<td>ended by its holder, revoked, or its host closed</td>
|
||
<td><code>ended</code></td>
|
||
<td>reject</td>
|
||
</tr>
|
||
</tbody></table>
|
||
<p><strong>Owner:</strong> <code>waiting</code> as a new cause kind (Opus, Fable; recommended: the cause says "not your fault, an event will wake
|
||
you" and a UI can say what is missing) or no new kind: <code>transient</code> with no <code>retryAt</code> (Astra; today's wire already
|
||
carries a <code>transient</code> cause without <code>retryIn</code>, <code>remote-object.ts</code>), or <code>loading</code> with a diagnostic (Sol; parks every
|
||
call on a missing plugin for its full <code>readyTimeout</code>).</p>
|
||
<h3 id="3-dependents-parked-until-their-dependencies-resolve">3. Dependents: parked until their dependencies resolve</h3>
|
||
<p>A <strong>dependency</strong> is what an attempt declares and needs before it goes live: <code>self.get</code> (local or remote),
|
||
<code>self.registry</code>, keyed pins (<code>at(key)</code> on another object's keyed member). <code>self.follow</code> is the attempt's own step (its
|
||
attach runs Durable's code), not a dependency; a follower declares <code>retryOn</code> on the registry it reads the harness
|
||
through, or recovers by its own backoff.</p>
|
||
<ol>
|
||
<li><p><strong>Order, as today up to the check:</strong> the factory runs; persisted singletons hydrate (the object's documents, kept
|
||
across attempts); the requests are made. The attempt is <strong>parked</strong> while any dependency is not <code>live</code> (hydrated):</p>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Its dependencies</th>
|
||
<th>The dependent shows</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody><tr>
|
||
<td>any <code>loading</code>, none failed</td>
|
||
<td><code>loading</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>any <code>failed</code></td>
|
||
<td><code>failed { waiting, "waiting for X: <X's cause>" }</code>, <code>retryAt</code> undefined; the message keeps the nearest blocker and the root cause within the 500-character rule</td>
|
||
</tr>
|
||
<tr>
|
||
<td>one failed with <code>invalid/cycle</code> (a dependent off the ring)</td>
|
||
<td><code>broken</code> with the same error: it would wait on a ring that never heals by itself</td>
|
||
</tr>
|
||
<tr>
|
||
<td>one <code>ended</code> (revoked, or its host closed)</td>
|
||
<td><code>broken</code> with that error: the request cannot come back; <code>retry()</code> or a reload makes new requests (a guard: a dependency ends only with its holder's principal or host, so the dependent ends too; Fable)</td>
|
||
</tr>
|
||
</tbody></table>
|
||
</li>
|
||
<li><p><strong>The dependency check is the activation boundary:</strong> once every dependency is <code>live</code>, the attempt runs its own
|
||
steps (natives in order, follow attaches, member checks, derives), bounded by its own <code>readyTimeout</code> deadline, and
|
||
goes live. A dependency that leaves <code>live</code> after the check is treated as after live (R18): the attempt goes on. This is
|
||
R18's rule for live objects applied from the check on, not a rule of its own (owner, 2026-10-29: a dependency can
|
||
go away at any point of an object's life; the object stays live, its calls through that reference meet its state). So a parked attempt holds no
|
||
natives, and an attempt that opened natives is never parked.</p>
|
||
</li>
|
||
<li><p><strong>No timer while parked:</strong> no dependency deadline, no backoff. The attempt's deadline is paused while parked (as
|
||
eager migrations pause it) and resumes with the budget left.</p>
|
||
</li>
|
||
<li><p><strong>Wakes, one rule:</strong> the dependent re-evaluates on every state change of any of its dependencies (live, loading,
|
||
failed, a new cause compared by value, ended; Opus: otherwise a ring of waiting attempts passes its causes round and
|
||
round), updating what it shows; it leaves parking when all are live. The factory never re-runs
|
||
while parked. Wakes are coalesced per object and run after the batch that caused them; a change during the
|
||
attempt's own steps or its teardown is remembered.</p>
|
||
</li>
|
||
<li><p><strong>Kept while parked:</strong> its documents (the object's), its declarations and requests, its seal token; no natives.
|
||
Ended with the object (its last holder; a host-lifetime object when its plugin is removed or the host closes).</p>
|
||
</li>
|
||
<li><p><strong><code>broken</code> only for the object's own code</strong> (a throwing factory, a derive throwing at activation, hydration,
|
||
<code>migrate</code>, a document fingerprint mismatch, an <code>open</code> throwing <code>broken(...)</code>), <strong>cycles</strong> (§7) and <strong>authority</strong>
|
||
(<code>forbidden</code>, an ended dependency).</p>
|
||
</li>
|
||
<li><p><strong>Retry passes on:</strong> <code>ref.retry()</code> on a <code>failed { waiting }</code> object retries its <code>broken</code> and <code>transient</code> blockers,
|
||
hop by hop (the existing <code>retry</code> message over links), once per retry operation (an id dedupes diamonds); <code>waiting</code>
|
||
blockers pass it on; nothing else is touched. Retry of a <code>broken</code> or <code>transient</code> object starts a new attempt, as
|
||
today.</p>
|
||
</li>
|
||
<li><p><strong><code>retryOn</code></strong> wakes failed objects as decided in slice 8 (a value call retries a <code>transient</code> or <code>broken</code> object);
|
||
while the object is parked <code>waiting</code>, a value call does nothing: its dependencies decide.</p>
|
||
</li>
|
||
<li><p><strong>Own-code reload of a parked object:</strong> its plugin's reload replaces the parked attempt with the new factory's,
|
||
as one handover: the new requests are made and bound first, then the old ones released (round 2, Astra N4: an
|
||
unchanged dependency held only by this object must not end in between; today's <code>#activate</code> binds before
|
||
<code>#releasePrevious</code>).</p>
|
||
</li>
|
||
<li><p><strong>After live, R18 unchanged:</strong> a dependency leaving <code>live</code> leaves its dependent live.</p>
|
||
</li>
|
||
</ol>
|
||
<h3 id="4-wakes-two-events">4. Wakes: two events</h3>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Event</th>
|
||
<th>Re-evaluates</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody><tr>
|
||
<td><strong>a target changes state</strong>: a dependency's object goes live, loading, fails, gets a new cause or ends; a hold's <code>bound</code>, <code>loading</code>, <code>failed</code> arrive, also after a re-hold on a new link</td>
|
||
<td>the requests bound to it, and their dependents (§3.4)</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>a name becomes usable, or an install ends, on a host</strong>: a publication (<code>load</code>'s setups, a reload's commit, an install's commit) serves a type, a registry, a version; a registry composes or maps a value at a version; an install ends without a commit (rejected)</td>
|
||
<td>the unbound requests indexed under those names; the provider's <code>installing</code> holds; a refused hold re-runs its start and sends <code>loading</code>, then <code>bound</code> or <code>failed</code></td>
|
||
</tr>
|
||
</tbody></table>
|
||
<p>No new message kind for wakes: <code>loading</code>, <code>bound</code>, <code>failed</code> and <code>retry</code> exist. The wire's <code>failed</code> carries the new cause
|
||
kind <code>waiting</code>; <code>loading</code> and a <code>waiting</code> <code>failed</code> carry the provider's attempt id for the probe (§7).</p>
|
||
<h3 id="5-removal-versions-rollout">5. Removal, versions, rollout</h3>
|
||
<ul>
|
||
<li><strong>A reload that removes a type:</strong> its objects end as today (natives close, reason <code>end</code>; callers rejected at once;
|
||
handlers drain). Their requests are not ended: each becomes unbound <code>not_served</code> and binds again if a later commit
|
||
serves the name.<ul>
|
||
<li><strong>Locally:</strong> a <code>Binding</code> whose target ended by removal becomes unbound and accepts a second <code>bind</code>: member sources
|
||
re-bind by name (one frame from the last value, as on a restart), keyed pins re-pin, parked calls flush or
|
||
reject, presence re-lists. The one new piece of machinery.</li>
|
||
<li><strong>Over a link:</strong> a <code>failed</code> on a bound hold unbinds it (Fable: one message, one rule; the consumer applies it as
|
||
<code>#startOver</code> plus <code>failed</code>); the provider detaches the hold's sources and drops its unsent pending ops and queued
|
||
<code>bound</code>; the ack slots of <code>bound</code>/<code>state</code> messages already sent stay counted until their <code>ack</code> or the link's loss;
|
||
calls in flight on it are rejected and free their slots once.</li>
|
||
</ul>
|
||
</li>
|
||
<li><strong>A reload that stops accepting a version</strong> unbinds only the requests on that version (<code>version</code>); the object stays
|
||
(its own plugin's reload still restarts it if it has natives, §21).</li>
|
||
<li><strong>A type moved to another host kind:</strong> the request carries the address its own consumer code computed; the old host
|
||
answers <code>not_served</code>; the consumer's own reload makes a new request. No redirect.</li>
|
||
<li><strong>A consumer whose code moves to a new version</strong> releases its old request and makes a new one (today).</li>
|
||
<li><strong>Rollout, without revision fields:</strong> each host knows only its own install. A provider host that installs keeps
|
||
holds that do not fit its current code <code>installing</code> (<code>loading</code>) and re-evaluates them when the install ends, by its
|
||
commit or by its rejection. A provider host that is queued but not installing yet answers a mismatch <code>failed { waiting, version }</code> until its install commits and wakes it: a visible blip of at most one install, accepted (Opus).
|
||
An install never waits for activations (a reverse-order rollout would deadlock); a host whose own install was
|
||
rejected fails its own requests at once with the reason (design §21). The loader keeps its own duties (the desired
|
||
code set, digests, serialized installs).</li>
|
||
<li><strong>Old calls across a reload</strong> follow §21 unchanged: a pure swap lets them finish on old handles; a native restart
|
||
drains them first; a removal rejects their callers at once and drains the handlers. Calls are never resent.</li>
|
||
</ul>
|
||
<h3 id="6-startup-publication-then-first-outcomes">6. Startup: publication, then first outcomes</h3>
|
||
<ol>
|
||
<li><strong>Publication:</strong> <code>load</code> runs every facet's setup, then publishes all at once in one batch (types served,
|
||
registries served and composed once, contributions in their tables), then wakes (§4) after that batch. Nothing
|
||
activates inside it.</li>
|
||
<li><strong>No gate beyond it:</strong> holds and <code>host.hold</code> wait only for the publication, never for the barrier. This ends the
|
||
startup deadlock: W's hold for C binds as soon as S's setups published; B goes live, then A.</li>
|
||
<li><strong>The barrier</strong> (<strong>Owner:</strong> (a) or (b); recommended (a), three reviewers of four):<ul>
|
||
<li><strong>(a):</strong> <code>load</code> resolves when every host-lifetime object of this <code>load</code> has a first outcome:
|
||
<code>firstOutcome(o) = o.live || o.ownFailure || (o.parked && o.blockers.every((b) => !b.local || firstOutcome(b)))</code>,
|
||
where <code>local</code> means executing on this host (an object here, a keyed pin still loading here), not the contract's
|
||
kind. So <code>CodeMode → Harness</code> waits for the harness's outcome; a wait on another host or on a failed dependency
|
||
ends the first pass. One <code>firstOutcome</code> signal per object, separate from the readiness dependents wait for (Sol),
|
||
counted once, never reopened by retries. An object of the cohort that ends before its first outcome (its plugin
|
||
removed, the host closing) settles its entry as ended; <code>host.close</code> settles the waiting <code>load</code> (Astra R3).</li>
|
||
<li><strong>What (a) promises:</strong> every host-lifetime object on this host had its first try, and the contributions of those
|
||
that went live are in their tables. <strong>Not:</strong> every tool that will arrive (a parked or slow contributor, tools found
|
||
in <code>onLive</code>, MCP; Sol's probe <code>/tmp/barrier-sol-first-outcome.log</code>). What a first turn needs is p3's explicit
|
||
admission check.</li>
|
||
<li><strong>(b):</strong> <code>load</code> resolves after the publication; Chord adds <code>host.startup</code>, a watchable of the host-lifetime
|
||
objects' first outcomes with their causes; the app decides (Fable). The same tracker, awaited by the app.</li>
|
||
<li>p2 today resumes its harness without waiting for all host objects (<code>apps/p2/src/hosts/worker.ts</code>), because its
|
||
worker objects that need server types are reachable only after <code>control.ready()</code>.</li>
|
||
</ul>
|
||
</li>
|
||
</ol>
|
||
<h3 id="7-cycles">7. Cycles</h3>
|
||
<p>On one host, cycles are found exactly as today: at <code>self.get</code>, following the waiting edges of activating and parked
|
||
attempts. Across hosts, a hold carries the activation chain it was made in (today), which finds every cycle whose
|
||
objects start one after another. What remains is the documented gap (design §6): objects on two hosts that start at
|
||
the same moment and need each other. The owner decided on 2026-10-24 (slice 4b review decisions (2)) that this is a
|
||
documented limitation that times out and retries, with no probe message. Without dependency deadlines that timeout no
|
||
longer exists, so the gap needs one of two answers (<strong>Owner</strong>):</p>
|
||
<ul>
|
||
<li><strong>(A) The 4b decision, narrowed.</strong> One deadline stays: an attempt parked on a dependency held over a link whose target
|
||
attempt is known and still activating or parked fails <code>transient</code> after its <code>readyTimeout</code> and retries with backoff;
|
||
its next attempt usually starts after the other side and its chain finds the cycle. It has a cost that decides
|
||
against it: the target being <em>parked</em> is the common case of a chain across hosts waiting for a missing plugin (a
|
||
tab's view on a worker object that waits for an uninstalled type), and those dependents would time out and poll
|
||
again, as today, so "local = remote" would not hold across hosts. Narrowing it to targets that are <code>loading</code> (not
|
||
<code>failed { waiting }</code>) avoids that but misses a real ring: A and B on two hosts wait on each other, B also on a
|
||
missing X; B is <code>failed { waiting }</code>, so A is too; when X appears, both stay <code>failed { waiting }</code> on each other, with
|
||
no deadline and no chain to find it. And even the timeout does not find what it is meant to (round 6, Fable): an
|
||
object's chain is fixed when it is created and its instance id survives retries, so a retried attempt sends the same
|
||
hold with the same chain; two simultaneous starts time out and retry in lockstep and are never found.</li>
|
||
<li><strong>(B) The probe (recommended)</strong>, below: no deadline at all, one new message kind, the most intricate rule set in the
|
||
model. Five
|
||
review rounds found a counterexample in each of the first four drafts; draft 6's version answers all of them, but it
|
||
has not been reviewed again, and slice 7 would have to prove it with tests before the deadline could go.</li>
|
||
</ul>
|
||
<h4>(B) The probe</h4>
|
||
<p>Replaces the activation chain in holds. On one host the <code>self.get</code> walk stays (it is synchronous and exact: a ring
|
||
with no remote edge is found there, where (B1)'s timer does not run; Opus, round 7); the probe serves the rings that
|
||
cross a link. It replaces draft 1's bounded chain too
|
||
draft 1's bounded chain (a ring of 33 is never found with chains cut at 32; chains keep stale ancestry). Draft 5 replaces the
|
||
second round by a check along the recorded path (round 4, Sol: two rounds through different stale paths can report a
|
||
ring that never existed, <code>/tmp/model/sol-probe4.log</code>).</p>
|
||
<ol>
|
||
<li><strong>Attempts, edges, tokens.</strong> Every attempt has its own id (uuidv7 per attempt: a retry or reload is a new
|
||
attempt). A <strong>wait edge</strong> runs from a parked attempt to the parked or activating attempt it waits for, through a
|
||
bound request (unbound waits and registry reads are never edges; they are not deadlocks). Each edge carries an <strong>edge
|
||
token</strong> (round 5, Astra, Sol): a number from a counter per attempt, never reused, replaced by a fresh one on any
|
||
interruption or change of that edge, so two requests to one object are two edges: a new target attempt (also a
|
||
local target's retry, without a re-bind), any <code>loading</code> on a bound or pending hold (a relay's upstream re-hold
|
||
included), an unbinding, a rebinding, a re-hold after a reconnect. An attempt is parked for one interval only (one
|
||
that passed its dependency check never parks again) and ids are never reused. A provider tells its consumers the attempt they wait for on
|
||
every non-live answer: <code>loading { hold, attempt }</code> and a <code>waiting</code> <code>failed { hold, cause, attempt }</code>.</li>
|
||
<li><strong>A search per edge change.</strong> Whenever a parked attempt gets a new edge or a new token on one, it starts a
|
||
search: <code>probe { kind: "search", round, initiator, trail }</code> along each of its wait edges, <code>round</code> the next number of
|
||
a counter the attempt keeps, <code>trail</code> the list of <code>(attempt, edge token)</code> hops it crossed, starting with
|
||
the initiator's own. A ring closes at its last
|
||
edge, and that edge's source starts the search that finds it. Coalesced per batch. Locally a call on the target
|
||
attempt, forwarded through a queue (no recursion); over a link the message <code>probe { hold, to, ... }</code>, consumer to
|
||
provider, relayed by hold id with the origin. <strong>Probes are honoured on parent and route links and dropped on child
|
||
links</strong> (<code>accept</code>), whatever <code>trusted</code> says.</li>
|
||
<li><strong>Forwarding.</strong> An attempt receiving a search drops it if <code>to</code> is not its current attempt or it is not parked. If
|
||
the initiator is itself, the search returned (4). If the attempt is already in the trail, it drops it (a ring that does not contain the initiator is
|
||
found by its own members' searches; Opus). Otherwise it forwards it along every wait edge, appending <code>(itself, the edge's token)</code> to the trail, only if the round number is higher than the one it stored for that
|
||
initiator (older or equal rounds are dropped: the initiator superseded them), and stores it. The seen map holds one
|
||
number per initiator, up to 256 initiators, and goes with the attempt.</li>
|
||
<li><strong>The check.</strong> A returned search is accepted while its initiating attempt is still parked, whatever round it
|
||
carries (round 7, Astra: a re-search must not invalidate an older search still under way); the check decides. The
|
||
initiator then sends <code>probe { kind: "check", round, trail }</code> along the recorded trail: the initiator first verifies its own hop; each hop passes it on only if it is still
|
||
that attempt, still parked, still has the recorded edge token, pointing at the next hop's attempt; otherwise the
|
||
check dies (also on a <code>to</code> mismatch). The check follows the trail by position and bypasses the seen
|
||
map. The initiator verifies its own hop again when the check returns. If the check returns, the initiator fails <code>broken invalid/cycle</code>, the path named for display from the trail,
|
||
and today's propagation breaks the other activating objects on the ring and the dependents off it. 5. <strong>No silent loss.</strong> Dedupe can drop the copy of a search that travelled the ring in favour of a copy whose path
|
||
later breaks (Astra, round 5); then the check dies and nothing is left to find the ring. Round 6 tried <code>dead</code>
|
||
notices back to the initiator; Sol and Astra showed that reliable notices need recovery state per search that
|
||
eviction may not drop and that a long-parked attempt would otherwise keep for every initiator that ever passed (a
|
||
leak), i.e. a full search lifecycle with cancellation. Two ways (<strong>Owner</strong>, part of decision 6):<ul>
|
||
<li><strong>(B1) Re-search with backoff (recommended).</strong> An attempt parked on a remote attempt starts a fresh search again
|
||
after a backoff (1 s doubling to 60 s, reset by an edge change), whether it shows <code>loading</code> or <code>failed { waiting }</code>;
|
||
the timer stops when the attempt leaves parking. No <code>dead</code> messages, no recovery state, eviction
|
||
only costs work; a new search starts within one backoff period, so every stable ring that crosses a link is
|
||
eventually found (no wall-clock bound: delivery is only fair; a ring on one host is the walk's). The timer runs as long as the attempt stays parked on a remote attempt: a view on a worker object that
|
||
waits for a missing plugin re-searches every 60 s until a reload (Fable: cheap, but for as long as it waits). The timer sends probes only: it never
|
||
fails an object, re-holds or polls a provider. Cost: one probe per parked remote dependent per period, which
|
||
crosses the downstream wait graph and dies at its leaves (an unbound wait is not an edge).</li>
|
||
<li><strong>(B2) Events only.</strong> Each search is owned until it settles: branches report completion or abort back along their
|
||
trails, a dropped probe answers <code>dead { initiator, round }</code> with a reverse route, an edge records the searches it
|
||
carries until they settle, and an initiator that ends or starts a new round cancels its old one along the same
|
||
routes. Complete without a timer, bounded by the searches in flight, and the most intricate protocol in Chord; not
|
||
yet specified to the reviewers' satisfaction.
|
||
Rejected: enumerating every simple trail without dedupe (Sol's correctness reference): no ring copy is ever
|
||
suppressed, but work is exponential on chains of diamonds.</li>
|
||
</ul>
|
||
</li>
|
||
</ol>
|
||
<ul>
|
||
<li><strong>Why a reported ring existed:</strong> every edge of the trail kept its token from the moment the search crossed it
|
||
to the moment the check crossed it; the search crossed all of them before the check started, so at the moment the
|
||
check started every edge of the ring existed at once. A token changes on every interruption and is never reused, so an edge cannot
|
||
vanish and come back unnoticed. Sol's counterexample fails at the check: <code>C1</code> is no longer <code>B</code>'s target.</li>
|
||
<li><strong>Why every ring is found</strong> (under an eventually stable wait graph and fair delivery; endless edge churn can keep
|
||
invalidating searches; with rule 5's (B1) or (B2) for a search suppressed behind a path that broke): a ring is closed by its last edge; its source starts a search when the edge appears; every
|
||
other edge exists then and every attempt on the ring waits transitively on the source, so none goes live before the
|
||
search and the check return. Rings of any length, local and remote mixed. On a DAG searches die at the leaves; on a
|
||
diamond the second arrival is dropped.</li>
|
||
<li><strong>What it does not promise:</strong> (1) a ring that existed when the check started may be broken up by a concurrent reload,
|
||
retry, unbinding or revocation just after; it is still reported (it did exist). Its broken objects recover as every
|
||
broken object does: <code>ref.retry()</code> or a reload of their plugin. (2) Linear work under every schedule: with more than
|
||
256 initiators passing one attempt at once, an evicted entry lets a search be forwarded again, so work can grow with
|
||
the number of simple paths (never endlessly: a search never revisits an attempt in its trail). (3) Constant message size: a probe carries its trail, so its size grows with the length of the
|
||
path it crossed (a returning search's trail is as long as its ring); no cap cuts it, since the trail is the proof.</li>
|
||
<li><strong>Cost:</strong> one search per edge change, crossing each downstream edge once, one check per returned search; with (B1),
|
||
one more search per backoff period for every attempt parked on a remote attempt, also while an acyclic chain waits
|
||
for a missing plugin, plus the trail bytes. The bounded memory is the seen map's; probes in flight are bounded by
|
||
the searches started.</li>
|
||
<li><strong>Transition:</strong> today's dependency deadline for activations waiting across links stays until the probe's tests pass
|
||
in slice 7 (long rings, diamonds, a ring closed after a reconnect with unchanged attempt ids, Sol's phantom schedule,
|
||
300 concurrent initiators, edges added and removed, reloads, relays, forged probes); then it goes in the same slice.</li>
|
||
</ul>
|
||
<h3 id="8-getters-holds-calls-one-rule">8. Getters, holds, calls: one rule</h3>
|
||
<ul>
|
||
<li><strong><code>host.hold</code></strong> returns a reference at once; only invalid params throw (also before <code>load</code>). A missing local type
|
||
gives <code>failed { waiting }</code> after the publication, <code>loading</code> before it.</li>
|
||
<li><strong><code>host.get</code>, <code>ctx.get</code> and <code>host.registry</code>:</strong> resolve when live; wait while <code>loading</code>, or <code>failed { transient }</code>
|
||
(its own retry is under way), up to their <code>readyTimeout</code>; reject at once on <code>failed { waiting }</code> or <code>{ broken }</code>. On
|
||
success, rejection or timeout their temporary wait goes; <code>host.registry</code> returns the <code>RegistryValue</code> with its
|
||
ordinary watch ownership (no <code>release()</code>). <strong>Owner:</strong> recommended (nobody waits 60 s for a plugin that is not
|
||
there; code that wants to wait holds instead), or wait through <code>waiting</code> too.</li>
|
||
<li><strong>Calls</strong> on a <code>loading</code> reference park, each bounded by the reference's <code>readyTimeout</code> from when it was made; on a
|
||
<code>failed</code> one they reject at once with the cause; sent calls are never resent.</li>
|
||
</ul>
|
||
<h3 id="9-registries">9. Registries</h3>
|
||
<ul>
|
||
<li>The same resolver on one host: a reader's request for R at its version is bound while R is served and has a usable
|
||
value at that version (composed; mapped by <code>accepts[old].value</code> for an older reader), otherwise unbound
|
||
<code>not_served</code>, <code>version</code> or <code>compose</code>, or <code>loading</code> before the publication. Woken at every transition to usable
|
||
(<code>#valued</code>'s first-value latch goes). S2's <code>broken</code> and S3's backoff go.</li>
|
||
<li><strong>Version skew within one host:</strong> a reload of the registry's plugin alone leaves other plugins' contributors and
|
||
readers on their bundled versions; <code>accepts[old].contribution</code> and <code>.value</code> keep them working, unsupported
|
||
contributions are left out and reported, unsupported readers are <code>waiting</code> (<code>version</code>).</li>
|
||
<li>A registry waits for a value, never for a particular contribution (an empty table composes). After live, a later
|
||
<code>compose</code> failure changes nothing for the reader: <code>.value</code> keeps the last good composition, <strong>but only while every
|
||
contribution it was built from is still in the table</strong> (round 2, Astra N2): when one of them leaves (its
|
||
contributor ended or was reloaded), the last good value is dropped and <code>.value</code> throws until <code>compose</code> succeeds, so
|
||
it never hands out a native whose contributor ended. <code>compose</code> represents a missing contributor as a value.</li>
|
||
<li>Registries stay host-local (contributions are in-process values).</li>
|
||
</ul>
|
||
<h3 id="10-keep-alive-presence-revocation-jobs">10. Keep-alive, presence, revocation, jobs</h3>
|
||
<ul>
|
||
<li>A request over a link counts once in <code>host.activity.references</code> from its arrival to its end, bound or unbound;
|
||
presence (<code>host.references</code>) lists only bound references.</li>
|
||
<li>Revocation ends the requests of a revoked principal, bound and unbound, at every hop; nothing revives them.</li>
|
||
<li><code>gone</code> stays only for revocation (a provider host closing closes its links and sends nothing; its consumers see a
|
||
drop, <code>loading</code>, and hold again on the next link).</li>
|
||
<li>Persisted job runs of a parked host-lifetime object are loaded with its documents; <code>run</code> and <code>finish</code> start only
|
||
once it is live; the run count keeps the object as today.</li>
|
||
</ul>
|
||
<h3 id="11-timeouts-after-the-model">11. Timeouts after the model</h3>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Timeout or backoff</th>
|
||
<th>After</th>
|
||
<th>Why</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody><tr>
|
||
<td><code>readyTimeout</code> of an attempt</td>
|
||
<td>only its own steps; paused while parked</td>
|
||
<td>a slow dependency is bounded where it runs</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>readyTimeout</code> of <code>host.get</code>, <code>ctx.get</code>, <code>host.registry</code></td>
|
||
<td>kept</td>
|
||
<td>a caller gets an answer</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>readyTimeout</code> of a parked call</td>
|
||
<td>kept</td>
|
||
<td>an action must not run long after it was asked for</td>
|
||
</tr>
|
||
<tr>
|
||
<td>identity backoff 1-60 s</td>
|
||
<td>only for the object's own <code>transient</code> failures</td>
|
||
<td>nothing else says the disk or network is back</td>
|
||
</tr>
|
||
<tr>
|
||
<td>dependency backoff, polling of refused holds</td>
|
||
<td>removed</td>
|
||
<td>recovery is an event</td>
|
||
</tr>
|
||
<tr>
|
||
<td>dependency deadline across links</td>
|
||
<td>(A): only for a parked attempt whose remote target attempt stays activating or parked; (B): until the probe's tests pass, then removed</td>
|
||
<td>the simultaneous-cycle gap</td>
|
||
</tr>
|
||
<tr>
|
||
<td>probe re-search backoff (B1)</td>
|
||
<td>new, only with (B1): 1 s doubling to 60 s while parked on a remote attempt</td>
|
||
<td>finds a ring whose search was suppressed; sends probes only, never fails or re-holds</td>
|
||
</tr>
|
||
<tr>
|
||
<td>route backoff, heartbeat, <code>closeTimeout</code>, routine and <code>finish</code> backoffs, <code>linger</code></td>
|
||
<td>kept</td>
|
||
<td>not dependency waits</td>
|
||
</tr>
|
||
<tr>
|
||
<td>rollout install timeout (not built)</td>
|
||
<td>a watchdog on the install only</td>
|
||
<td>never a deadline for dependencies</td>
|
||
</tr>
|
||
</tbody></table>
|
||
<h3 id="12-what-changes-in-the-design">12. What changes in the design</h3>
|
||
<p>R18 (dependency failures <code>waiting</code>, no backoff, the deadline paused while parked, the dependency check as boundary,
|
||
wakes on every change, an ended dependency breaks), S2 and S3 (<code>waiting</code>, woken at every usable value), slice 8's
|
||
last-good rule (dropped when a contribution leaves), §3's destination table (by address; the publication gate), §6 (the
|
||
simultaneous-cycle gap: (A) one narrowed deadline or (B) the probe), §7 (<code>host.hold</code>, getters, calls), §21 (no <code>gone</code> on
|
||
removal; a dropped version unbinds only its requests; installs never await activations; rollout without revision
|
||
fields; the reload handover), the start barrier (R17, W6), <code>FailureCause</code> (+ <code>waiting</code>), <code>loading</code>/<code>failed</code> with the
|
||
attempt id, one message kind (<code>probe</code>, tagged <code>search</code> or <code>check</code>). Unchanged: what an object's own code reload does (§21), R18 after live,
|
||
<code>retryOn</code> outside parking, <code>self.follow</code>'s R4, the call window and flow control, revocation, no client providers.</p>
|
||
<h3 id="13-open-for-the-owner">13. Open for the owner</h3>
|
||
<ol>
|
||
<li>The barrier: (a) or (b) (§6.3).</li>
|
||
<li>Getters on <code>waiting</code>: reject at once (recommended) or wait up to <code>readyTimeout</code> (§8).</li>
|
||
<li><code>waiting</code> as a new cause kind (recommended) or not (§2).</li>
|
||
<li>A pure swap whose new factory declares a dependency that is not there: the object parks (its gate closes, holders
|
||
see <code>loading</code> or <code>failed { waiting }</code>, calls park or reject; recommended) or stays on the old code until it
|
||
resolves (old and new code would overlap without bound, against §21's "a host is at R or R−1").</li>
|
||
<li><strong>The last good value</strong> (§9): kept only while every contribution it was built from is still in the table (draft;
|
||
Astra), or kept as slice 8 decided, with natives in a registry looked up at use through their owner (Sol).</li>
|
||
<li><strong>Cycles across hosts</strong> (§7): (B) the probe, with (B1) a re-search backoff for probes only (recommended) or (B2) a
|
||
full search lifecycle without any timer; or (A) the slice 4b decision narrowed to one deadline, which polls chains
|
||
across hosts again, misses a ring of <code>waiting</code> attempts, and retries in lockstep without finding the cycle it is for.</li>
|
||
</ol>
|
||
</section>
|
||
|
||
<p class="colophon">Parked Until Resolved · first edition · 2026-10-29 · Chord v2 design proposal for slice 7</p>
|
||
</div></div>
|
||
<script>(function activate(root) {
|
||
const replay = (f) => {
|
||
f.classList.remove("play");
|
||
void f.getBoundingClientRect();
|
||
f.classList.add("play");
|
||
};
|
||
const io = new IntersectionObserver(
|
||
(es) => {
|
||
for (const e of es)
|
||
if (e.isIntersecting) {
|
||
e.target.classList.add("play");
|
||
io.unobserve(e.target);
|
||
}
|
||
},
|
||
{ threshold: 0.3 },
|
||
);
|
||
root.querySelectorAll(".anim:not(.armed)").forEach((f) => {
|
||
f.classList.add("armed");
|
||
io.observe(f);
|
||
const b = f.querySelector(".replay");
|
||
if (b) b.onclick = () => replay(f);
|
||
});
|
||
root.querySelectorAll(".steps:not(.js)").forEach((s) => {
|
||
s.classList.add("js");
|
||
const ps = [...s.querySelectorAll(":scope>.step-panel")];
|
||
let i = 0;
|
||
const c = s.querySelector(".count"),
|
||
p = s.querySelector(".prev"),
|
||
n = s.querySelector(".next");
|
||
const show = () => {
|
||
ps.forEach((x, j) => x.classList.toggle("on", j === i));
|
||
c.textContent = `${i + 1} / ${ps.length}`;
|
||
p.disabled = i === 0;
|
||
n.disabled = i === ps.length - 1;
|
||
ps[i].querySelectorAll(".anim").forEach(replay);
|
||
};
|
||
p.onclick = () => {
|
||
i--;
|
||
show();
|
||
};
|
||
n.onclick = () => {
|
||
i++;
|
||
show();
|
||
};
|
||
show();
|
||
});
|
||
})(document)</script>
|
||
</body></html>
|