libfx 0.0.13-dev.1472.g2c7a4f83f06f → 0.0.13-dev.1510.ga878d4900a6a

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/node.cjs CHANGED
@@ -53,6 +53,7 @@ __export(exports_node, {
53
53
  listModels: () => listModels,
54
54
  memory: () => memory,
55
55
  supportsJspi: () => supportsJspi,
56
+ wakeHandler: () => wakeHandler,
56
57
  xtermAdapter: () => xtermAdapter
57
58
  });
58
59
  module.exports = __toCommonJS(exports_node);
@@ -214,8 +215,10 @@ var idRule = "1 to 128 letters, digits, '.', '_' or '-'";
214
215
  var newId = (prefix) => `${prefix}_${crypto.randomUUID().replaceAll("-", "")}`;
215
216
  var defaultReserveMs = 30000;
216
217
  var defaultLeaseMs = 15000;
218
+ var queueHeld = (backend, deadline) => !backend.livenessKnown && backend.queueLeases === true && deadline !== null;
219
+ var heldToDeadline = (backend, deadline) => backend.livenessKnown || queueHeld(backend, deadline);
217
220
  function leaseEnd(backend, deadline) {
218
- if (backend.livenessKnown)
221
+ if (heldToDeadline(backend, deadline))
219
222
  return deadline;
220
223
  const renewed = Date.now() + backend.leaseMs;
221
224
  return deadline === null ? renewed : Math.min(renewed, deadline);
@@ -285,6 +288,7 @@ function foldSessionLog(entries, { now = 0, alive = () => null } = {}) {
285
288
  const records = [];
286
289
  let openTurn = typeof base?.openTurn?.id === "string" ? { id: base.openTurn.id, at: Number(base.openTurn.at) || 0, context: base.openTurn.context ?? null, settings: base.openTurn.settings ?? null } : null;
287
290
  let lastTurnId = base?.lastTurnId ?? null;
291
+ let agent = typeof base?.agent === "string" ? base.agent : base && base.through != null ? defaultAgentName : null;
288
292
  let yielded = base?.yielded === true;
289
293
  const contexts = new Map;
290
294
  const settingsOf = new Map;
@@ -316,6 +320,7 @@ function foldSessionLog(entries, { now = 0, alive = () => null } = {}) {
316
320
  maxCursor = position;
317
321
  switch (entry?.k) {
318
322
  case "input":
323
+ agent ??= typeof entry.agent === "string" ? entry.agent : defaultAgentName;
319
324
  if (seenInputs.has(entry.key))
320
325
  break;
321
326
  seenInputs.add(entry.key);
@@ -332,7 +337,7 @@ function foldSessionLog(entries, { now = 0, alive = () => null } = {}) {
332
337
  break;
333
338
  head = cursor;
334
339
  if (entry.k === "lease") {
335
- lease = { holder: entry.holder, epoch: entry.epoch, expiresAt: entry.expiresAt ?? null, pid: entry.pid, host: entry.host, cursor };
340
+ lease = { holder: entry.holder, epoch: entry.epoch, expiresAt: entry.expiresAt ?? null, pid: entry.pid, host: entry.host, delivery: entry.delivery, cursor };
336
341
  if (Number.isSafeInteger(entry.epoch) && entry.epoch > epoch)
337
342
  epoch = entry.epoch;
338
343
  } else if (entry.k === "release") {
@@ -427,6 +432,7 @@ function foldSessionLog(entries, { now = 0, alive = () => null } = {}) {
427
432
  cutoffs: openTurn === null ? 0 : cut.cutoffs,
428
433
  openFailed,
429
434
  lastTurnId,
435
+ agent,
430
436
  yielded,
431
437
  pending,
432
438
  steers,
@@ -452,11 +458,19 @@ function leaseStands(lease, now, alive) {
452
458
  return false;
453
459
  return true;
454
460
  }
461
+ function deliveryOf(meta) {
462
+ const messageId = meta?.messageId;
463
+ const attempt = meta?.attempt;
464
+ return typeof messageId === "string" && messageId.length > 0 && Number.isSafeInteger(attempt) && attempt > 0 ? { messageId, attempt } : null;
465
+ }
466
+ function redelivers(delivery, lease) {
467
+ const held = lease?.delivery;
468
+ return Boolean(delivery && held && held.messageId === delivery.messageId && Number.isSafeInteger(held.attempt) && delivery.attempt >= held.attempt);
469
+ }
455
470
  function createMemoryBackend({ maxDurationMs, reserveMs } = {}) {
456
471
  const sessions = new Map;
457
472
  const holders = liveHolders;
458
- const handlers = [];
459
- let turnOf = 0;
473
+ const handlers = new Map;
460
474
  const timers = new Set;
461
475
  const sessionFor = (id) => {
462
476
  let session = sessions.get(id);
@@ -473,21 +487,28 @@ function createMemoryBackend({ maxDurationMs, reserveMs } = {}) {
473
487
  }, ms);
474
488
  timers.add(timer);
475
489
  };
476
- const deliver = (message, attempt) => {
477
- if (handlers.length === 0)
478
- return later(() => deliver(message, attempt), 10);
479
- const handler = handlers[turnOf++ % handlers.length];
480
- Promise.resolve().then(() => handler(message, { attempt })).then((result) => {
490
+ const deliver = (message, attempt, messageId = newId("qmsg")) => {
491
+ if (handlers.size === 0)
492
+ return later(() => deliver(message, attempt, messageId), 10);
493
+ const group = handlers.get(agentOf(message));
494
+ if (!group) {
495
+ if (attempt < 48)
496
+ later(() => deliver(message, attempt + 1, messageId), 1000);
497
+ return;
498
+ }
499
+ const handler = group.members[group.turn++ % group.members.length];
500
+ Promise.resolve().then(() => handler(message, { messageId, attempt })).then((result) => {
481
501
  if (Number.isFinite(result?.timeoutSeconds))
482
- later(() => deliver(message, attempt + 1), result.timeoutSeconds * 1000);
502
+ later(() => deliver(message, attempt + 1, messageId), result.timeoutSeconds * 1000);
483
503
  }, () => {
484
504
  if (attempt < 48)
485
- later(() => deliver(message, attempt + 1), 1000);
505
+ later(() => deliver(message, attempt + 1, messageId), 1000);
486
506
  });
487
507
  };
488
508
  return {
489
509
  name: "memory",
490
510
  livenessKnown: true,
511
+ runInRequest: false,
491
512
  queueDurable: false,
492
513
  pollMs: null,
493
514
  reserveMs: reserveMs ?? defaultReserveMs,
@@ -580,13 +601,11 @@ function createMemoryBackend({ maxDurationMs, reserveMs } = {}) {
580
601
  later(() => deliver(message, 1), delaySeconds * 1000);
581
602
  }
582
603
  },
583
- listen(fn) {
584
- handlers.push(fn);
604
+ listen(fn, name = defaultAgentName) {
605
+ const group = joinGroup(handlers, name, fn);
585
606
  return () => {
586
- const index = handlers.indexOf(fn);
587
- if (index >= 0)
588
- handlers.splice(index, 1);
589
- if (handlers.length > 0)
607
+ leaveGroup(handlers, name, group, fn);
608
+ if (handlers.size > 0)
590
609
  return;
591
610
  for (const timer of timers)
592
611
  clearTimeout(timer);
@@ -598,6 +617,9 @@ function createMemoryBackend({ maxDurationMs, reserveMs } = {}) {
598
617
  var logStep = "fx.log";
599
618
  var uiStreamOf = (runId) => `${runId.replace(/^wrun_/, "strm_")}_user_bGliZngtdWk`;
600
619
  var queuePrefix = "__libfx_wkf_workflow_";
620
+ var defaultAgentName = "default";
621
+ var agentNameRule = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
622
+ var agentOf = (message) => typeof message?.agent === "string" ? message.agent : defaultAgentName;
601
623
  var queueName = `${queuePrefix}session`;
602
624
  var eventIdPattern = /^[a-z]+_(\d{26})$/;
603
625
  function ulid(now = Date.now()) {
@@ -610,6 +632,13 @@ function ulid(now = Date.now()) {
610
632
  random += alphabet[byte % 32];
611
633
  return time + random;
612
634
  }
635
+ function chainedKind(k) {
636
+ return k === "lease" || k === "release" || k === "record";
637
+ }
638
+ var inBandAtRunCreation = 1;
639
+ function inBandSuperseded(error) {
640
+ return error?.name === "InBandSupersededError" || error?.code === "in-band-superseded" || error?.status === 412;
641
+ }
613
642
  function slotOf(event) {
614
643
  const match = eventIdPattern.exec(String(event?.eventId ?? ""));
615
644
  if (!match)
@@ -635,7 +664,56 @@ var missingStreamFastMs = 2000;
635
664
  var firstPageEvents = 10;
636
665
  var laterPageEvents = 100;
637
666
  var listenersOf = new WeakMap;
638
- function createWorldBackend(world, { name, reserveMs, pollMs, leaseMs, alive, holderInfo, livenessKnown = false, queueDurable = false, maxDurationMs, owned = true }) {
667
+ function joinGroup(groups, name, member) {
668
+ let group = groups.get(name);
669
+ if (!group)
670
+ groups.set(name, group = { members: [], turn: 0 });
671
+ group.members.push(member);
672
+ return group;
673
+ }
674
+ function leaveGroup(groups, name, group, member) {
675
+ const index = group.members.indexOf(member);
676
+ if (index >= 0)
677
+ group.members.splice(index, 1);
678
+ if (group.members.length === 0 && groups.get(name) === group)
679
+ groups.delete(name);
680
+ }
681
+ function noAgentNamed(name) {
682
+ return Object.assign(new Error(`no libfx agent named ${JSON.stringify(name)} runs in this process; the queue delivers this again`), { code: "FX_NO_AGENT", agent: name });
683
+ }
684
+ var agentsByName = globalThis[Symbol.for("libfx.durableAgents")] ??= new Map;
685
+ async function agentNamed(name, matches) {
686
+ const group = agentsByName.get(name);
687
+ if (!group)
688
+ return null;
689
+ const { members } = group;
690
+ const first = group.turn++;
691
+ for (let index = 0;index < members.length; index += 1) {
692
+ const member = members[(first + index) % members.length];
693
+ if (!matches(await member.durability()))
694
+ continue;
695
+ const created = await member.open();
696
+ if (typeof created.deliverHere === "function")
697
+ return created;
698
+ }
699
+ return null;
700
+ }
701
+ var agentSharing = (name, durability) => agentNamed(name, (other) => other === durability);
702
+ var agentOnRoute = async (name, durability) => await agentSharing(name, durability) ?? agentNamed(name, (other) => other !== durability && other.name === durability.name && typeof other.world === "function" && other.create === undefined);
703
+ function wakeHandler() {
704
+ return async (request) => {
705
+ for (const group of agentsByName.values()) {
706
+ for (const member of group.members) {
707
+ const created = await member.open();
708
+ if (created.viaRoute === true)
709
+ return created.queueHandler(request);
710
+ }
711
+ }
712
+ return new Response("no libfx agent in this process takes queue deliveries from a route", { status: 503 });
713
+ };
714
+ }
715
+ function createWorldBackend(world, durability) {
716
+ const { name, reserveMs, pollMs, leaseMs, alive, holderInfo, livenessKnown = false, queueLeases = false, runInRequest = queueLeases, waitUntil, queueDurable = false, maxDurationMs, owned = true } = durability;
639
717
  const spec = world.specVersion === undefined ? {} : { specVersion: world.specVersion };
640
718
  const created = new Set;
641
719
  const creating = new Map;
@@ -651,7 +729,7 @@ function createWorldBackend(world, { name, reserveMs, pollMs, leaseMs, alive, ho
651
729
  pagination: { sortOrder: "desc", limit, ...cursor ? { cursor } : {} },
652
730
  resolveData: "all"
653
731
  });
654
- const deliverTo = async (handle, message) => {
732
+ const deliverTo = async (handle, message, meta) => {
655
733
  const input = message?.input;
656
734
  if (!input || typeof input !== "object")
657
735
  return;
@@ -660,13 +738,24 @@ function createWorldBackend(world, { name, reserveMs, pollMs, leaseMs, alive, ho
660
738
  return;
661
739
  throw new Error("no libfx agent is listening; the queue delivers this again");
662
740
  }
663
- const result = await handle(input);
741
+ const result = await handle(input, meta);
742
+ if (result?.redeliver === true)
743
+ throw new Error("libfx asks the queue to deliver this message again");
664
744
  return Number.isFinite(result?.timeoutSeconds) ? { timeoutSeconds: result.timeoutSeconds } : undefined;
665
745
  };
666
- const queueHandler = world.createQueueHandler(queuePrefix, (message) => deliverTo(handler, message));
746
+ const queueHandler = world.createQueueHandler(queuePrefix, async (message, meta) => {
747
+ const named = agentOf(message?.input);
748
+ const agent = await agentOnRoute(named, durability);
749
+ if (!agent)
750
+ throw noAgentNamed(named);
751
+ return agent.deliverHere(message, meta);
752
+ });
667
753
  const backend = {
668
754
  name,
669
755
  livenessKnown,
756
+ queueLeases,
757
+ runInRequest: queueLeases && runInRequest,
758
+ waitUntil: waitUntil ?? null,
670
759
  queueDurable,
671
760
  pollMs: pollMs ?? 1000,
672
761
  reserveMs: reserveMs ?? defaultReserveMs,
@@ -677,10 +766,14 @@ function createWorldBackend(world, { name, reserveMs, pollMs, leaseMs, alive, ho
677
766
  validSessionId: (id) => typeof id === "string" && /^wrun_[0-9A-Za-z]{10,64}$/.test(id),
678
767
  deadline: () => null,
679
768
  holderInfo: holderInfo ?? (() => ({})),
680
- alive(lease) {
769
+ alive(lease, delivery = null) {
770
+ if (liveHolders.has(lease.holder))
771
+ return true;
681
772
  const mine = holderInfo?.() ?? {};
682
773
  if (mine.pid !== undefined && lease.pid === mine.pid && lease.host === mine.host)
683
- return liveHolders.has(lease.holder);
774
+ return false;
775
+ if (redelivers(delivery, lease))
776
+ return false;
684
777
  return alive ? alive(lease) : null;
685
778
  },
686
779
  holding(holder, held) {
@@ -708,6 +801,21 @@ function createWorldBackend(world, { name, reserveMs, pollMs, leaseMs, alive, ho
708
801
  },
709
802
  async session(runId) {
710
803
  let known = 0;
804
+ const inBandAt = new Map;
805
+ let inBand = world.capabilities?.inBandFence === true;
806
+ const noteInBand = (cursor, count) => {
807
+ inBandAt.delete(cursor);
808
+ inBandAt.set(cursor, count);
809
+ if (inBandAt.size > 16)
810
+ inBandAt.delete(inBandAt.keys().next().value);
811
+ };
812
+ const noteSnapshot = (snapshot, cursor) => {
813
+ if (!inBand || cursor === undefined || inBandAt.has(cursor) || !Number.isSafeInteger(snapshot?.seqInBand))
814
+ return;
815
+ if (cursor !== "null" && !(Number(cursor) <= snapshot.seq))
816
+ return;
817
+ noteInBand(cursor, snapshot.seqInBand);
818
+ };
711
819
  const ensureRun = () => {
712
820
  if (created.has(runId))
713
821
  return Promise.resolve();
@@ -741,34 +849,45 @@ function createWorldBackend(world, { name, reserveMs, pollMs, leaseMs, alive, ho
741
849
  const newestFirst = async (stopBelow, stopAtCheckpoint) => {
742
850
  const events = [];
743
851
  let through = null;
852
+ let snapshot;
853
+ let newestChained;
854
+ const done = (head) => {
855
+ noteSnapshot(snapshot, newestChained ?? head);
856
+ return { events, through };
857
+ };
744
858
  for (let cursor, limit = firstPageEvents;; limit = laterPageEvents) {
745
859
  let page;
746
860
  try {
747
861
  page = await listEvents(runId, cursor, limit);
748
862
  } catch (error) {
749
- if (missingRun(error))
750
- return { events, through };
751
- throw error;
863
+ if (!missingRun(error))
864
+ throw error;
865
+ if (stopBelow === 0)
866
+ noteSnapshot({ seq: 0, seqInBand: inBandAtRunCreation }, "null");
867
+ return { events, through };
752
868
  }
869
+ snapshot ??= page.snapshot;
753
870
  for (const event of page.data) {
754
871
  const slot = slotOf(event);
755
872
  if (slot > known)
756
873
  known = slot;
757
874
  if (slot <= stopBelow)
758
- return { events, through };
875
+ return done(undefined);
759
876
  if (through !== null && slot <= through)
760
- return { events, through };
877
+ return done(through === 0 ? "null" : String(through));
761
878
  const entry = logEntryOf(event);
762
879
  if (!entry)
763
880
  continue;
764
881
  created.add(runId);
882
+ if (newestChained === undefined && chainedKind(entry.k))
883
+ newestChained = String(slot);
765
884
  events.push({ cursor: String(slot), entry });
766
885
  if (stopAtCheckpoint && through === null && entry.k === "checkpoint") {
767
886
  through = entry.through === null || entry.through === undefined ? 0 : Number(entry.through);
768
887
  }
769
888
  }
770
889
  if (!page.hasMore)
771
- return { events, through };
890
+ return done(stopBelow > 0 ? undefined : through ? String(through) : "null");
772
891
  cursor = page.cursor;
773
892
  }
774
893
  };
@@ -783,16 +902,50 @@ function createWorldBackend(world, { name, reserveMs, pollMs, leaseMs, alive, ho
783
902
  },
784
903
  async append(entry) {
785
904
  await ensureRun();
786
- const chained = entry.k === "lease" || entry.k === "release" || entry.k === "record";
905
+ const chained = chainedKind(entry.k);
787
906
  const after = chained ? entry.a === null || entry.a === undefined ? 0 : Number(entry.a) : known;
788
- const result = await world.events.create(runId, {
907
+ const continues = entry.a === null || entry.a === undefined ? "null" : String(entry.a);
908
+ let expected = chained && inBand ? inBandAt.get(continues) : undefined;
909
+ const event = {
789
910
  eventType: "step_created",
790
911
  correlationId: `step_${ulid()}`,
791
912
  eventData: { stepName: logStep, input: encoder.encode(JSON.stringify(entry)) }
792
- }, { eventCount: after });
913
+ };
914
+ let result;
915
+ for (let recounted = false;; ) {
916
+ try {
917
+ result = await world.events.create(runId, event, expected === undefined ? { eventCount: after } : { eventCount: after, inBand: true, expectedSeqInBand: expected });
918
+ break;
919
+ } catch (error) {
920
+ if (expected === undefined)
921
+ throw error;
922
+ if (inBandSuperseded(error)) {
923
+ inBandAt.delete(continues);
924
+ if (!recounted && Number.isSafeInteger(error?.seqInBand)) {
925
+ const { events: since } = await newestFirst(after, false);
926
+ const rivals = since.filter(({ entry: other }) => chainedKind(other.k) && (other.a ?? null) === (entry.a ?? null));
927
+ const ours = rivals.find(({ entry: other }) => other.key !== undefined && other.key === entry.key && other.k === entry.k);
928
+ if (ours)
929
+ return ours.cursor;
930
+ if (rivals.length === 0) {
931
+ recounted = true;
932
+ expected = error.seqInBand;
933
+ continue;
934
+ }
935
+ }
936
+ throw new FencedError(`another worker continued session ${runId}`);
937
+ }
938
+ if (!(error?.status >= 400 && error?.status < 500) || error?.status === 409)
939
+ throw error;
940
+ inBand = false;
941
+ expected = undefined;
942
+ }
943
+ }
793
944
  const slot = slotOf(result.event);
794
945
  if (slot > known)
795
946
  known = slot;
947
+ if (expected !== undefined && result.allocated !== undefined)
948
+ noteInBand(String(slot), expected + result.allocated);
796
949
  changed(runId);
797
950
  if (!chained || slot === after + 1)
798
951
  return String(slot);
@@ -802,7 +955,7 @@ function createWorldBackend(world, { name, reserveMs, pollMs, leaseMs, alive, ho
802
955
  const otherSlot = event.entry ? Number(event.cursor) : slotOf(event);
803
956
  if (!other || otherSlot <= after || otherSlot >= slot)
804
957
  continue;
805
- const otherChained = other.k === "lease" || other.k === "release" || other.k === "record";
958
+ const otherChained = chainedKind(other.k);
806
959
  if (!otherChained || (other.a ?? null) !== (entry.a ?? null))
807
960
  continue;
808
961
  if (other.key !== undefined && other.key === entry.key && other.k === entry.k)
@@ -875,14 +1028,18 @@ function createWorldBackend(world, { name, reserveMs, pollMs, leaseMs, alive, ho
875
1028
  },
876
1029
  queue: {
877
1030
  async send(message, { delaySeconds = 0, idempotencyKey } = {}) {
878
- await world.queue(queueName, { runId: message.sessionId, requestId: message.messageId, input: message }, {
1031
+ const sent = await world.queue(queueName, { runId: message.sessionId, requestId: message.messageId, input: message }, {
879
1032
  ...idempotencyKey ? { idempotencyKey } : {},
880
1033
  ...delaySeconds > 0 ? { delaySeconds } : {}
881
1034
  });
1035
+ return typeof sent?.messageId === "string" && sent.messageId.length > 0 ? { messageId: sent.messageId } : {};
882
1036
  }
883
1037
  },
884
1038
  queueHandler,
885
- listen(fn) {
1039
+ durability,
1040
+ viaRoute: route,
1041
+ deliverHere: (message, meta) => deliverTo(handler, message, meta),
1042
+ listen(fn, name = defaultAgentName) {
886
1043
  handler = fn;
887
1044
  if (route)
888
1045
  return () => {
@@ -891,20 +1048,26 @@ function createWorldBackend(world, { name, reserveMs, pollMs, leaseMs, alive, ho
891
1048
  };
892
1049
  let listeners = listenersOf.get(world);
893
1050
  if (!listeners) {
894
- listeners = { handlers: [], turn: 0 };
1051
+ listeners = new Map;
895
1052
  listenersOf.set(world, listeners);
896
- world.registerHandler(queuePrefix, world.createQueueHandler(queuePrefix, (message) => {
897
- const { handlers } = listeners;
898
- return deliverTo(handlers.length === 0 ? null : handlers[listeners.turn++ % handlers.length], message);
1053
+ world.registerHandler(queuePrefix, world.createQueueHandler(queuePrefix, async (message, meta) => {
1054
+ const named = agentOf(message?.input);
1055
+ const group = listeners.get(named);
1056
+ if (group)
1057
+ return deliverTo(group.members[group.turn++ % group.members.length], message, meta);
1058
+ const agent = await agentSharing(named, durability);
1059
+ if (agent)
1060
+ return agent.deliverHere(message, meta);
1061
+ if (listeners.size === 0)
1062
+ return deliverTo(null, message, meta);
1063
+ throw noAgentNamed(named);
899
1064
  }));
900
1065
  }
901
- listeners.handlers.push(fn);
1066
+ const group = joinGroup(listeners, name, fn);
902
1067
  return () => {
903
1068
  if (handler === fn)
904
1069
  handler = null;
905
- const index = listeners.handlers.indexOf(fn);
906
- if (index >= 0)
907
- listeners.handlers.splice(index, 1);
1070
+ leaveGroup(listeners, name, group, fn);
908
1071
  };
909
1072
  },
910
1073
  async close() {
@@ -946,8 +1109,9 @@ var errorSummary = (error) => ({ name: error?.name ?? "Error", message: String(e
946
1109
  var isFenced = (error) => error?.code === "FX_FENCED";
947
1110
  function messageEntries(state, message) {
948
1111
  const entries = [];
1112
+ const named = typeof message.agent === "string" ? { agent: message.agent } : {};
949
1113
  if (message.seed && state.checkpoint === null && state.records.length === 0) {
950
- entries.push({ k: "checkpoint", through: null, data: message.seed });
1114
+ entries.push({ k: "checkpoint", through: null, data: message.seed, ...named });
951
1115
  }
952
1116
  const key = `${message.type}:${message.messageId}`;
953
1117
  const known = state.inputKeys.has(key) || state.consumed.has(message.messageId);
@@ -957,6 +1121,7 @@ function messageEntries(state, message) {
957
1121
  key,
958
1122
  type: message.type,
959
1123
  messageId: message.messageId,
1124
+ ...named,
960
1125
  ...message.input === undefined ? {} : { input: message.input },
961
1126
  ...typeof message.target === "string" ? { target: message.target } : {},
962
1127
  ...(message.type === "prompt" || message.type === "steer") && message.context !== undefined ? { context: message.context } : {},
@@ -1000,6 +1165,8 @@ function gatedLog(log, ready, claimError) {
1000
1165
  var hardStopMs = 2000;
1001
1166
  var durableInternals = Symbol.for("libfx.durableInternals");
1002
1167
  var runningBackstopSeconds = 2;
1168
+ var deliveryDeadline = (backend) => backend.deadline() ?? (backend.maxDurationMs === null ? null : Date.now() + backend.maxDurationMs);
1169
+ var inlineBackstopMarginSeconds = 5;
1003
1170
 
1004
1171
  class SessionWorker {
1005
1172
  constructor(agent, backend, sessionId) {
@@ -1010,24 +1177,32 @@ class SessionWorker {
1010
1177
  this.deliveries = 0;
1011
1178
  this.holder = null;
1012
1179
  }
1013
- fold(entries) {
1014
- return foldSessionLog(entries, { now: Date.now(), alive: (lease) => this.backend.alive(lease) });
1180
+ fold(entries, delivery = null) {
1181
+ return foldSessionLog(entries, { now: Date.now(), alive: (lease) => this.backend.alive(lease, delivery) });
1015
1182
  }
1016
- async consume(message) {
1183
+ async consume(message, delivery = null) {
1017
1184
  const log = await this.backend.session(this.sessionId);
1018
1185
  await this.backend.refreshDeadline?.();
1019
1186
  const deadline = this.backend.deadline() ?? (this.backend.maxDurationMs === null ? null : Date.now() + this.backend.maxDurationMs);
1020
1187
  let entries = await log.read();
1021
- let state = this.fold(entries);
1188
+ let state = this.fold(entries, delivery);
1189
+ const owner = state.agent ?? agentOf(message);
1190
+ if (owner !== this.agent.name) {
1191
+ await this.backend.queue.send({ ...message, agent: owner }, {
1192
+ idempotencyKey: `${message.sessionId}:${message.type}:${message.messageId}:to:${owner}`
1193
+ });
1194
+ this.agent.emit("session.forwarded", { sessionId: this.sessionId, agent: owner });
1195
+ return;
1196
+ }
1022
1197
  for (let claims = 0;claims < 8; claims += 1) {
1023
- const early = this.running || state.lease ? null : earlyStart(entries, state, message, (items) => this.fold(items));
1198
+ const early = this.running || state.lease ? null : earlyStart(entries, state, message, (items) => this.fold(items, delivery));
1024
1199
  let outcome;
1025
1200
  if (early) {
1026
- outcome = await this.claim(log, state, deadline, early);
1201
+ outcome = await this.claim(log, state, deadline, early, delivery);
1027
1202
  } else {
1028
1203
  if (await recordMessage(log, state, message) > 0) {
1029
1204
  entries = await log.read();
1030
- state = this.fold(entries);
1205
+ state = this.fold(entries, delivery);
1031
1206
  }
1032
1207
  if (this.running)
1033
1208
  return { timeoutSeconds: runningBackstopSeconds };
@@ -1040,22 +1215,22 @@ class SessionWorker {
1040
1215
  const left = state.lease.expiresAt ? Math.ceil((state.lease.expiresAt - Date.now()) / 1000) + 1 : 5;
1041
1216
  return { timeoutSeconds: Math.max(1, Math.min(maxBackstopSeconds, left)) };
1042
1217
  }
1043
- outcome = await this.claim(log, state, deadline, null);
1218
+ outcome = await this.claim(log, state, deadline, null, delivery);
1044
1219
  }
1045
1220
  if (outcome === "yielded")
1046
1221
  return { timeoutSeconds: 0 };
1047
1222
  if (outcome === "failed")
1048
- return { timeoutSeconds: 1 };
1223
+ return queueHeld(this.backend, deadline) ? { redeliver: true } : { timeoutSeconds: 1 };
1049
1224
  if (outcome === "fenced") {
1050
1225
  this.agent.emit("session.fenced", { sessionId: this.sessionId, epoch: state.lastEpoch + 1 });
1051
1226
  return;
1052
1227
  }
1053
1228
  entries = await log.read();
1054
- state = this.fold(entries);
1229
+ state = this.fold(entries, delivery);
1055
1230
  }
1056
1231
  return { timeoutSeconds: 1 };
1057
1232
  }
1058
- async claim(log, state, deadline, early) {
1233
+ async claim(log, state, deadline, early, delivery = null) {
1059
1234
  this.running = true;
1060
1235
  this.holder = newId("wkr");
1061
1236
  const lease = {
@@ -1064,7 +1239,8 @@ class SessionWorker {
1064
1239
  holder: this.holder,
1065
1240
  epoch: state.lastEpoch + 1,
1066
1241
  expiresAt: leaseEnd(this.backend, deadline),
1067
- ...this.backend.holderInfo()
1242
+ ...this.backend.holderInfo(),
1243
+ ...delivery ? { delivery } : {}
1068
1244
  };
1069
1245
  this.backend.holding(this.holder, true);
1070
1246
  try {
@@ -1186,19 +1362,20 @@ class SessionWorker {
1186
1362
  pending: now.unconsumed.filter((input) => input.cursor <= Number(through)),
1187
1363
  recent: now.recent,
1188
1364
  lastTurnId: now.lastTurnId,
1365
+ ...now.agent === null ? {} : { agent: now.agent },
1189
1366
  ...now.openTurn ? { openTurn: now.openTurn, yielded: now.yielded } : {},
1190
- lease: { holder: this.holder, epoch: lease.epoch, expiresAt: leaseExpiresAt, pid: lease.pid, host: lease.host }
1367
+ lease: { holder: this.holder, epoch: lease.epoch, expiresAt: leaseExpiresAt, pid: lease.pid, host: lease.host, ...lease.delivery ? { delivery: lease.delivery } : {} }
1191
1368
  });
1192
1369
  }
1193
1370
  };
1194
1371
  let turnNow = null;
1195
1372
  let renew = null;
1196
- if (!this.backend.livenessKnown) {
1373
+ if (!heldToDeadline(this.backend, deadline)) {
1197
1374
  renew = setInterval(() => {
1198
1375
  if (released || failed || this.stopping)
1199
1376
  return;
1200
1377
  leaseExpiresAt = leaseEnd(this.backend, deadline);
1201
- chained({ k: "lease", holder: this.holder, epoch: lease.epoch, expiresAt: leaseExpiresAt, ...this.backend.holderInfo() }).catch((error) => {
1378
+ chained({ k: "lease", holder: this.holder, epoch: lease.epoch, expiresAt: leaseExpiresAt, ...this.backend.holderInfo(), ...lease.delivery ? { delivery: lease.delivery } : {} }).catch((error) => {
1202
1379
  if (isFenced(error))
1203
1380
  turnNow?.cancel({ reason: "handoff" });
1204
1381
  });
@@ -1362,11 +1539,14 @@ class SessionWorker {
1362
1539
  if (state.lastLease?.holder !== this.holder && !failed)
1363
1540
  return "fenced";
1364
1541
  }
1365
- await harness.close();
1366
- harness = null;
1367
- if (failed)
1542
+ if (failed) {
1543
+ await harness.close();
1544
+ harness = null;
1368
1545
  return broken();
1546
+ }
1369
1547
  await release();
1548
+ agent.track(harness.close().catch(() => {}));
1549
+ harness = null;
1370
1550
  return failed ? broken() : "done";
1371
1551
  } finally {
1372
1552
  if (hardStop)
@@ -1467,8 +1647,7 @@ class SessionWorker {
1467
1647
  }
1468
1648
  }
1469
1649
  var internalEvent = new Set(["turn_start", "turn_resume", "turn_yield", "turn_end", "idle"]);
1470
- function uiRanker() {
1471
- let high = 0;
1650
+ function uiRanker(high = 0) {
1472
1651
  return (event) => {
1473
1652
  if (!Number.isSafeInteger(event?.epoch))
1474
1653
  return true;
@@ -1587,9 +1766,18 @@ function turnView({ messageId, resumeRequest = null, start }) {
1587
1766
  settle.resolve(repeated ? { ...value, repeated: true } : value);
1588
1767
  wake();
1589
1768
  };
1590
- (async () => {
1769
+ const started = start();
1770
+ started.catch(() => {});
1771
+ let following = false;
1772
+ const follow = () => {
1773
+ if (following)
1774
+ return;
1775
+ following = true;
1776
+ read();
1777
+ };
1778
+ const read = async () => {
1591
1779
  try {
1592
- const { log, from, settled, attaching, endWithinMs, unsure } = await start();
1780
+ const { log, from, settled, attaching, endWithinMs, unsure } = await started;
1593
1781
  repeated = settled?.repeated === true || endWithinMs !== undefined && !unsure;
1594
1782
  if (settled) {
1595
1783
  const { messageId: _id, ...rest } = settled;
@@ -1654,7 +1842,7 @@ function turnView({ messageId, resumeRequest = null, start }) {
1654
1842
  } catch (error) {
1655
1843
  finish(null, error);
1656
1844
  }
1657
- })();
1845
+ };
1658
1846
  async function* iterate(raw) {
1659
1847
  for (let index = 0;; index += 1) {
1660
1848
  while (index >= events.length) {
@@ -1678,11 +1866,16 @@ function turnView({ messageId, resumeRequest = null, start }) {
1678
1866
  get messageId() {
1679
1867
  return id;
1680
1868
  },
1681
- result,
1869
+ get result() {
1870
+ follow();
1871
+ return result;
1872
+ },
1682
1873
  [Symbol.asyncIterator]() {
1874
+ follow();
1683
1875
  return iterate(false);
1684
1876
  },
1685
1877
  get readable() {
1878
+ follow();
1686
1879
  const iterator = iterate(true);
1687
1880
  return new ReadableStream({
1688
1881
  async pull(controller) {
@@ -1709,10 +1902,13 @@ function createDurableAgentFactory({ harness, defaultDurability, name = "createA
1709
1902
  if (!options || typeof options !== "object" || Array.isArray(options)) {
1710
1903
  throw new TypeError(`${name}() options must be an object`);
1711
1904
  }
1712
- const { durability, ...harnessOptions } = options;
1905
+ const { durability, name: agentName = defaultAgentName, ...harnessOptions } = options;
1713
1906
  if (durability !== undefined && !isDurability(durability)) {
1714
1907
  throw new TypeError("durability must come from memory(), local(), vercel() or world()");
1715
1908
  }
1909
+ if (typeof agentName !== "string" || !agentNameRule.test(agentName)) {
1910
+ throw new TypeError(`${name}() name must be 1 to 64 letters, digits, ".", "_" or "-", starting with a letter or digit`);
1911
+ }
1716
1912
  const plugged = harness(harnessOptions);
1717
1913
  const seed = plugged.seed === undefined ? undefined : toBase64(plugged.seed);
1718
1914
  const emit = (type, detail = {}) => {
@@ -1721,7 +1917,12 @@ function createDurableAgentFactory({ harness, defaultDurability, name = "createA
1721
1917
  } catch {}
1722
1918
  };
1723
1919
  const agent = {
1920
+ name: agentName,
1724
1921
  emit,
1922
+ track(work) {
1923
+ active.add(work);
1924
+ work.finally(() => active.delete(work));
1925
+ },
1725
1926
  openHarness: (sessionId, store, run, ready) => plugged.open({
1726
1927
  sessionId,
1727
1928
  store,
@@ -1732,6 +1933,7 @@ function createDurableAgentFactory({ harness, defaultDurability, name = "createA
1732
1933
  })
1733
1934
  };
1734
1935
  let backendPromise = null;
1936
+ let durabilityPromise = null;
1735
1937
  let chosenDurability = null;
1736
1938
  let unlisten = null;
1737
1939
  let closed = false;
@@ -1741,33 +1943,48 @@ function createDurableAgentFactory({ harness, defaultDurability, name = "createA
1741
1943
  if (closed)
1742
1944
  return Promise.reject(new Error(`${label} is closed`));
1743
1945
  backendPromise ??= (async () => {
1744
- const chosen = durability ?? await defaultDurability();
1946
+ const chosen = await durabilityOf();
1745
1947
  chosenDurability = chosen;
1746
1948
  const created = await createBackend(chosen);
1747
1949
  await created.start?.();
1748
- unlisten = created.listen((message) => {
1749
- let worker = workers.get(message.sessionId);
1750
- if (!worker) {
1751
- worker = new SessionWorker(agent, created, message.sessionId);
1752
- workers.set(message.sessionId, worker);
1753
- }
1754
- worker.deliveries += 1;
1755
- const work = worker.consume(message);
1756
- active.add(work);
1757
- work.catch((error) => {
1758
- emit("session.error", { sessionId: message.sessionId, error: errorSummary(error) });
1759
- }).finally(() => {
1760
- active.delete(work);
1761
- worker.deliveries -= 1;
1762
- if (worker.deliveries === 0 && !worker.running && workers.get(message.sessionId) === worker)
1763
- workers.delete(message.sessionId);
1764
- });
1765
- return work;
1766
- });
1950
+ unlisten = created.listen((message, meta) => receive(created, message, deliveryOf(meta)), agentName);
1767
1951
  return created;
1768
1952
  })();
1769
1953
  return backendPromise;
1770
1954
  };
1955
+ const durabilityOf = () => durabilityPromise ??= durability ? Promise.resolve(durability) : defaultDurability();
1956
+ const member = { open: backend, durability: durabilityOf };
1957
+ const registered = joinGroup(agentsByName, agentName, member);
1958
+ const receive = (created, message, delivery, { inRequest = false } = {}) => {
1959
+ let worker = workers.get(message.sessionId);
1960
+ if (!worker) {
1961
+ worker = new SessionWorker(agent, created, message.sessionId);
1962
+ workers.set(message.sessionId, worker);
1963
+ }
1964
+ worker.deliveries += 1;
1965
+ let work = worker.consume(message, delivery);
1966
+ if (inRequest) {
1967
+ work = work.then(async (result) => {
1968
+ if (Number.isFinite(result?.timeoutSeconds)) {
1969
+ await created.queue.send(message, {
1970
+ idempotencyKey: `${message.sessionId}:${message.type}:${message.messageId}:${newId("again")}`,
1971
+ ...result.timeoutSeconds > 0 ? { delaySeconds: result.timeoutSeconds } : {}
1972
+ });
1973
+ }
1974
+ return result;
1975
+ });
1976
+ }
1977
+ active.add(work);
1978
+ work.catch((error) => {
1979
+ emit("session.error", { sessionId: message.sessionId, error: errorSummary(error) });
1980
+ }).finally(() => {
1981
+ active.delete(work);
1982
+ worker.deliveries -= 1;
1983
+ if (worker.deliveries === 0 && !worker.running && workers.get(message.sessionId) === worker)
1984
+ workers.delete(message.sessionId);
1985
+ });
1986
+ return work;
1987
+ };
1771
1988
  const openSession = (requestedId, sessionOptions = {}) => {
1772
1989
  if (requestedId !== undefined && !validId(requestedId))
1773
1990
  throw new TypeError(`session id must be ${idRule}`);
@@ -1789,19 +2006,33 @@ function createDurableAgentFactory({ harness, defaultDurability, name = "createA
1789
2006
  return { created, id };
1790
2007
  });
1791
2008
  resolved.catch(() => {});
1792
- const send = async (message) => {
2009
+ const send = async (message, { inRequest = false } = {}) => {
1793
2010
  const { created, id: sessionId } = await resolved;
1794
2011
  const extra = {
1795
2012
  ...seeded ? {} : { seed },
1796
2013
  ...context === undefined ? {} : { context },
1797
2014
  ...settings === undefined ? {} : { settings }
1798
2015
  };
1799
- const full = { ...message, ...extra, sessionId };
2016
+ const full = { ...message, ...extra, agent: agentName, sessionId };
1800
2017
  if (!created.queueDurable) {
1801
2018
  const log = await created.session(sessionId);
1802
2019
  await recordMessage(log, foldSessionLog(await log.read()), full);
1803
2020
  }
1804
- await created.queue.send(full, { idempotencyKey: `${sessionId}:${message.type}:${message.messageId}` });
2021
+ const idempotencyKey = `${sessionId}:${message.type}:${message.messageId}`;
2022
+ if (inRequest && created.runInRequest) {
2023
+ await created.refreshDeadline?.();
2024
+ const deadline = deliveryDeadline(created);
2025
+ if (deadline !== null) {
2026
+ const delaySeconds = Math.ceil(Math.max(0, deadline - Date.now()) / 1000) + inlineBackstopMarginSeconds;
2027
+ const { messageId: backstop } = await created.queue.send(full, { idempotencyKey, delaySeconds }) ?? {};
2028
+ seeded = true;
2029
+ const delivery = backstop ? { messageId: backstop, attempt: 0 } : null;
2030
+ const work = receive(created, full, delivery, { inRequest: true }).then(() => {}, () => {});
2031
+ created.waitUntil?.(work);
2032
+ return;
2033
+ }
2034
+ }
2035
+ await created.queue.send(full, { idempotencyKey });
1805
2036
  seeded = true;
1806
2037
  };
1807
2038
  const startTurn = (type, messageId, input, retried = false, aborted = false) => {
@@ -1823,7 +2054,7 @@ function createDurableAgentFactory({ harness, defaultDurability, name = "createA
1823
2054
  from = await log.ui.length();
1824
2055
  const [state] = await Promise.all([
1825
2056
  log.read().then(foldSessionLog),
1826
- send({ type, messageId, ...input === undefined ? {} : { input } }).then(() => accept.resolve({ messageId, sessionId }))
2057
+ send({ type, messageId, ...input === undefined ? {} : { input } }, { inRequest: type === "prompt" }).then(() => accept.resolve({ messageId, sessionId }))
1827
2058
  ]);
1828
2059
  const outcome = await retryOutcome(log, state, messageId, from, { sent: true });
1829
2060
  if (outcome.settled)
@@ -1849,7 +2080,7 @@ function createDurableAgentFactory({ harness, defaultDurability, name = "createA
1849
2080
  return { settled: { messageId, stopReason: "cancelled", usage: {} } };
1850
2081
  }
1851
2082
  from ??= await log.ui.length();
1852
- await send({ type, messageId, ...input === undefined ? {} : { input } });
2083
+ await send({ type, messageId, ...input === undefined ? {} : { input } }, { inRequest: type === "prompt" });
1853
2084
  accept.resolve({ messageId, sessionId });
1854
2085
  return { log, from, attaching };
1855
2086
  } catch (error) {
@@ -1899,12 +2130,40 @@ function createDurableAgentFactory({ harness, defaultDurability, name = "createA
1899
2130
  let reader = null;
1900
2131
  let buffered = "";
1901
2132
  let next = 0;
1902
- const shows = uiRanker();
2133
+ let shows = uiRanker();
2134
+ const open = async (ui) => {
2135
+ if (cursor === 0)
2136
+ return ui.read(0).getReader();
2137
+ const at = ui.read(cursor - 1).getReader();
2138
+ let first = "";
2139
+ for (;; ) {
2140
+ const index = first.indexOf(`
2141
+ `);
2142
+ if (index >= 0) {
2143
+ let epoch = null;
2144
+ try {
2145
+ epoch = JSON.parse(first.slice(0, index))?.epoch;
2146
+ } catch {}
2147
+ if (!Number.isSafeInteger(epoch))
2148
+ break;
2149
+ shows = uiRanker(epoch);
2150
+ buffered = first.slice(index + 1);
2151
+ next = cursor;
2152
+ return at;
2153
+ }
2154
+ const { value, done } = await at.read();
2155
+ if (done)
2156
+ return at;
2157
+ first += decoder.decode(value, { stream: true });
2158
+ }
2159
+ at.cancel().catch(() => {});
2160
+ return ui.read(0).getReader();
2161
+ };
1903
2162
  return new ReadableStream({
1904
2163
  async pull(controller) {
1905
2164
  if (!reader) {
1906
2165
  const { created, id: sessionId } = await resolved;
1907
- reader = (await created.session(sessionId)).ui.read(0).getReader();
2166
+ reader = await open((await created.session(sessionId)).ui);
1908
2167
  }
1909
2168
  for (;; ) {
1910
2169
  const index = buffered.indexOf(`
@@ -1974,7 +2233,12 @@ function createDurableAgentFactory({ harness, defaultDurability, name = "createA
1974
2233
  const durableAgent = {
1975
2234
  [durableInternals]: {
1976
2235
  stopAtDeadline: (sessionId) => workers.get(sessionId)?.stopNow?.(),
2236
+ settled: async (sessionId) => {
2237
+ await workers.get(sessionId)?.settled?.();
2238
+ },
2239
+ deliver: async (message, delivery) => receive(await backend(), message, delivery),
1977
2240
  liveWorkers: () => workers.size,
2241
+ openLog: async (sessionId) => (await backend()).session(sessionId),
1978
2242
  lastLease: async (sessionId) => {
1979
2243
  const log = await (await backend()).session(sessionId);
1980
2244
  return foldSessionLog(await log.read()).lastLease;
@@ -1993,6 +2257,9 @@ function createDurableAgentFactory({ harness, defaultDurability, name = "createA
1993
2257
  async newSessionId() {
1994
2258
  return (await backend()).newSessionId();
1995
2259
  },
2260
+ get name() {
2261
+ return agentName;
2262
+ },
1996
2263
  get sessionId() {
1997
2264
  return defaultSession?.id ?? null;
1998
2265
  },
@@ -2016,6 +2283,7 @@ function createDurableAgentFactory({ harness, defaultDurability, name = "createA
2016
2283
  if (closed)
2017
2284
  return;
2018
2285
  closed = true;
2286
+ leaveGroup(agentsByName, agentName, registered, member);
2019
2287
  while (active.size > 0)
2020
2288
  await Promise.allSettled([...active]);
2021
2289
  if (backendPromise) {
@@ -2484,6 +2752,7 @@ var maxApiKeyBytes = 64 * 1024;
2484
2752
  var maxModelBytes = 1024;
2485
2753
  var maxEffortBytes = 64;
2486
2754
  var maxUrlBytes = 16 * 1024;
2755
+ var unfinishedToolOutput = "fx stopped while this tool was running, so it may have partly run. Check its effects before running it again.";
2487
2756
  var maxModelCatalogBytes = 4 * 1024 * 1024;
2488
2757
  var maxModelCatalogEntries = 1e4;
2489
2758
  var streamReadsPerTaskYield = 32;
@@ -2721,12 +2990,20 @@ function journalAppendError(cause) {
2721
2990
  function withoutSupersededProgress(events) {
2722
2991
  const kept = new Array(events.length);
2723
2992
  let superseded = false;
2993
+ let answered = false;
2724
2994
  for (let index = events.length - 1;index >= 0; index -= 1) {
2725
2995
  const event = events[index];
2726
2996
  const type = event?.type;
2727
- kept[index] = superseded && type === "turn_progress" && event.data != null ? { ...event, data: null } : event;
2728
- if (type === "turn_progress" || type === "turn_committed" || type === "turn_progress_cleared")
2997
+ const dropped = event?.data != null && (type === "turn_progress" && superseded || type === "tool_result" && answered);
2998
+ kept[index] = dropped ? { ...event, data: null } : event;
2999
+ if (type === "turn_committed" || type === "turn_progress_cleared") {
2729
3000
  superseded = true;
3001
+ answered = true;
3002
+ } else if (type === "turn_progress") {
3003
+ answered = true;
3004
+ if (event.base === undefined)
3005
+ superseded = true;
3006
+ }
2730
3007
  }
2731
3008
  return kept;
2732
3009
  }
@@ -2836,9 +3113,9 @@ function journalMarks(events) {
2836
3113
  if (event?.type === "turn_progress") {
2837
3114
  if (typeof event.turnId === "string")
2838
3115
  marks.push({ start: event.turnId });
2839
- const steps = event.data?.execution?.tool_steps;
2840
- if (Array.isArray(steps))
2841
- marks.push({ step: `${steps.length}:${String(event.data.tool_state ?? "none")}` });
3116
+ const steps = Number.isSafeInteger(event.steps) ? event.steps : event.data?.execution?.tool_steps?.length;
3117
+ if (Number.isSafeInteger(steps))
3118
+ marks.push({ step: `${steps}:${String(event.data?.tool_state ?? "none")}` });
2842
3119
  } else if (event?.type === "turn_committed" || event?.type === "turn_progress_cleared")
2843
3120
  marks.push({ end: true });
2844
3121
  else if (event?.type === "turn_yielded")
@@ -4779,6 +5056,7 @@ async function createFxEngine(options = {}) {
4779
5056
  let coreExitError = null;
4780
5057
  const isCurrentTurn = (turn) => turn && activeTurn === turn && !turn.cancelled && !closing;
4781
5058
  const persistence = options.persistence ?? null;
5059
+ const wantsMarks = persistence?.marks === true || persistence?.[journalMarksWanted] === true;
4782
5060
  const checkpointAfterBytes = options.checkpointAfterBytes ?? defaultCheckpointAfterBytes;
4783
5061
  let journalHead = null;
4784
5062
  let appendedSeq = 0;
@@ -4786,12 +5064,12 @@ async function createFxEngine(options = {}) {
4786
5064
  let appendTail = Promise.resolve();
4787
5065
  const writerId = crypto.randomUUID();
4788
5066
  const journal = persistence && {
4789
- append(events) {
5067
+ append(events, barrier) {
4790
5068
  const data = encodeJournalRecord(events);
4791
5069
  const lastSeq = events.at(-1)?.seq;
4792
- const marks = persistence[journalMarksWanted] === true ? { marks: journalMarks(events) } : {};
5070
+ const marks = wantsMarks ? { marks: journalMarks(events) } : {};
4793
5071
  const appended = appendTail.then(async () => {
4794
- const result = await persistence.append({ expected: journalHead, idempotencyKey: `${writerId}:${events[0]?.seq}`, data, ...marks });
5072
+ const result = await persistence.append({ expected: journalHead, idempotencyKey: `${writerId}:${events[0]?.seq}`, data, barrier, ...marks });
4795
5073
  if (!validCursor(result?.cursor))
4796
5074
  throw new TypeError("persistence.append() must resolve to { cursor }");
4797
5075
  journalHead = result.cursor;
@@ -4807,12 +5085,15 @@ async function createFxEngine(options = {}) {
4807
5085
  let openTurnId = null;
4808
5086
  let lastTurnId = null;
4809
5087
  let ambiguousCalls = [];
4810
- const rerunIds = (onAmbiguous) => {
5088
+ const settleAmbiguous = (onAmbiguous) => {
4811
5089
  const calls = ambiguousCalls;
4812
5090
  ambiguousCalls = [];
4813
- if (onAmbiguous === undefined)
4814
- return [];
4815
- return calls.filter((call) => onAmbiguous({ executionId: call.id, name: call.name, input: parsedToolInput(call.arguments) }) === "rerun").map((call) => call.id);
5091
+ const rerun = onAmbiguous === undefined ? [] : calls.filter((call) => onAmbiguous({ executionId: call.id, name: call.name, input: parsedToolInput(call.arguments) }) === "rerun").map((call) => call.id);
5092
+ return { rerun, answered: calls.filter((call) => !rerun.includes(call.id)) };
5093
+ };
5094
+ const resumeOpenTurn = (promptOptions) => {
5095
+ const { rerun, answered } = settleAmbiguous(promptOptions.onAmbiguous);
5096
+ return normalizeTurn(startTurn(null, { ...promptOptions, [resumeTurn]: true, [rerunCallIds]: rerun }), answered);
4816
5097
  };
4817
5098
  const followUps = [];
4818
5099
  const runNextFollowUp = (resuming = false) => {
@@ -4834,6 +5115,8 @@ async function createFxEngine(options = {}) {
4834
5115
  let journalQueue = [];
4835
5116
  let journalFlushScheduled = false;
4836
5117
  const journalPending = new Set;
5118
+ let journalInFlight = null;
5119
+ let journalBarrier = false;
4837
5120
  let journalFailure = null;
4838
5121
  let handedOff = false;
4839
5122
  const handedOffError = () => new Error("this agent handed its session off; open it again to continue");
@@ -4873,7 +5156,7 @@ async function createFxEngine(options = {}) {
4873
5156
  journalFailureReported = true;
4874
5157
  return journalFailure;
4875
5158
  };
4876
- const queueJournalEvents = (events) => {
5159
+ const queueJournalEvents = (events, barrier = false) => {
4877
5160
  if (journalFailure || !Array.isArray(events) || events.length === 0)
4878
5161
  return;
4879
5162
  if (handedOff) {
@@ -4882,6 +5165,8 @@ async function createFxEngine(options = {}) {
4882
5165
  return;
4883
5166
  }
4884
5167
  journalQueue.push(...events);
5168
+ if (barrier || events.some((event) => event?.type === "input_accepted" || event?.type === "input_withdrawn"))
5169
+ journalBarrier = true;
4885
5170
  if (events.every((event) => event?.type === "session_config"))
4886
5171
  return;
4887
5172
  if (journalFlushScheduled)
@@ -4891,14 +5176,16 @@ async function createFxEngine(options = {}) {
4891
5176
  };
4892
5177
  function flushJournal() {
4893
5178
  journalFlushScheduled = false;
4894
- if (journalFailure || journalQueue.length === 0)
5179
+ if (journalFailure || journalInFlight || journalQueue.length === 0)
4895
5180
  return;
4896
5181
  const batch = journalQueue;
4897
5182
  journalQueue = [];
4898
- emit("journal.append", { events: batch.length });
5183
+ const barrier = journalBarrier;
5184
+ journalBarrier = false;
5185
+ emit("journal.append", { events: batch.length, barrier });
4899
5186
  let appended;
4900
5187
  try {
4901
- appended = Promise.resolve(journal.append(batch));
5188
+ appended = Promise.resolve(journal.append(batch, barrier));
4902
5189
  } catch (error) {
4903
5190
  appended = Promise.reject(error);
4904
5191
  }
@@ -4912,7 +5199,12 @@ async function createFxEngine(options = {}) {
4912
5199
  emit("journal.error", { error: error instanceof Error ? error.name : "Error" });
4913
5200
  rejectInputWaiters([...inputWaiters.keys()], journalFailure);
4914
5201
  activeTurn?.cancel();
4915
- }).finally(() => journalPending.delete(settled));
5202
+ }).finally(() => {
5203
+ journalPending.delete(settled);
5204
+ journalInFlight = null;
5205
+ flushJournal();
5206
+ });
5207
+ journalInFlight = settled;
4916
5208
  journalPending.add(settled);
4917
5209
  }
4918
5210
  let journalClosed = null;
@@ -4922,6 +5214,8 @@ async function createFxEngine(options = {}) {
4922
5214
  }
4923
5215
  async function journalSettled() {
4924
5216
  for (;; ) {
5217
+ if (journalQueue.length > 0)
5218
+ journalBarrier = true;
4925
5219
  if (journalFlushScheduled || journalQueue.length > 0)
4926
5220
  flushJournal();
4927
5221
  if (journalPending.size === 0)
@@ -5202,7 +5496,7 @@ async function createFxEngine(options = {}) {
5202
5496
  emit("acp.receive", { message });
5203
5497
  if (message.method === "libfx/journal_append") {
5204
5498
  if (journal && message.params?.sessionId === sessionId) {
5205
- queueJournalEvents(message.params.events);
5499
+ queueJournalEvents(message.params.events, message.params.barrier === true);
5206
5500
  }
5207
5501
  return;
5208
5502
  }
@@ -5282,7 +5576,11 @@ async function createFxEngine(options = {}) {
5282
5576
  sessionId = sessionResult.sessionId;
5283
5577
  if (initialCheckpoint) {
5284
5578
  const [checkpointAttachment] = attachBytes([initialCheckpoint]);
5285
- const restored = await request("libfx/restore", { sessionId, checkpointAttachment });
5579
+ const restored = await request("libfx/restore", { sessionId, checkpointAttachment }).catch((error) => {
5580
+ if (/newer fx/.test(error?.message ?? ""))
5581
+ throw new FxJournalVersionError(error.message);
5582
+ throw error;
5583
+ });
5286
5584
  compareCheckpointMeta(restored?.checkpointMeta);
5287
5585
  }
5288
5586
  if (journal) {
@@ -5373,7 +5671,7 @@ async function createFxEngine(options = {}) {
5373
5671
  if (turnId !== undefined && journal) {
5374
5672
  if (resumable && turnId === openTurnId) {
5375
5673
  resumable = false;
5376
- return normalizeTurn(startTurn(null, { ...promptOptions, [resumeTurn]: true, [rerunCallIds]: rerunIds(promptOptions.onAmbiguous) }));
5674
+ return resumeOpenTurn(promptOptions);
5377
5675
  }
5378
5676
  if (turnId === lastTurnId)
5379
5677
  return endedTurn(turnId);
@@ -5394,7 +5692,7 @@ async function createFxEngine(options = {}) {
5394
5692
  checkOnAmbiguous(promptOptions.onAmbiguous);
5395
5693
  if (resumable) {
5396
5694
  resumable = false;
5397
- return normalizeTurn(startTurn(null, { ...promptOptions, [resumeTurn]: true, [rerunCallIds]: rerunIds(promptOptions.onAmbiguous) }));
5695
+ return resumeOpenTurn(promptOptions);
5398
5696
  }
5399
5697
  const next = followUps.find((entry) => entry.held);
5400
5698
  if (!next)
@@ -5494,7 +5792,7 @@ async function createFxEngine(options = {}) {
5494
5792
  }
5495
5793
  };
5496
5794
  return agent;
5497
- function normalizeTurn(rawTurn) {
5795
+ function normalizeTurn(rawTurn, answered = []) {
5498
5796
  const toolNames = new Map;
5499
5797
  const started = new Set;
5500
5798
  const eventFor = (update) => {
@@ -5555,6 +5853,9 @@ async function createFxEngine(options = {}) {
5555
5853
  },
5556
5854
  [Symbol.asyncIterator]() {
5557
5855
  const iterator = async function* () {
5856
+ for (const call of answered) {
5857
+ yield { type: "tool_end", id: call.id, name: call.name, content: unfinishedToolOutput, isError: true };
5858
+ }
5558
5859
  for await (const update of rawTurn) {
5559
5860
  const event = eventFor(update);
5560
5861
  if (event)
@@ -5934,6 +6235,7 @@ async function createFxEngine(options = {}) {
5934
6235
 
5935
6236
  // sdk/fx-harness.js
5936
6237
  var encoder5 = new TextEncoder;
6238
+ var durableCheckpointAfterBytes = 64 * 1024;
5937
6239
  function fxHarness({ createEngine, defaultApiKey = async () => {
5938
6240
  return;
5939
6241
  } }) {
@@ -5958,7 +6260,7 @@ function fxHarness({ createEngine, defaultApiKey = async () => {
5958
6260
  [credentialMemory]: credentials,
5959
6261
  persistence: { ...store, [journalMarksWanted]: true },
5960
6262
  inputsDurable: true,
5961
- checkpointAfterBytes: 0,
6263
+ checkpointAfterBytes: durableCheckpointAfterBytes,
5962
6264
  ...context === undefined || context === null ? {} : { toolContext: context }
5963
6265
  });
5964
6266
  return fxSession(engine, idempotent);
@@ -6080,9 +6382,16 @@ function claimedTools(tools, ready) {
6080
6382
  }
6081
6383
  function idempotentTools(tools) {
6082
6384
  const names = new Set;
6083
- for (const tool of toolEntries(tools))
6084
- if (tool?.idempotent === true && typeof tool.name === "string")
6385
+ for (const [index, tool] of toolEntries(tools).entries()) {
6386
+ if (!tool || typeof tool !== "object" || tool.providerExecuted === true)
6387
+ continue;
6388
+ if (typeof tool.idempotent !== "boolean") {
6389
+ const name = typeof tool.name === "string" ? tool.name : index;
6390
+ throw new TypeError(`tool ${name} must set idempotent: true if running a call twice is safe, or idempotent: false if it is not`);
6391
+ }
6392
+ if (tool.idempotent && typeof tool.name === "string")
6085
6393
  names.add(tool.name);
6394
+ }
6086
6395
  return names;
6087
6396
  }
6088
6397
  function checkpointBytesOf(value) {
@@ -6107,7 +6416,7 @@ function snapshotOf(checkpoint) {
6107
6416
  }
6108
6417
 
6109
6418
  // sdk/node.js
6110
- var libfxApiVersion = 2;
6419
+ var libfxApiVersion = 3;
6111
6420
  var nativeCoreApiVersion = 4;
6112
6421
  var fetchOperationStale = 0;
6113
6422
  var fetchOperationApplied = 1;
@@ -6792,14 +7101,26 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
6792
7101
  async function createFxEngine2(options = {}) {
6793
7102
  return createWithFallback("agent", "createCore", createFxEngine, defaultCoreWasm, options);
6794
7103
  }
6795
- async function environmentDurability() {
7104
+ var environments = new Map;
7105
+ function environmentDurability() {
7106
+ const dir = process.env.VERCEL ? null : process.env.FX_SESSIONS_DIR || import_node_path.resolve(import_node_os.tmpdir(), "libfx", "sessions");
7107
+ const key = dir ?? "vercel";
7108
+ if (!environments.has(key)) {
7109
+ environments.set(key, chooseEnvironmentDurability(dir).catch((error) => {
7110
+ environments.delete(key);
7111
+ throw error;
7112
+ }));
7113
+ }
7114
+ return environments.get(key);
7115
+ }
7116
+ async function chooseEnvironmentDurability(dir) {
6796
7117
  const load = (asset) => import((asset.protocol === "" ? bundledAssetUrl(asset) : asset).href);
6797
- if (process.env.VERCEL) {
7118
+ if (dir === null) {
6798
7119
  const { vercel } = await load(new URL("./durable/vercel.mjs", __libfxModuleUrl));
6799
7120
  return vercel();
6800
7121
  }
6801
7122
  const { local } = await load(new URL("./durable/local.mjs", __libfxModuleUrl));
6802
- return local({ dir: process.env.FX_SESSIONS_DIR || import_node_path.resolve(import_node_os.tmpdir(), "libfx", "sessions") });
7123
+ return local({ dir });
6803
7124
  }
6804
7125
  var createFxAgent = createDurableAgentFactory({
6805
7126
  name: "createFxAgent",