libfx 0.0.15 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/durable/world.mjs CHANGED
@@ -1 +1,4 @@
1
1
  var c=Symbol.for("libfx.durability");function T(r,f={}){let u=typeof r==="function";if(!u&&!(r&&typeof r==="object"))throw TypeError("world() takes a World or a function that creates one");if(!f||typeof f!=="object"||Array.isArray(f))throw TypeError("world() options must be an object");let{name:e="world",store:d=e,livenessKnown:l=!1,queueLeases:s=!1,runInRequest:w=s,waitUntil:y,queueDurable:h=!1,pollMs:t,leaseMs:o,maxDurationMs:b,reserveMs:p,alive:a,holderInfo:m,gatewayKey:g}=f;if(typeof e!=="string"||e.length===0)throw TypeError("world() name must be a non-empty string");if(typeof d!=="string"||d.length===0)throw TypeError(`${e}() store must be a non-empty string`);for(let[i,n]of Object.entries({livenessKnown:l,queueLeases:s,runInRequest:w,queueDurable:h}))if(typeof n!=="boolean")throw TypeError(`${e}() ${i} must be a boolean`);if(t!==void 0&&!(Number.isSafeInteger(t)&&t>0))throw TypeError(`${e}() pollMs must be a positive integer`);if(o!==void 0&&!(Number.isSafeInteger(o)&&o>=1000))throw TypeError(`${e}() leaseMs must be an integer of at least 1000`);for(let[i,n]of Object.entries({maxDurationMs:b,reserveMs:p}))if(n!==void 0&&!(Number.isSafeInteger(n)&&n>=0))throw TypeError(`${e}() ${i} must be a non-negative integer`);for(let[i,n]of Object.entries({alive:a,holderInfo:m,gatewayKey:g,waitUntil:y}))if(n!==void 0&&typeof n!=="function")throw TypeError(`${e}() ${i} must be a function`);if(l&&!a)throw TypeError(`${e}() livenessKnown needs alive()`);if(w&&!s)throw TypeError(`${e}() runInRequest needs queueLeases`);return{[c]:!0,name:e,store:d,world:u?r:()=>r,owned:u,livenessKnown:l,queueLeases:s,runInRequest:w,queueDurable:h,...t===void 0?{}:{pollMs:t},...o===void 0?{}:{leaseMs:o},...b===void 0?{}:{maxDurationMs:b},...p===void 0?{}:{reserveMs:p},...a===void 0?{}:{alive:a},...m===void 0?{}:{holderInfo:m},...g===void 0?{}:{gatewayKey:g},...y===void 0?{}:{waitUntil:y}}}export{T as world};
2
+
3
+ //# debugId=B929A0E4F469F75F64756E2164756E21
4
+ //# sourceMappingURL=world.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["world.mjs"],"mappings":"AAIA,IAAM,EAAgB,OAAO,IAAI,kBAAkB,EAwC5C,SAAS,CAAK,CAAC,EAAQ,EAAU,CAAC,EAAG,CAC1C,IAAM,EAAQ,OAAO,IAAW,WAChC,GAAI,CAAC,GAAS,EAAE,GAAU,OAAO,IAAW,UAAW,MAAU,UAAU,sDAAsD,EACjI,GAAI,CAAC,GAAW,OAAO,IAAY,UAAY,MAAM,QAAQ,CAAO,EAAG,MAAU,UAAU,mCAAmC,EAC9H,IAAQ,OAAO,QAAS,QAAQ,EAAM,gBAAgB,GAAO,cAAc,GAAO,eAAe,EAAa,YAAW,eAAe,GAAO,SAAQ,UAAS,gBAAe,YAAW,QAAO,aAAY,cAAe,EAC5N,GAAI,OAAO,IAAS,UAAY,EAAK,SAAW,EAAG,MAAU,UAAU,yCAAyC,EAChH,GAAI,OAAO,IAAU,UAAY,EAAM,SAAW,EAAG,MAAU,UAAU,GAAG,sCAAyC,EACrH,QAAY,EAAK,KAAU,OAAO,QAAQ,CAAE,gBAAe,cAAa,eAAc,cAAa,CAAC,EAClG,GAAI,OAAO,IAAU,UAAW,MAAU,UAAU,GAAG,OAAU,qBAAuB,EAE1F,GAAI,IAAW,QAAa,EAAE,OAAO,cAAc,CAAM,GAAK,EAAS,GAAI,MAAU,UAAU,GAAG,uCAA0C,EAE5I,GAAI,IAAY,QAAa,EAAE,OAAO,cAAc,CAAO,GAAK,GAAW,MAAO,MAAU,UAAU,GAAG,iDAAoD,EAC7J,QAAY,EAAK,KAAU,OAAO,QAAQ,CAAE,gBAAe,WAAU,CAAC,EACpE,GAAI,IAAU,QAAa,EAAE,OAAO,cAAc,CAAK,GAAK,GAAS,GAAI,MAAU,UAAU,GAAG,OAAU,kCAAoC,EAEhJ,QAAY,EAAK,KAAU,OAAO,QAAQ,CAAE,QAAO,aAAY,aAAY,WAAU,CAAC,EACpF,GAAI,IAAU,QAAa,OAAO,IAAU,WAAY,MAAU,UAAU,GAAG,OAAU,sBAAwB,EAEnH,GAAI,GAAiB,CAAC,EAAO,MAAU,UAAU,GAAG,iCAAoC,EAGxF,GAAI,GAAgB,CAAC,EAAa,MAAU,UAAU,GAAG,oCAAuC,EAChG,MAAO,EACJ,GAAgB,GACjB,OACA,QACA,MAAO,EAAQ,EAAS,IAAM,EAC9B,QACA,gBACA,cACA,eACA,kBACI,IAAW,OAAY,CAAC,EAAI,CAAE,QAAO,KACrC,IAAY,OAAY,CAAC,EAAI,CAAE,SAAQ,KACvC,IAAkB,OAAY,CAAC,EAAI,CAAE,eAAc,KACnD,IAAc,OAAY,CAAC,EAAI,CAAE,WAAU,KAC3C,IAAU,OAAY,CAAC,EAAI,CAAE,OAAM,KACnC,IAAe,OAAY,CAAC,EAAI,CAAE,YAAW,KAC7C,IAAe,OAAY,CAAC,EAAI,CAAE,YAAW,KAC7C,IAAc,OAAY,CAAC,EAAI,CAAE,WAAU,CACjD","debugId":"B929A0E4F469F75F64756E2164756E21","names":[]}
package/durable.js CHANGED
@@ -747,6 +747,11 @@ function slotOf(event) {
747
747
  return Number(match[1]);
748
748
  }
749
749
 
750
+ // The slot of the event a write returned, or NaN when the World returned none.
751
+ function slotWritten(result) {
752
+ return Number(eventIdPattern.exec(String(result?.event?.eventId ?? ""))?.[1]);
753
+ }
754
+
750
755
  function logEntryOf(event) {
751
756
  if (event?.eventType !== "step_created" || event.eventData?.stepName !== logStep) return null;
752
757
  const input = event.eventData.input;
@@ -766,6 +771,7 @@ const missingStreamFastMs = 2000;
766
771
  // may hold together, least recently read dropped first.
767
772
  const maxLogViews = 32;
768
773
  const maxLogViewChars = 32 * 1024 * 1024;
774
+ const maxMintedIds = 1024;
769
775
 
770
776
  // Whether a newest-first listing showed every slot from the lowest it
771
777
  // listed to the newest. Pure.
@@ -1098,24 +1104,41 @@ function createWorldBackend(world, durability) {
1098
1104
  }
1099
1105
  return pending;
1100
1106
  };
1107
+ // The newest slot the last read() returned every entry through, or
1108
+ // null when that read could not show it had them all: 0 when it listed
1109
+ // nothing, as for a run not created yet.
1110
+ let covered = null;
1111
+ // The run's creation events hold no entry, so a read that showed every
1112
+ // entry through the slot before one still does through it.
1113
+ const passOver = (slot) => {
1114
+ if (covered !== null && slot === covered + 1) covered = slot;
1115
+ };
1116
+ // Resolves true when this call's own `run_created` made the run: the
1117
+ // World creates a run only when none exists (a second one is refused,
1118
+ // 409), and its first event takes the first slot. A run that did not
1119
+ // exist held no event and no stream.
1101
1120
  const createRun = async () => {
1121
+ let made = false;
1102
1122
  try {
1103
1123
  const deploymentId = typeof world.getDeploymentId === "function" ? await world.getDeploymentId() : "libfx";
1104
- await world.events.create(runId, {
1124
+ const slot = slotWritten(await world.events.create(runId, {
1105
1125
  eventType: "run_created",
1106
1126
  ...spec,
1107
1127
  eventData: { deploymentId, workflowName: "libfx/session", input: encoder.encode("{}") },
1108
- });
1128
+ }));
1129
+ made = slot === firstEventSlot;
1130
+ passOver(slot);
1109
1131
  } catch (error) {
1110
1132
  // Another worker created it first.
1111
1133
  if (!/exist|conflict|409|already/i.test(String(error?.message ?? "")) && error?.status !== 409) throw error;
1112
1134
  }
1113
1135
  try {
1114
- await world.events.create(runId, { eventType: "run_started", ...spec });
1136
+ passOver(slotWritten(await world.events.create(runId, { eventType: "run_started", ...spec })));
1115
1137
  } catch (error) {
1116
1138
  if (!/exist|conflict|409|already|started|status/i.test(String(error?.message ?? "")) && error?.status !== 409) throw error;
1117
1139
  }
1118
1140
  created.add(runId);
1141
+ return made;
1119
1142
  };
1120
1143
  // Events newest first, down to and excluding slot `stopBelow`.
1121
1144
  const newestFirst = async (stopBelow, stopAtCheckpoint) => {
@@ -1175,9 +1198,6 @@ function createWorldBackend(world, durability) {
1175
1198
  cursor = page.cursor;
1176
1199
  }
1177
1200
  };
1178
- // The newest slot the last read() returned every entry through, or
1179
- // null when that read could not show it had them all.
1180
- let covered = null;
1181
1201
  const fullRead = async () => {
1182
1202
  const read = await newestFirst(0, true);
1183
1203
  const entries = read.events.reverse();
@@ -1191,12 +1211,31 @@ function createWorldBackend(world, durability) {
1191
1211
  covered = read.newest;
1192
1212
  } else {
1193
1213
  forgetView(runId);
1194
- covered = null;
1214
+ covered = read.missing || read.listed === 0 ? 0 : null;
1195
1215
  }
1196
1216
  return entries;
1197
1217
  };
1198
1218
  return {
1199
1219
  newest: () => covered,
1220
+ // Creates the run, as its first append would, and resolves true only
1221
+ // when this call's own write made it: the log was then empty and its
1222
+ // stream had no line, as a read just before the write would have
1223
+ // shown, so the sender need not read either. False, without writing,
1224
+ // when this process already created the run or is creating it; false,
1225
+ // after the same writes, when the run existed or the World cannot show
1226
+ // which. The caller then reads as before.
1227
+ async fresh() {
1228
+ if (created.has(runId) || creating.has(runId)) return false;
1229
+ // As a read that found no run leaves it, so the creation events
1230
+ // this write lands pass it over. Of a run it did not make, this
1231
+ // object knows nothing; the caller then reads it.
1232
+ covered = 0;
1233
+ const made = await ensureRun();
1234
+ if (!made) covered = null;
1235
+ // As a read that found no run notes it.
1236
+ if (made) noteSnapshot({ seq: 0, seqInBand: inBandAtRunCreation }, "null");
1237
+ return made;
1238
+ },
1200
1239
  async read() {
1201
1240
  const view = viewOf(runId);
1202
1241
  if (!view) return fullRead();
@@ -1596,7 +1635,9 @@ async function recordMessage(log, state, message) {
1596
1635
  * The log a sender read and then extended with its own entries, as the
1597
1636
  * worker it starts in the same request may use it instead of reading the
1598
1637
  * log again. Only when the sender's read showed every entry through slot
1599
- * `newestSeen`, and every entry the sender then wrote took the next slot:
1638
+ * `newestSeen` (for a run it found missing, the run's creation events it
1639
+ * then saw land hold none), and every entry the sender then wrote took the
1640
+ * next slot:
1600
1641
  * then nothing landed in between, so the view holds every input that comes
1601
1642
  * before this message's. The sender's entries go on as the JSON the World
1602
1643
  * stored, not as the objects it wrote. Null otherwise. Pure.
@@ -1673,9 +1714,22 @@ class SessionWorker {
1673
1714
  this.sessionId = sessionId;
1674
1715
  // Set while this process runs the session; each run holds its own lease.
1675
1716
  this.running = false;
1676
- // Deliveries in progress for the session in this process.
1717
+ // How many claims this worker has started. A read taken at a lower count
1718
+ // may predate a turn that took the inputs it shows pending.
1719
+ this.claimsStarted = 0;
1720
+ // Deliveries in progress for the session in this process, and senders
1721
+ // about to hand one a read (see consume).
1677
1722
  this.deliveries = 0;
1678
1723
  this.holder = null;
1724
+ // A write-free early claim can give way to a backed delivery. Once its
1725
+ // backstop succeeds, it is a normal claim and cannot be displaced here.
1726
+ this.opening = null;
1727
+ }
1728
+
1729
+ // Taken just before a read of the log, for consume to tell whether a turn
1730
+ // ran here since: the claims started, or null while one runs.
1731
+ mark() {
1732
+ return this.running ? null : this.claimsStarted;
1679
1733
  }
1680
1734
 
1681
1735
  // The log folded as `delivery` judges whether a lease's holder lives.
@@ -1693,15 +1747,37 @@ class SessionWorker {
1693
1747
  // have the same message delivered again later.
1694
1748
  // `handoff` is the sender's own session object and the log it read and
1695
1749
  // extended, when the request that took the prompt runs it (see
1696
- // handoffEntries); the worker starts from them instead of reading again.
1697
- async consume(message, delivery = null, handoff = null) {
1750
+ // handoffEntries), with `seen`, what mark() said before that read; the
1751
+ // worker starts from them instead of reading again.
1752
+ // `backstop`, from that request too, is its backstop's send still in
1753
+ // flight, which settles with `delivery`. Until it does, the worker goes on
1754
+ // only to an early start, which writes nothing before it (see claim). A
1755
+ // send that failed ends the delivery; its sender fails the prompt.
1756
+ async consume(message, delivery = null, handoff = null, backstop = null) {
1698
1757
  const log = handoff?.log ?? await this.backend.session(this.sessionId);
1758
+ // The claims this worker had started when the read began, or null when
1759
+ // one was running then, whose writes it may not show.
1760
+ let seen = handoff ? handoff.seen : this.mark();
1699
1761
  let entries = handoff?.entries ?? await log.read();
1700
1762
  let state = this.fold(entries, delivery);
1701
1763
  if (!knownMessage(state, message)) {
1702
1764
  this.agent.emit("session.refused", { sessionId: this.sessionId, messageId: message.messageId, reason: "unknown message" });
1703
1765
  return undefined;
1704
1766
  }
1767
+ // Folded again with the backstop's delivery, which can only end a lease
1768
+ // that stood without it (see redelivers).
1769
+ const awaitBackstop = async () => {
1770
+ if (!backstop) return true;
1771
+ try {
1772
+ delivery = await backstop;
1773
+ } catch {
1774
+ return false;
1775
+ } finally {
1776
+ backstop = null;
1777
+ }
1778
+ state = this.fold(entries, delivery);
1779
+ return true;
1780
+ };
1705
1781
  await this.backend.refreshDeadline?.();
1706
1782
  // When this delivery's function stops, if it does.
1707
1783
  const deadline = this.backend.deadline() ?? (this.backend.maxDurationMs === null ? null : Date.now() + this.backend.maxDurationMs);
@@ -1710,20 +1786,48 @@ class SessionWorker {
1710
1786
  // to another agent's session does, goes on to it.
1711
1787
  const owner = state.agent ?? agentOf(message);
1712
1788
  if (owner !== this.agent.name) {
1789
+ if (!(await awaitBackstop())) return undefined;
1713
1790
  await this.backend.queue.send(queuedMessage({ ...message, agent: owner }, this.backend.store), {
1714
1791
  idempotencyKey: `${message.sessionId}:${message.type}:${message.messageId}:to:${owner}`,
1715
1792
  });
1716
1793
  this.agent.emit("session.forwarded", { sessionId: this.sessionId, agent: owner });
1717
1794
  return undefined;
1718
1795
  }
1796
+ // A turn this worker ran since the read may have run this message,
1797
+ // which the read still shows pending; starting from it would send the
1798
+ // turn's model request twice. A turn still running answers below.
1799
+ const caughtUp = async () => {
1800
+ while (!this.running && seen !== this.claimsStarted) {
1801
+ seen = this.claimsStarted;
1802
+ entries = await log.read();
1803
+ state = this.fold(entries, delivery);
1804
+ }
1805
+ };
1719
1806
  for (let claims = 0; claims < 8; claims += 1) {
1807
+ await caughtUp();
1720
1808
  // A session with no open turn starts its next prompt at once: the
1721
1809
  // first model request goes out while the lease is written, and
1722
1810
  // nothing else is written until it lands.
1723
- const early = this.running || state.lease ? null : earlyStart(entries, state, message, (items) => this.fold(items, delivery));
1811
+ const startable = () => (this.running || state.lease ? null : earlyStart(entries, state, message, (items) => this.fold(items, delivery)));
1812
+ let early = startable();
1813
+ if (!early) {
1814
+ if (!(await awaitBackstop())) return undefined;
1815
+ // Only a write-free opener may be displaced. Never await a live
1816
+ // claim here: its run/turn may be waiting for this delivery's steer.
1817
+ const opening = this.opening;
1818
+ if (opening) {
1819
+ opening.stop();
1820
+ await opening.stopped;
1821
+ }
1822
+ // Fold the durable log again after displacement: this delivery's
1823
+ // backstop can recover earlier stored inputs too, in log order.
1824
+ await caughtUp();
1825
+ early = startable();
1826
+ }
1724
1827
  let outcome;
1725
1828
  if (early) {
1726
- outcome = await this.claim(log, state, deadline, early, delivery);
1829
+ outcome = await this.claim(log, state, deadline, early, delivery, backstop);
1830
+ if (!(await awaitBackstop())) return undefined;
1727
1831
  } else {
1728
1832
  if (this.running) return { timeoutSeconds: runningBackstopSeconds };
1729
1833
  if (!state.hasWork) {
@@ -1752,6 +1856,7 @@ class SessionWorker {
1752
1856
  }
1753
1857
  // The turns ran, or another worker's write came before this claim:
1754
1858
  // read the log again.
1859
+ seen = this.mark();
1755
1860
  entries = await log.read();
1756
1861
  state = this.fold(entries, delivery);
1757
1862
  }
@@ -1762,12 +1867,16 @@ class SessionWorker {
1762
1867
  // the chain there, and runs it. With `early`, what `earlyStart` returned,
1763
1868
  // the message's input is written with the lease and the next turn starts
1764
1869
  // before either lands. The lease names `delivery`, so a later delivery of
1765
- // the same message can tell it lost its holder. Returns what run()
1766
- // returns, or "refused" when another worker's write came before the claim.
1767
- async claim(log, state, deadline, early, delivery = null) {
1870
+ // the same message can tell it lost its holder. With `backstop`, the send
1871
+ // that names that delivery, still in flight: every write waits for it, so
1872
+ // the lease names it, and the engine opens meanwhile. Returns what run()
1873
+ // returns, "refused" when another worker's write came before the claim,
1874
+ // or "unqueued" when the backstop failed or the write-free opener gave way.
1875
+ async claim(log, state, deadline, early, delivery = null, backstop = null) {
1768
1876
  // Claimed before the first await, so a delivery that arrives meanwhile
1769
1877
  // sees the session running here.
1770
1878
  this.running = true;
1879
+ this.claimsStarted += 1;
1771
1880
  this.holder = newId("wkr");
1772
1881
  const lease = {
1773
1882
  k: "lease",
@@ -1779,11 +1888,29 @@ class SessionWorker {
1779
1888
  ...(delivery ? { delivery } : {}),
1780
1889
  };
1781
1890
  this.backend.holding(this.holder, true);
1891
+ let unqueued = false;
1892
+ let opening = null;
1893
+ let stopped;
1894
+ let displaced = false;
1895
+ const revoked = backstop ? new Error("early opener displaced by a backed delivery") : null;
1896
+ const giveWay = backstop ? new Promise((_, reject) => {
1897
+ opening = {
1898
+ stopped: new Promise((resolve) => { stopped = resolve; }),
1899
+ stop: () => { displaced = true; reject(revoked); },
1900
+ };
1901
+ this.opening = opening;
1902
+ }) : null;
1903
+ const sent = backstop ? Promise.race([backstop, giveWay]).then((delivered) => {
1904
+ if (displaced) throw revoked;
1905
+ this.opening = null;
1906
+ if (delivered) lease.delivery = delivered;
1907
+ }) : null;
1908
+ void sent?.catch(() => { unqueued = true; });
1782
1909
  try {
1783
1910
  // Both writes go out at once and neither waits for the other; the
1784
1911
  // claim settles once both have, with the lease's cursor if it landed
1785
- // and the first failure.
1786
- const writes = [log.append(lease), ...(early?.writes ?? []).map((entry) => log.append(entry))];
1912
+ // and the first failure. A failed backstop fails both unmade.
1913
+ const writes = [lease, ...(early?.writes ?? [])].map((entry) => (sent ? sent.then(() => log.append(entry)) : log.append(entry)));
1787
1914
  const claimed = Promise.allSettled(writes).then(async ([leased, ...rest]) => {
1788
1915
  const head = leased.status === "fulfilled" ? leased.value : null;
1789
1916
  const error = leased.status === "rejected" ? leased.reason : rest.find((item) => item.status === "rejected")?.reason ?? null;
@@ -1802,7 +1929,7 @@ class SessionWorker {
1802
1929
  });
1803
1930
  let outcome;
1804
1931
  try {
1805
- outcome = await this.run(log, claimed, lease, deadline, early?.state ?? null);
1932
+ outcome = await this.run(log, claimed, lease, deadline, early?.state ?? null, sent?.then(() => true, () => false));
1806
1933
  } catch (error) {
1807
1934
  if ((await claimed).error === null) throw error;
1808
1935
  }
@@ -1812,14 +1939,17 @@ class SessionWorker {
1812
1939
  // wrote nothing and showed nothing. A fenced one leaves the delivery
1813
1940
  // to read the log and try again; after any other failure the same
1814
1941
  // message runs the session again shortly, once a lease that landed is
1815
- // let go.
1942
+ // let go. One whose backstop failed is its sender's to report.
1816
1943
  if (isFenced(settled.error)) return "refused";
1944
+ if (unqueued) return "unqueued";
1817
1945
  this.agent.emit("session.error", { sessionId: this.sessionId, error: errorSummary(settled.error) });
1818
1946
  if (settled.head !== null) await log.append({ k: "release", a: settled.head, holder: this.holder }).catch(() => {});
1819
1947
  return "failed";
1820
1948
  } finally {
1821
1949
  this.backend.holding(this.holder, false);
1822
1950
  this.running = false;
1951
+ if (this.opening === opening) this.opening = null;
1952
+ stopped?.();
1823
1953
  }
1824
1954
  }
1825
1955
 
@@ -1835,8 +1965,9 @@ class SessionWorker {
1835
1965
  // over. With `early`, the state the session was read in, the first turn
1836
1966
  // starts before the claim lands: every write waits for it, and once it
1837
1967
  // failed nothing more is written and the turn stops. The caller answers
1838
- // for a claim that failed.
1839
- async run(raw, claimed, lease, deadline, early) {
1968
+ // for a claim that failed. `backstopSent`, when given, resolves whether
1969
+ // the backstop the lease names was sent without displacing this opener.
1970
+ async run(raw, claimed, lease, deadline, early, backstopSent = null) {
1840
1971
  const agent = this.agent;
1841
1972
  let head = null;
1842
1973
  let failed = null;
@@ -2009,7 +2140,16 @@ class SessionWorker {
2009
2140
  // What the turn it runs first runs with: the open turn's context and
2010
2141
  // settings, or the next prompt's.
2011
2142
  harnessRun = runOf(state.openTurn ?? state.pending[0]);
2012
- harness = await agent.openHarness(this.sessionId, store, harnessRun, ready);
2143
+ const opening = agent.openHarness(this.sessionId, store, harnessRun, ready);
2144
+ // A failed backstop does not wait out a slow open: its sender fails
2145
+ // the prompt once this worker stops. The late engine closes in the
2146
+ // background, and the agent's close() waits for it.
2147
+ if (backstopSent && !(await Promise.race([backstopSent, opening.then(() => true, () => true)]))) {
2148
+ agent.track(opening.then((late) => late.close()).catch(() => {}));
2149
+ await ready;
2150
+ return broken();
2151
+ }
2152
+ harness = await opening;
2013
2153
  } catch (error) {
2014
2154
  // The image store failed to answer while the engine read the
2015
2155
  // session's images: as when the log can't be read, the delivery
@@ -2026,6 +2166,14 @@ class SessionWorker {
2026
2166
  agent.emit("session.error", { sessionId: this.sessionId, error: errorSummary(error) });
2027
2167
  return "done";
2028
2168
  }
2169
+ // The opener is inert until its own backstop succeeds. A displaced
2170
+ // opener closes without taking a turn or writing anything.
2171
+ if (backstopSent && !(await backstopSent)) {
2172
+ agent.track(harness.close().catch(() => {}));
2173
+ harness = null;
2174
+ await ready;
2175
+ return broken();
2176
+ }
2029
2177
  const yieldAt = deadline === null ? undefined : Math.max(1, deadline - this.backend.reserveMs);
2030
2178
  for (;;) {
2031
2179
  if (failed) return broken();
@@ -2645,6 +2793,15 @@ export function createDurableAgentFactory({ harness, defaultDurability, defaultS
2645
2793
  let closed = false;
2646
2794
  const workers = new Map();
2647
2795
  const active = new Set();
2796
+ // Session ids this process minted whose first prompt has not yet tried
2797
+ // to create their run, newest kept. Only a hint that the run may not
2798
+ // exist: whether it did is what that prompt's own write shows.
2799
+ const minted = new Set();
2800
+ const mint = (id) => {
2801
+ minted.add(id);
2802
+ if (minted.size > maxMintedIds) minted.delete(minted.values().next().value);
2803
+ return id;
2804
+ };
2648
2805
  const backend = () => {
2649
2806
  if (closed) return Promise.reject(new Error(`${label} is closed`));
2650
2807
  backendPromise ??= (async () => {
@@ -2687,17 +2844,27 @@ export function createDurableAgentFactory({ harness, defaultDurability, defaultS
2687
2844
  const member = { open: backend, durability: durabilityOf, definition: definitionOf(harnessOptions), store: durability ? storeOf(durability) : unresolved, defaults: durability ? null : unresolved };
2688
2845
  claimName(agentName, member);
2689
2846
  const registered = joinGroup(agentsByName, agentName, member);
2690
- // One delivery of `message` to the session's worker here: from the
2691
- // queue, with `delivery` naming the queue's message and attempt, or with
2692
- // `inRequest` from the request that took the prompt.
2693
- const receive = (created, message, delivery, { inRequest = false, handoff = null } = {}) => {
2694
- let worker = workers.get(message.sessionId);
2847
+ // The session's worker here, kept until as many letGo() calls.
2848
+ const hold = (created, sessionId) => {
2849
+ let worker = workers.get(sessionId);
2695
2850
  if (!worker) {
2696
- worker = new SessionWorker(agent, created, message.sessionId);
2697
- workers.set(message.sessionId, worker);
2851
+ worker = new SessionWorker(agent, created, sessionId);
2852
+ workers.set(sessionId, worker);
2698
2853
  }
2699
2854
  worker.deliveries += 1;
2700
- let work = worker.consume(message, delivery, handoff);
2855
+ return worker;
2856
+ };
2857
+ const letGo = (worker, sessionId) => {
2858
+ worker.deliveries -= 1;
2859
+ // A worker lives while a delivery runs it.
2860
+ if (worker.deliveries === 0 && !worker.running && workers.get(sessionId) === worker) workers.delete(sessionId);
2861
+ };
2862
+ // One delivery of `message` to the session's worker here: from the
2863
+ // queue, with `delivery` naming the queue's message and attempt, or with
2864
+ // `inRequest` from the request that took the prompt.
2865
+ const receive = (created, message, delivery, { inRequest = false, handoff = null, backstop = null } = {}) => {
2866
+ const worker = hold(created, message.sessionId);
2867
+ let work = worker.consume(message, delivery, handoff, backstop);
2701
2868
  if (inRequest) {
2702
2869
  // No queue delivered this message, so none delivers it again: a turn
2703
2870
  // that must go on later continues in a fresh message. One whose
@@ -2718,9 +2885,7 @@ export function createDurableAgentFactory({ harness, defaultDurability, defaultS
2718
2885
  emit("session.error", { sessionId: message.sessionId, error: errorSummary(error) });
2719
2886
  }).finally(() => {
2720
2887
  active.delete(work);
2721
- worker.deliveries -= 1;
2722
- // A worker lives while a delivery runs it.
2723
- if (worker.deliveries === 0 && !worker.running && workers.get(message.sessionId) === worker) workers.delete(message.sessionId);
2888
+ letGo(worker, message.sessionId);
2724
2889
  });
2725
2890
  return work;
2726
2891
  };
@@ -2741,14 +2906,17 @@ export function createDurableAgentFactory({ harness, defaultDurability, defaultS
2741
2906
  let id = requestedId ?? null;
2742
2907
  let seeded = seed === undefined || requestedId !== undefined;
2743
2908
  const resolved = backend().then((created) => {
2744
- id ??= created.newSessionId();
2909
+ id ??= mint(created.newSessionId());
2745
2910
  if (!created.validSessionId(id)) throw new TypeError(`${created.name} durability cannot hold session id ${JSON.stringify(id)}`);
2746
2911
  return { created, id };
2747
2912
  });
2748
2913
  void resolved.catch(() => {});
2749
2914
  // With `inRequest`, a prompt runs here, in the request that took it,
2750
2915
  // where the durability allows (see `runInRequest`).
2751
- const send = async (message, { inRequest = false } = {}) => {
2916
+ // `fresh` is the log whose own write just created the session's run, so
2917
+ // the log is empty without reading it. That object, not a new one, goes
2918
+ // on: it holds what the creation showed of the log.
2919
+ const send = async (message, { inRequest = false, fresh = null } = {}) => {
2752
2920
  const { created, id: sessionId } = await resolved;
2753
2921
  const extra = {
2754
2922
  ...(seeded ? {} : { seed }),
@@ -2762,35 +2930,64 @@ export function createDurableAgentFactory({ harness, defaultDurability, defaultS
2762
2930
  // a turn runs or as whom (`tests/tla/sdk/TenantIsolation.tla`
2763
2931
  // OnlySentRuns), and a queue that ends with the process loses no
2764
2932
  // input.
2765
- const log = await created.session(sessionId);
2766
- const read = await log.read();
2767
- const newestSeen = log.newest?.();
2768
- const appended = await recordMessage(log, foldSessionLog(read), full);
2769
- const queued = queuedMessage(full, created.store);
2770
- const idempotencyKey = `${sessionId}:${message.type}:${message.messageId}`;
2771
- if (inRequest && created.runInRequest) {
2772
- await created.refreshDeadline?.();
2773
- const deadline = deliveryDeadline(created);
2774
- if (deadline !== null) {
2775
- // This request runs the turn, and the queue keeps the message as
2776
- // a backstop it delivers only after the request's deadline, by
2777
- // when the request has stopped. The lease names the backstop, so
2778
- // if the request died, the backstop takes the session over and
2779
- // continues the turn.
2780
- const delaySeconds = Math.ceil(Math.max(0, deadline - Date.now()) / 1000) + inlineBackstopMarginSeconds;
2781
- const { messageId: backstop } = await created.queue.send(queued, { idempotencyKey, delaySeconds }) ?? {};
2782
- seeded = true;
2783
- const delivery = backstop ? { messageId: backstop, attempt: 0 } : null;
2784
- // receive() reports a failure to the host; the backstop retries it.
2785
- const entries = handoffEntries(read, newestSeen, appended);
2786
- const handoff = entries ? { log, entries } : null;
2787
- const work = receive(created, queued, delivery, { inRequest: true, handoff }).then(() => {}, () => {});
2788
- created.waitUntil?.(work);
2789
- return;
2933
+ const log = fresh ?? await created.session(sessionId);
2934
+ const inline = inRequest && created.runInRequest;
2935
+ // Held from before the read until receive() takes over, so a turn
2936
+ // run here meanwhile is counted on the worker the handoff goes to,
2937
+ // not on one dropped in between (see consume).
2938
+ const worker = inline ? hold(created, sessionId) : null;
2939
+ try {
2940
+ // Taken before this message is written, so a turn here that could
2941
+ // have run it counts, also when a fresh log is not read at all.
2942
+ const seen = worker?.mark() ?? null;
2943
+ // Fresh: the caller's own write just created the run, so its log
2944
+ // held no entry, as a read before that write would have shown.
2945
+ const read = fresh ? [] : await log.read();
2946
+ const appended = await recordMessage(log, foldSessionLog(read), full);
2947
+ // Taken after the writes, which pass it over a run they created.
2948
+ const newestSeen = log.newest?.();
2949
+ const queued = queuedMessage(full, created.store);
2950
+ const idempotencyKey = `${sessionId}:${message.type}:${message.messageId}`;
2951
+ if (inline) {
2952
+ await created.refreshDeadline?.();
2953
+ const deadline = deliveryDeadline(created);
2954
+ if (deadline !== null) {
2955
+ // This request runs the turn, and the queue keeps the message as
2956
+ // a backstop it delivers only after the request's deadline, by
2957
+ // when the request has stopped. The lease names the backstop, so
2958
+ // if the request died, the backstop takes the session over and
2959
+ // continues the turn.
2960
+ const delaySeconds = Math.ceil(Math.max(0, deadline - Date.now()) / 1000) + inlineBackstopMarginSeconds;
2961
+ const backstop = (async () => {
2962
+ const { messageId: backstopId } = await created.queue.send(queued, { idempotencyKey, delaySeconds }) ?? {};
2963
+ return backstopId ? { messageId: backstopId, attempt: 0 } : null;
2964
+ })();
2965
+ void backstop.catch(() => {});
2966
+ // The worker opens its engine while the backstop is sent; its
2967
+ // lease, its writes and its first model request wait for the
2968
+ // send (see claim). receive() reports a failure to the host; the
2969
+ // backstop retries it.
2970
+ const entries = handoffEntries(read, newestSeen, appended);
2971
+ const handoff = entries ? { log, entries, seen } : null;
2972
+ const work = receive(created, queued, null, { inRequest: true, handoff, backstop }).then(() => {}, () => {});
2973
+ created.waitUntil?.(work);
2974
+ try {
2975
+ await backstop;
2976
+ } catch (error) {
2977
+ // The worker has stopped, having stored nothing, before the
2978
+ // caller hears: a retry finds no worker here, as if none ran.
2979
+ await work;
2980
+ throw error;
2981
+ }
2982
+ seeded = true;
2983
+ return;
2984
+ }
2790
2985
  }
2986
+ await created.queue.send(queued, { idempotencyKey });
2987
+ seeded = true;
2988
+ } finally {
2989
+ if (worker) letGo(worker, sessionId);
2791
2990
  }
2792
- await created.queue.send(queued, { idempotencyKey });
2793
- seeded = true;
2794
2991
  };
2795
2992
  // Settles once the latest prompt from this session object with images
2796
2993
  // to store is stored or has failed; null while none has. Each later
@@ -2812,20 +3009,26 @@ export function createDurableAgentFactory({ harness, defaultDurability, defaultS
2812
3009
  try {
2813
3010
  const read = input === undefined || ready === null || aborted() ? input : await ready();
2814
3011
  const message = { type, messageId, ...(input === undefined ? {} : { input: read }) };
3012
+ const { created, id: sessionId } = await resolved;
3013
+ const log = await created.session(sessionId);
3014
+ // The first prompt to a session id this process minted creates
3015
+ // its run before anything reads it. When that write made the
3016
+ // run, the log held no entry and the stream no line: what this
3017
+ // turn would read is known, retried or not, as reads just before
3018
+ // the write would have returned it.
3019
+ const fresh = type === "prompt" && !aborted() && minted.delete(sessionId) && typeof log.fresh === "function" && await log.fresh();
2815
3020
  const queue = () => {
2816
3021
  if (progress) progress.sent = true;
2817
- return send(message, { inRequest: type === "prompt" });
3022
+ return send(message, { inRequest: type === "prompt", fresh: fresh ? log : null });
2818
3023
  };
2819
- const { created, id: sessionId } = await resolved;
2820
- const log = await created.session(sessionId);
2821
3024
  let attaching = type === "resume";
2822
- let from = null;
3025
+ let from = fresh ? 0 : null;
2823
3026
  // The viewer's start is read before the message is queued, so
2824
3027
  // every line this message's own turn writes comes after it, and
2825
3028
  // an end for this id before it can only be from a turn this id
2826
3029
  // already ran.
2827
- if (retried) from = await log.ui.length();
2828
- if (retried && !aborted()) {
3030
+ if (retried && !fresh) from = await log.ui.length();
3031
+ if (retried && !fresh && !aborted()) {
2829
3032
  // The log, for that answer, is read alongside the one queue
2830
3033
  // send.
2831
3034
  const [state] = await Promise.all([
@@ -2841,7 +3044,7 @@ export function createDurableAgentFactory({ harness, defaultDurability, defaultS
2841
3044
  if (outcome.unsure) return { log, from, attaching: true, endWithinMs: retryEndWaitMs, unsure: true };
2842
3045
  return { log, from, attaching: outcome.follow === true };
2843
3046
  }
2844
- if (retried) {
3047
+ if (retried && !fresh) {
2845
3048
  // A turn this id already ran answers from the log, without
2846
3049
  // queueing it again. The viewer's start was read first, so a
2847
3050
  // turn that ends after it has its end in view.
@@ -3145,7 +3348,7 @@ export function createDurableAgentFactory({ harness, defaultDurability, defaultS
3145
3348
  },
3146
3349
  /** An id for a new session, before its first prompt. */
3147
3350
  async newSessionId() {
3148
- return (await backend()).newSessionId();
3351
+ return mint((await backend()).newSessionId());
3149
3352
  },
3150
3353
  /** The agent's name: its sessions' turns run only on agents with this name. */
3151
3354
  get name() {