@bastani/atomic 0.9.61-alpha.2 → 0.9.61-alpha.4

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.
Files changed (28) hide show
  1. package/CHANGELOG.md +27 -2
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/package.json +1 -1
  4. package/dist/builtin/subagents/package.json +1 -1
  5. package/dist/builtin/web-access/package.json +1 -1
  6. package/dist/builtin/workflows/package.json +1 -1
  7. package/dist/builtin/workflows/src/extension/index.bundle.mjs +49 -9
  8. package/dist/builtin/workflows/src/extension/local-worker-entry.mjs +93 -13
  9. package/dist/builtin/workflows/src/index.js +49 -9
  10. package/dist/builtin/workflows/src/local-worker-entry.mjs +93 -13
  11. package/dist/modes/interactive-engine/engine-render-service.d.ts +8 -0
  12. package/dist/modes/interactive-engine/engine-render-service.d.ts.map +1 -1
  13. package/dist/modes/interactive-engine/engine-render-service.js +28 -6
  14. package/dist/modes/interactive-engine/engine-render-service.js.map +1 -1
  15. package/dist/modes/interactive-engine/remote-renderer.d.ts +6 -0
  16. package/dist/modes/interactive-engine/remote-renderer.d.ts.map +1 -1
  17. package/dist/modes/interactive-engine/remote-renderer.js +17 -3
  18. package/dist/modes/interactive-engine/remote-renderer.js.map +1 -1
  19. package/dist/modes/rpc/queued-writer.d.ts +2 -0
  20. package/dist/modes/rpc/queued-writer.d.ts.map +1 -1
  21. package/dist/modes/rpc/queued-writer.js +8 -0
  22. package/dist/modes/rpc/queued-writer.js.map +1 -1
  23. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  24. package/dist/modes/rpc/rpc-client.js +4 -0
  25. package/dist/modes/rpc/rpc-client.js.map +1 -1
  26. package/docs/workflows/operations.md +1 -1
  27. package/npm-shrinkwrap.json +32 -32
  28. package/package.json +3 -3
@@ -17462,6 +17462,7 @@ class SqlResourceAdmission {
17462
17462
  epoch: this.options.attempt.epoch
17463
17463
  }) : null);
17464
17464
  });
17465
+ let quarantinedSince;
17465
17466
  try {
17466
17467
  for (;; ) {
17467
17468
  signal?.throwIfAborted();
@@ -17471,7 +17472,9 @@ class SqlResourceAdmission {
17471
17472
  }
17472
17473
  const hostReason = this.hostReason();
17473
17474
  const pressure = this.stageStartPressure(kind, request?.delegated);
17475
+ let quarantined = undefined;
17474
17476
  const granted = await this.storage.transaction(async (tx) => {
17477
+ quarantined = undefined;
17475
17478
  await tx.run(`UPDATE ${this.owners} SET expires_at = CAST(unixepoch('subsec') * 1000 AS INTEGER) + ? WHERE id = ?`, OWNER_LEASE_MS, this.owner);
17476
17479
  const { root } = this.options;
17477
17480
  if (!await this.assertCurrentAttempt(tx)) {
@@ -17489,6 +17492,8 @@ class SqlResourceAdmission {
17489
17492
  if (control && control.state !== "running")
17490
17493
  reason ??= `root ${control.state}`;
17491
17494
  }
17495
+ const held = [];
17496
+ let blockedOtherwise = reason !== undefined;
17492
17497
  for (const scope of [`root:${root}`, "user"]) {
17493
17498
  const row = await tx.get(`SELECT config FROM ${this.configuration} WHERE scope = ?`, scope);
17494
17499
  const limit = row ? JSON.parse(String(row.config))[kind] : undefined;
@@ -17498,9 +17503,21 @@ class SqlResourceAdmission {
17498
17503
  const args = scope === "user" ? [kind] : [kind, root];
17499
17504
  const count = await tx.get(`SELECT COUNT(*) AS count FROM ${this.permits} WHERE kind = ? ${rootClause} AND state = 'granted'`, ...args);
17500
17505
  const older = await tx.get(`SELECT 1 AS waiting FROM ${this.permits} WHERE kind = ? ${rootClause} AND state = 'waiting' AND reason IS NOT ? AND ticket < (SELECT ticket FROM ${this.permits} WHERE operation = ?) LIMIT 1`, ...args, STAGE_START_PRESSURE_REASON, operation);
17501
- if (Number(count?.count) >= limit || older)
17502
- reason ??= `${scope} ${kind}`;
17506
+ const used = Number(count?.count);
17507
+ if (used < limit && !older)
17508
+ continue;
17509
+ reason ??= `${scope} ${kind}`;
17510
+ const quarantinedPermits = kind === "process" && used >= limit ? await tx.all(`SELECT process_identity FROM ${this.permits} WHERE kind = ? ${rootClause} AND state = 'granted' AND reason = ?`, ...args, QUARANTINED_REASON) : [];
17511
+ if (quarantinedPermits.length > 0 && used - quarantinedPermits.length < limit)
17512
+ held.push({
17513
+ limit: `${scope === "user" ? "admission.userLimits" : "admission.limits"}.${kind} (${limit})`,
17514
+ groups: quarantinedPermits.flatMap(({ process_identity }) => process_identity === null ? [] : [JSON.parse(process_identity).id])
17515
+ });
17516
+ else
17517
+ blockedOtherwise = true;
17503
17518
  }
17519
+ if (!blockedOtherwise && held.length > 0)
17520
+ quarantined = held;
17504
17521
  if (reason && request?.delegated)
17505
17522
  throw new Error(`Delegated session cannot be admitted without capacity headroom: ${reason}`);
17506
17523
  const stageStart = kind === "session" && request?.delegated !== true;
@@ -17513,6 +17530,14 @@ class SqlResourceAdmission {
17513
17530
  signal?.throwIfAborted();
17514
17531
  return release;
17515
17532
  }
17533
+ if (quarantined === undefined)
17534
+ quarantinedSince = undefined;
17535
+ else {
17536
+ const now = performance.now();
17537
+ quarantinedSince ??= now;
17538
+ if (now - quarantinedSince >= QUARANTINED_CAPACITY_WAIT_MS)
17539
+ throw new QuarantinedCapacityError(quarantined, now - quarantinedSince);
17540
+ }
17516
17541
  await delay(50 + Math.floor(Math.random() * 25), undefined, { signal });
17517
17542
  }
17518
17543
  } catch (error) {
@@ -17554,9 +17579,9 @@ class SqlResourceAdmission {
17554
17579
  await tx.run(`UPDATE ${this.permits} SET state = 'released', reason = 'host reboot confirmed' WHERE owner = ? AND state = 'granted'`, owner.id);
17555
17580
  } else if (stopped) {
17556
17581
  await tx.run(`UPDATE ${this.permits} SET state = 'released', reason = 'owner death confirmed' WHERE owner = ? AND kind = 'session' AND state = 'granted'`, owner.id);
17557
- await tx.run(`UPDATE ${this.permits} SET reason = ? WHERE owner = ? AND kind = 'process' AND state = 'granted' AND reason IS NOT ?`, UNCONFIRMED_REASON, owner.id, UNCONFIRMED_REASON);
17582
+ await tx.run(`UPDATE ${this.permits} SET reason = ? WHERE owner = ? AND kind = 'process' AND state = 'granted' AND reason IS NOT ? AND reason IS NOT ?`, UNCONFIRMED_REASON, owner.id, UNCONFIRMED_REASON, QUARANTINED_REASON);
17558
17583
  } else {
17559
- await tx.run(`UPDATE ${this.permits} SET reason = ? WHERE owner = ? AND state = 'granted' AND reason IS NOT ? AND EXISTS (SELECT 1 FROM ${this.owners} WHERE id = ? AND expires_at < ${NOW_MS})`, EXPIRED_REASON, owner.id, EXPIRED_REASON, owner.id);
17584
+ await tx.run(`UPDATE ${this.permits} SET reason = ? WHERE owner = ? AND state = 'granted' AND reason IS NOT ? AND reason IS NOT ? AND EXISTS (SELECT 1 FROM ${this.owners} WHERE id = ? AND expires_at < ${NOW_MS})`, EXPIRED_REASON, owner.id, EXPIRED_REASON, QUARANTINED_REASON, owner.id);
17560
17585
  }
17561
17586
  if (owner.host === admissionHost && process.platform !== "win32") {
17562
17587
  const processes = await tx.all(`SELECT * FROM ${this.permits} WHERE owner = ? AND state = 'granted' AND kind = 'process' AND process_identity IS NOT NULL`, owner.id);
@@ -17584,9 +17609,10 @@ class SqlResourceAdmission {
17584
17609
  return true;
17585
17610
  if (permit.state === "waiting")
17586
17611
  return stopped || !!permit.expired;
17587
- if (stopped && (permit.kind === "session" || permit.reason !== UNCONFIRMED_REASON))
17612
+ const quarantined = permit.reason === QUARANTINED_REASON;
17613
+ if (stopped && (permit.kind === "session" || permit.reason !== UNCONFIRMED_REASON && !quarantined))
17588
17614
  return true;
17589
- if (!stopped && permit.expired && permit.reason !== EXPIRED_REASON)
17615
+ if (!stopped && permit.expired && permit.reason !== EXPIRED_REASON && !quarantined)
17590
17616
  return true;
17591
17617
  return permit.kind === "process" && permit.host === admissionHost && process.platform !== "win32" && permit.process_identity !== null && processGroupStopped(permit.process_identity);
17592
17618
  }
@@ -17597,7 +17623,9 @@ class SqlResourceAdmission {
17597
17623
  await this.reconcile();
17598
17624
  return this.storage.transaction(async (tx) => {
17599
17625
  await tx.run(`UPDATE ${this.permits} SET state = 'released', reason = 'worker exit confirmed' WHERE root = ? AND holder = ? AND state IN ('waiting', 'granted') AND (kind = 'session' OR state = 'waiting')`, this.options.root, holder);
17600
- return (await tx.run(`UPDATE ${this.permits} SET reason = 'cleanup unconfirmed' WHERE root = ? AND holder = ? AND kind = 'process' AND state = 'granted'`, this.options.root, holder)).changes;
17626
+ await tx.run(`UPDATE ${this.permits} SET reason = ? WHERE root = ? AND holder = ? AND kind = 'process' AND state = 'granted' AND reason IS NOT ?`, UNCONFIRMED_REASON, this.options.root, holder, QUARANTINED_REASON);
17627
+ const held = await tx.get(`SELECT COUNT(*) AS count FROM ${this.permits} WHERE root = ? AND holder = ? AND kind = 'process' AND state = 'granted'`, this.options.root, holder);
17628
+ return Number(held?.count);
17601
17629
  });
17602
17630
  }
17603
17631
  async reapStoppedProcesses(holder) {
@@ -17644,10 +17672,13 @@ async function settleProcessGroups(storage, root, scope, reap) {
17644
17672
  survivors = await settle(REAP_KILL_GRACE_MS);
17645
17673
  }
17646
17674
  const confirmed = [...groups].flatMap(([group, operations]) => survivors.includes(group) ? [] : operations);
17647
- if (confirmed.length > 0)
17675
+ const quarantined = reap ? survivors.flatMap((group) => groups.get(group) ?? []) : [];
17676
+ if (confirmed.length > 0 || quarantined.length > 0)
17648
17677
  await storage.transaction(async (tx) => {
17649
17678
  for (const operation of confirmed)
17650
17679
  await tx.run(`UPDATE ${permits} SET state = 'released', reason = 'process group cleanup confirmed' WHERE operation = ? AND state = 'granted'`, operation);
17680
+ for (const operation of quarantined)
17681
+ await tx.run(`UPDATE ${permits} SET reason = ? WHERE operation = ? AND state = 'granted'`, QUARANTINED_REASON, operation);
17651
17682
  });
17652
17683
  return { survivors, unprobeable };
17653
17684
  }
@@ -17664,11 +17695,20 @@ async function reapProcessGroups(journal, root, scope) {
17664
17695
  return { survivors: [], unprobeable: [] };
17665
17696
  return settleProcessGroups(storage, root, scope, true);
17666
17697
  }
17667
- var OWNER_LEASE_MS = 30000, REAP_TERM_GRACE_MS = 5000, REAP_KILL_GRACE_MS = 1e4, REAP_POLL_MS = 50, RELEASED_PERMIT_RETENTION = 256, RELEASED_PERMIT_PRUNE_BATCH = 1000, EXPIRED_REASON = "owner expired; cleanup unconfirmed", UNCONFIRMED_REASON = "cleanup unconfirmed", ANONYMOUS_REASON = "owner identity unavailable; cleanup unconfirmed", NOW_MS = "CAST(unixepoch('subsec') * 1000 AS INTEGER)", LATE_ATTEMPT_POLL_MS = 100, LATE_ATTEMPT_REASON = "waiting for its attempt lease", STAGE_START_SPACING_MS = 5000, STAGE_START_PRESSURE_REASON = "paced for host load";
17698
+ var OWNER_LEASE_MS = 30000, REAP_TERM_GRACE_MS = 5000, REAP_KILL_GRACE_MS = 1e4, REAP_POLL_MS = 50, RELEASED_PERMIT_RETENTION = 256, RELEASED_PERMIT_PRUNE_BATCH = 1000, EXPIRED_REASON = "owner expired; cleanup unconfirmed", UNCONFIRMED_REASON = "cleanup unconfirmed", QUARANTINED_REASON = "cleanup unconfirmed; permit quarantined", ANONYMOUS_REASON = "owner identity unavailable; cleanup unconfirmed", QUARANTINED_CAPACITY_WAIT_MS, QuarantinedCapacityError, NOW_MS = "CAST(unixepoch('subsec') * 1000 AS INTEGER)", LATE_ATTEMPT_POLL_MS = 100, LATE_ATTEMPT_REASON = "waiting for its attempt lease", STAGE_START_SPACING_MS = 5000, STAGE_START_PRESSURE_REASON = "paced for host load";
17668
17699
  var init_resource_admission = __esm(() => {
17669
17700
  init_internal();
17670
17701
  init_sql_run_journal();
17702
+ init_workflow_infrastructure_error();
17671
17703
  init_admission_host();
17704
+ QUARANTINED_CAPACITY_WAIT_MS = REAP_TERM_GRACE_MS + REAP_KILL_GRACE_MS;
17705
+ QuarantinedCapacityError = class QuarantinedCapacityError extends WorkflowInfrastructureError {
17706
+ constructor(capacity, waitedMs) {
17707
+ const limits = capacity.map(({ limit }) => limit).join(" and ");
17708
+ const groups = [...new Set(capacity.flatMap(({ groups }) => groups))].join(", ");
17709
+ super("worker_cleanup_unconfirmed", `Local worker process cleanup unconfirmed; process permit refused after ${Math.round(waitedMs)} ms: ${limits} ${capacity.length === 1 ? "is" : "are"} held by quarantined permits of process groups ${groups} that survived SIGKILL`);
17710
+ }
17711
+ };
17672
17712
  });
17673
17713
 
17674
17714
  // dist/builtin/workflows/src/workers/admission-binding.ts
@@ -81600,6 +81640,30 @@ class WorkAttemptFencedError extends Error {
81600
81640
  this.name = "WorkAttemptFencedError";
81601
81641
  }
81602
81642
  }
81643
+ var FENCED_STAGE_CLOSE_MS = 1e4;
81644
+ async function closeAttemptStages(stageControls, store, renew) {
81645
+ const closing = Promise.allSettled(store.runs().map((run) => stageControls.clear(run.id))).then((results) => {
81646
+ for (const result of results)
81647
+ if (result.status === "rejected")
81648
+ console.warn("atomic-workflows: a fenced attempt's stage session did not close", result.reason);
81649
+ });
81650
+ let renewal;
81651
+ const pulse = () => {
81652
+ renewal ??= renew().then(() => {}, () => {}).finally(() => {
81653
+ renewal = undefined;
81654
+ });
81655
+ };
81656
+ pulse();
81657
+ const heartbeat = setInterval(pulse, WORKER_HEARTBEAT_INTERVAL_MS);
81658
+ const bound = new AbortController;
81659
+ try {
81660
+ await Promise.race([closing, delay4(FENCED_STAGE_CLOSE_MS, undefined, { signal: bound.signal }).catch(() => {})]);
81661
+ } finally {
81662
+ clearInterval(heartbeat);
81663
+ bound.abort();
81664
+ await renewal;
81665
+ }
81666
+ }
81603
81667
  function trackTurnStarts(registry, started) {
81604
81668
  return new Proxy(registry, {
81605
81669
  get(target, key) {
@@ -81760,7 +81824,7 @@ async function runWorker(start) {
81760
81824
  }
81761
81825
  return state;
81762
81826
  };
81763
- const backend = new Proxy(reader, {
81827
+ const attemptBackend = (write) => new Proxy(reader, {
81764
81828
  get(target, key) {
81765
81829
  if (typeof key !== "string" || key === "then" || /^(archive|readSettled|readPending|hasCached|hasPending|getPending)/.test(key))
81766
81830
  return;
@@ -81779,7 +81843,7 @@ async function runWorker(start) {
81779
81843
  if (key === "commitCheckpoint" || key === "commitAdditiveCheckpointBestEffort")
81780
81844
  return async (checkpoint, options) => {
81781
81845
  options?.signal?.throwIfAborted();
81782
- await commit({ kind: "checkpoint", checkpoint });
81846
+ await write({ kind: "checkpoint", checkpoint });
81783
81847
  return true;
81784
81848
  };
81785
81849
  if (typeof key === "string" && reads.has(key)) {
@@ -81850,6 +81914,9 @@ async function runWorker(start) {
81850
81914
  activeController = controller;
81851
81915
  heldAttempt = work;
81852
81916
  const store = createStore();
81917
+ let writable = true;
81918
+ const write = (mutation) => writable ? commit(mutation) : Promise.reject(new WorkAttemptFencedError("Work attempt ended"));
81919
+ const backend = attemptBackend(write);
81853
81920
  currentStore = store;
81854
81921
  const stageUiBroker = new WorkerStageUiBroker(store);
81855
81922
  prompts = { store, broker: stageUiBroker, fail: failablePrompts(store) };
@@ -81929,7 +81996,18 @@ async function runWorker(start) {
81929
81996
  const resourceAdmission = {
81930
81997
  async acquire(kind, signal, requestOptions) {
81931
81998
  await awaitAdmission(signal);
81932
- return admission ? admission.acquire(kind, signal, requestOptions) : async () => {};
81999
+ if (!admission)
82000
+ return async () => {};
82001
+ try {
82002
+ return await admission.acquire(kind, signal, requestOptions);
82003
+ } catch (error) {
82004
+ if (error instanceof QuarantinedCapacityError && !attemptFenced) {
82005
+ poolFault = error.message;
82006
+ attemptFenced = true;
82007
+ controller.abort(new WorkAttemptFencedError(error.message));
82008
+ }
82009
+ throw error;
82010
+ }
81933
82011
  }
81934
82012
  };
81935
82013
  const admitted = admittedSessionAdapter(adapter, resourceAdmission, async (signal) => {
@@ -81985,7 +82063,7 @@ async function runWorker(start) {
81985
82063
  backend: reader,
81986
82064
  store,
81987
82065
  root: start.root,
81988
- commit
82066
+ commit: write
81989
82067
  })
81990
82068
  }
81991
82069
  }));
@@ -82025,6 +82103,8 @@ async function runWorker(start) {
82025
82103
  await renewal;
82026
82104
  await report;
82027
82105
  await controlPulse;
82106
+ await closeAttemptStages(stageControls, store, () => request({ kind: "renew" }));
82107
+ writable = false;
82028
82108
  await request({ kind: "fenced", ...poolFault === undefined ? {} : { fault: poolFault } });
82029
82109
  }
82030
82110
  if (rootLeaseLost)
@@ -1,6 +1,14 @@
1
1
  import type { AgentSession } from "../../core/agent-session.js";
2
+ /**
3
+ * How many disposed row ids the engine remembers. A render request can reach the engine after its row's disposal:
4
+ * renders are coalesced behind the host's ordered writes, and a render's callback runs a turn after its line is read.
5
+ * Without the tombstone that late render would rebuild the record the disposal released, for the rest of the session.
6
+ * Row ids are never reused, and a render this many disposals late does not happen in practice.
7
+ */
8
+ export declare const DISPOSED_RENDER_TOMBSTONE_LIMIT = 10000;
2
9
  export declare class EngineRenderService {
3
10
  private readonly records;
11
+ private readonly disposedIds;
4
12
  private session;
5
13
  private readonly write;
6
14
  constructor(write: (line: string) => void);
@@ -1 +1 @@
1
- {"version":3,"file":"engine-render-service.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive-engine/engine-render-service.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AA2DhE,qBAAa,mBAAmB;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;IAC3D,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyB;IAE/C,YAAY,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAExC;IAED,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAGvC;IAED,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAgChC;IAED,OAAO,IAAI,IAAI,CAEd;IAED,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,UAAU;IA8ClB,OAAO,CAAC,aAAa;IA8BrB,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,IAAI;CAGZ"}
1
+ {"version":3,"file":"engine-render-service.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive-engine/engine-render-service.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AA2DhE;;;;;GAKG;AACH,eAAO,MAAM,+BAA+B,QAAS,CAAC;AAEtD,qBAAa,mBAAmB;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;IAC3D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IACjD,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyB;IAE/C,YAAY,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAExC;IAED,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAGvC;IAED,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAyChC;IAED,OAAO,IAAI,IAAI,CAEd;IAED,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,UAAU;IA8ClB,OAAO,CAAC,aAAa;IA8BrB,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,IAAI;CAGZ"}
@@ -29,8 +29,16 @@ class RenderTerminal {
29
29
  setProgress() { }
30
30
  setProgramStatus() { }
31
31
  }
32
+ /**
33
+ * How many disposed row ids the engine remembers. A render request can reach the engine after its row's disposal:
34
+ * renders are coalesced behind the host's ordered writes, and a render's callback runs a turn after its line is read.
35
+ * Without the tombstone that late render would rebuild the record the disposal released, for the rest of the session.
36
+ * Row ids are never reused, and a render this many disposals late does not happen in practice.
37
+ */
38
+ export const DISPOSED_RENDER_TOMBSTONE_LIMIT = 10_000;
32
39
  export class EngineRenderService {
33
40
  records = new Map();
41
+ disposedIds = new Set();
34
42
  session;
35
43
  write;
36
44
  constructor(write) {
@@ -46,6 +54,12 @@ export class EngineRenderService {
46
54
  return false;
47
55
  if (command.type === "engine_render_dispose") {
48
56
  this.disposeRecord(command.componentId);
57
+ this.disposedIds.add(command.componentId);
58
+ if (this.disposedIds.size > DISPOSED_RENDER_TOMBSTONE_LIMIT) {
59
+ const oldest = this.disposedIds.values().next().value;
60
+ if (oldest !== undefined)
61
+ this.disposedIds.delete(oldest);
62
+ }
49
63
  return true;
50
64
  }
51
65
  if (command.type !== "engine_tool_render" && command.type !== "engine_message_render")
@@ -54,14 +68,22 @@ export class EngineRenderService {
54
68
  ? `tool:${command.toolName}`
55
69
  : `message:${command.message.customType ?? "custom"}`;
56
70
  void runCallback({ kind: "renderer", name }, async () => {
71
+ // The row may have been disposed before this render arrived, or while it waited for its turn: rendering it
72
+ // now would rebuild the record its disposal released.
73
+ if (this.disposedIds.has(command.componentId))
74
+ return undefined;
57
75
  return command.type === "engine_tool_render" ? this.renderTool(command) : this.renderMessage(command);
58
76
  })
59
- .then((lines) => this.send({
60
- type: "engine_custom_frame",
61
- componentId: command.componentId,
62
- requestId: command.requestId,
63
- lines,
64
- }))
77
+ .then((lines) => {
78
+ if (lines === undefined)
79
+ return;
80
+ this.send({
81
+ type: "engine_custom_frame",
82
+ componentId: command.componentId,
83
+ requestId: command.requestId,
84
+ lines,
85
+ });
86
+ })
65
87
  .catch((error) => this.send({
66
88
  type: "engine_custom_frame",
67
89
  componentId: command.componentId,
@@ -1 +1 @@
1
- {"version":3,"file":"engine-render-service.js","sourceRoot":"","sources":["../../../src/modes/interactive-engine/engine-render-service.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Terminal } from \"@earendil-works/pi-tui\";\nimport { type TUI, TuiMainScreen } from \"@earendil-works/pi-tui\";\nimport { getAgentDir } from \"../../config.js\";\nimport type { AgentSession } from \"../../core/agent-session.js\";\nimport { runCallback } from \"../../core/callback-activity.ts\";\nimport type { CustomMessage } from \"../../core/messages.ts\";\nimport { ensurePngTranscoder } from \"../../utils/image-convert.ts\";\nimport { CustomMessageComponent } from \"../interactive/components/custom-message.ts\";\nimport { ToolExecutionComponent } from \"../interactive/components/tool-execution.ts\";\nimport {\n\ttype InteractiveEngineCommand,\n\ttype InteractiveEngineMessage,\n\tparseInteractiveEngineCommand,\n\tserializeInteractiveEngineMessage,\n} from \"./protocol.ts\";\n\ntype ToolRenderCommand = Extract<InteractiveEngineCommand, { type: \"engine_tool_render\" }>;\ntype MessageRenderCommand = Extract<InteractiveEngineCommand, { type: \"engine_message_render\" }>;\n\n/**\n * Cached per-componentId render state. Records are REUSED across render\n * requests: recreating (and re-seeding) the component on every request made\n * the seeding setters call `ui.requestRender()` each time, whose eventual\n * terminal write emitted `engine_custom_invalidate` back to the host, which\n * re-sent the render request — a self-sustaining host⇄engine render\n * ping-pong that presented as full-screen TUI flicker (quit/pause while a\n * workflow stage streams). Reuse plus idempotent seeding setters lets the\n * loop converge after at most one round trip while still forwarding\n * legitimate async invalidations (e.g. image conversion). A record is rebuilt\n * once the session's extension runner changes, so rows rendered before\n * deferred resources loaded pick up the extensions' renderers.\n */\ntype RenderRecord = { runner: AgentSession[\"extensionRunner\"]; terminal: RenderTerminal; tui: TUI } & (\n\t| { kind: \"message\"; component: CustomMessageComponent }\n\t| { kind: \"tool\"; component: ToolExecutionComponent }\n);\n\nclass RenderTerminal implements Terminal {\n\tcolumns = 80;\n\trows = 24;\n\tkittyProtocolActive = false;\n\tprivate readonly requestRender: () => void;\n\tconstructor(requestRender: () => void) {\n\t\tthis.requestRender = requestRender;\n\t}\n\tstart(): void {}\n\tstop(): void {}\n\tasync drainInput(): Promise<void> {}\n\twrite(): void {\n\t\tthis.requestRender();\n\t}\n\tmoveBy(): void {}\n\thideCursor(): void {}\n\tshowCursor(): void {}\n\tclearLine(): void {}\n\tclearFromCursor(): void {}\n\tclearScreen(): void {}\n\tsetTitle(): void {}\n\tsetProgress(): void {}\n\tsetProgramStatus(): void {}\n}\n\nexport class EngineRenderService {\n\tprivate readonly records = new Map<string, RenderRecord>();\n\tprivate session: AgentSession | undefined;\n\tprivate readonly write: (line: string) => void;\n\n\tconstructor(write: (line: string) => void) {\n\t\tthis.write = write;\n\t}\n\n\tbindSession(session: AgentSession): void {\n\t\tthis.dispose();\n\t\tthis.session = session;\n\t}\n\n\thandleLine(line: string): boolean {\n\t\tconst command = parseInteractiveEngineCommand(line);\n\t\tif (!command?.type.startsWith(\"engine_\") || command.type.startsWith(\"engine_custom_\")) return false;\n\t\tif (command.type === \"engine_render_dispose\") {\n\t\t\tthis.disposeRecord(command.componentId);\n\t\t\treturn true;\n\t\t}\n\t\tif (command.type !== \"engine_tool_render\" && command.type !== \"engine_message_render\") return false;\n\t\tconst name =\n\t\t\tcommand.type === \"engine_tool_render\"\n\t\t\t\t? `tool:${command.toolName}`\n\t\t\t\t: `message:${command.message.customType ?? \"custom\"}`;\n\t\tvoid runCallback({ kind: \"renderer\", name }, async () => {\n\t\t\treturn command.type === \"engine_tool_render\" ? this.renderTool(command) : this.renderMessage(command);\n\t\t})\n\t\t\t.then((lines) =>\n\t\t\t\tthis.send({\n\t\t\t\t\ttype: \"engine_custom_frame\",\n\t\t\t\t\tcomponentId: command.componentId,\n\t\t\t\t\trequestId: command.requestId,\n\t\t\t\t\tlines,\n\t\t\t\t}),\n\t\t\t)\n\t\t\t.catch((error: Error) =>\n\t\t\t\tthis.send({\n\t\t\t\t\ttype: \"engine_custom_frame\",\n\t\t\t\t\tcomponentId: command.componentId,\n\t\t\t\t\trequestId: command.requestId,\n\t\t\t\t\tlines: [`Remote renderer failed: ${error.message}`],\n\t\t\t\t}),\n\t\t\t);\n\t\treturn true;\n\t}\n\n\tdispose(): void {\n\t\tfor (const id of [...this.records.keys()]) this.disposeRecord(id);\n\t}\n\n\tprivate boundSession(): AgentSession {\n\t\tconst session = this.session;\n\t\tif (!session) throw new Error(\"Renderer session is not bound\");\n\t\treturn session;\n\t}\n\n\tprivate createTerminal(componentId: string): RenderTerminal {\n\t\treturn new RenderTerminal(() => this.send({ type: \"engine_custom_invalidate\", componentId }));\n\t}\n\n\tprivate renderTool(command: ToolRenderCommand): string[] {\n\t\tlet record = this.records.get(command.componentId);\n\t\tif (record?.kind !== \"tool\" || record.runner !== this.session?.extensionRunner) {\n\t\t\tconst session = this.boundSession();\n\t\t\tthis.disposeRecord(command.componentId);\n\t\t\tconst terminal = this.createTerminal(command.componentId);\n\t\t\tterminal.columns = Math.max(1, command.width);\n\t\t\tconst tui = new TuiMainScreen(terminal, undefined, getAgentDir());\n\t\t\tensurePngTranscoder(() => {\n\t\t\t\ttui.invalidate();\n\t\t\t\ttui.requestRender();\n\t\t\t});\n\t\t\tconst component = new ToolExecutionComponent(\n\t\t\t\tcommand.toolName,\n\t\t\t\tcommand.toolCallId,\n\t\t\t\tcommand.args,\n\t\t\t\t{ showImages: command.showImages, imageWidthCells: command.imageWidthCells, outputPad: command.outputPad },\n\t\t\t\tsession.extensionRunner.resolveToolRenderers(command.toolName, () =>\n\t\t\t\t\tsession.getToolDefinition(command.toolName),\n\t\t\t\t),\n\t\t\t\ttui,\n\t\t\t\tsession.sessionManager.getCwd(),\n\t\t\t);\n\t\t\ttui.addChild(component);\n\t\t\trecord = { kind: \"tool\", component, terminal, tui, runner: session.extensionRunner };\n\t\t\tthis.records.set(command.componentId, record);\n\t\t} else {\n\t\t\trecord.terminal.columns = Math.max(1, command.width);\n\t\t\trecord.component.updateArgs(command.args);\n\t\t\trecord.component.setShowImages(command.showImages);\n\t\t\trecord.component.setImageWidthCells(command.imageWidthCells);\n\t\t\trecord.component.setOutputPad(command.outputPad ?? 1);\n\t\t}\n\t\tconst tool = record.component;\n\t\tif (command.executionStarted) tool.markExecutionStarted();\n\t\tif (command.argsComplete) tool.setArgsComplete();\n\t\ttool.setExpanded(command.expanded);\n\t\tif (command.result) {\n\t\t\ttool.updateResult(\n\t\t\t\tcommand.result as unknown as Parameters<ToolExecutionComponent[\"updateResult\"]>[0],\n\t\t\t\tcommand.isPartial,\n\t\t\t);\n\t\t}\n\t\treturn tool.render(command.width);\n\t}\n\n\tprivate renderMessage(command: MessageRenderCommand): string[] {\n\t\tlet record = this.records.get(command.componentId);\n\t\tif (record?.kind !== \"message\" || record.runner !== this.session?.extensionRunner) {\n\t\t\tconst session = this.boundSession();\n\t\t\tthis.disposeRecord(command.componentId);\n\t\t\tconst terminal = this.createTerminal(command.componentId);\n\t\t\tterminal.columns = Math.max(1, command.width);\n\t\t\tconst tui = new TuiMainScreen(terminal, undefined, getAgentDir());\n\t\t\tensurePngTranscoder(() => {\n\t\t\t\ttui.invalidate();\n\t\t\t\ttui.requestRender();\n\t\t\t});\n\t\t\tconst message = command.message as unknown as CustomMessage<object>;\n\t\t\tconst component = new CustomMessageComponent(\n\t\t\t\tmessage,\n\t\t\t\tsession.extensionRunner.getMessageRenderer(message.customType),\n\t\t\t\tundefined,\n\t\t\t\tcommand.outputPad,\n\t\t\t);\n\t\t\ttui.addChild(component);\n\t\t\trecord = { kind: \"message\", component, terminal, tui, runner: session.extensionRunner };\n\t\t\tthis.records.set(command.componentId, record);\n\t\t} else {\n\t\t\trecord.terminal.columns = Math.max(1, command.width);\n\t\t}\n\t\trecord.component.setExpanded(command.expanded);\n\t\trecord.component.setOutputPad(command.outputPad);\n\t\treturn record.component.render(command.width);\n\t}\n\n\tprivate disposeRecord(id: string): void {\n\t\tconst record = this.records.get(id);\n\t\tif (!record) return;\n\t\tthis.records.delete(id);\n\t\tif (record.kind === \"tool\") record.component.dispose();\n\t\trecord.tui.stop();\n\t}\n\n\tprivate send(message: InteractiveEngineMessage): void {\n\t\tthis.write(serializeInteractiveEngineMessage(message));\n\t}\n}\n"]}
1
+ {"version":3,"file":"engine-render-service.js","sourceRoot":"","sources":["../../../src/modes/interactive-engine/engine-render-service.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAE9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AACrF,OAAO,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AACrF,OAAO,EAGN,6BAA6B,EAC7B,iCAAiC,GACjC,MAAM,eAAe,CAAC;AAuBvB,MAAM,cAAc;IACnB,OAAO,GAAG,EAAE,CAAC;IACb,IAAI,GAAG,EAAE,CAAC;IACV,mBAAmB,GAAG,KAAK,CAAC;IACX,aAAa,CAAa;IAC3C,YAAY,aAAyB;QACpC,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;IACpC,CAAC;IACD,KAAK,KAAU,CAAC;IAChB,IAAI,KAAU,CAAC;IACf,KAAK,CAAC,UAAU,KAAmB,CAAC;IACpC,KAAK;QACJ,IAAI,CAAC,aAAa,EAAE,CAAC;IACtB,CAAC;IACD,MAAM,KAAU,CAAC;IACjB,UAAU,KAAU,CAAC;IACrB,UAAU,KAAU,CAAC;IACrB,SAAS,KAAU,CAAC;IACpB,eAAe,KAAU,CAAC;IAC1B,WAAW,KAAU,CAAC;IACtB,QAAQ,KAAU,CAAC;IACnB,WAAW,KAAU,CAAC;IACtB,gBAAgB,KAAU,CAAC;CAC3B;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG,MAAM,CAAC;AAEtD,MAAM,OAAO,mBAAmB;IACd,OAAO,GAAG,IAAI,GAAG,EAAwB,CAAC;IAC1C,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IACzC,OAAO,CAA2B;IACzB,KAAK,CAAyB;IAE/C,YAAY,KAA6B;QACxC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACpB,CAAC;IAED,WAAW,CAAC,OAAqB;QAChC,IAAI,CAAC,OAAO,EAAE,CAAC;QACf,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACxB,CAAC;IAED,UAAU,CAAC,IAAY;QACtB,MAAM,OAAO,GAAG,6BAA6B,CAAC,IAAI,CAAC,CAAC;QACpD,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC;YAAE,OAAO,KAAK,CAAC;QACpG,IAAI,OAAO,CAAC,IAAI,KAAK,uBAAuB,EAAE,CAAC;YAC9C,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;YACxC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;YAC1C,IAAI,IAAI,CAAC,WAAW,CAAC,IAAI,GAAG,+BAA+B,EAAE,CAAC;gBAC7D,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;gBACtD,IAAI,MAAM,KAAK,SAAS;oBAAE,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC3D,CAAC;YACD,OAAO,IAAI,CAAC;QACb,CAAC;QACD,IAAI,OAAO,CAAC,IAAI,KAAK,oBAAoB,IAAI,OAAO,CAAC,IAAI,KAAK,uBAAuB;YAAE,OAAO,KAAK,CAAC;QACpG,MAAM,IAAI,GACT,OAAO,CAAC,IAAI,KAAK,oBAAoB;YACpC,CAAC,CAAC,QAAQ,OAAO,CAAC,QAAQ,EAAE;YAC5B,CAAC,CAAC,WAAW,OAAO,CAAC,OAAO,CAAC,UAAU,IAAI,QAAQ,EAAE,CAAC;QACxD,KAAK,WAAW,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,KAAK,IAAI,EAAE;YACvD,2GAA2G;YAC3G,sDAAsD;YACtD,IAAI,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC;gBAAE,OAAO,SAAS,CAAC;YAChE,OAAO,OAAO,CAAC,IAAI,KAAK,oBAAoB,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QACvG,CAAC,CAAC;aACA,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;YACf,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO;YAChC,IAAI,CAAC,IAAI,CAAC;gBACT,IAAI,EAAE,qBAAqB;gBAC3B,WAAW,EAAE,OAAO,CAAC,WAAW;gBAChC,SAAS,EAAE,OAAO,CAAC,SAAS;gBAC5B,KAAK;aACL,CAAC,CAAC;QACJ,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,KAAY,EAAE,EAAE,CACvB,IAAI,CAAC,IAAI,CAAC;YACT,IAAI,EAAE,qBAAqB;YAC3B,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,KAAK,EAAE,CAAC,2BAA2B,KAAK,CAAC,OAAO,EAAE,CAAC;SACnD,CAAC,CACF,CAAC;QACH,OAAO,IAAI,CAAC;IACb,CAAC;IAED,OAAO;QACN,KAAK,MAAM,EAAE,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YAAE,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;IACnE,CAAC;IAEO,YAAY;QACnB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC7B,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;QAC/D,OAAO,OAAO,CAAC;IAChB,CAAC;IAEO,cAAc,CAAC,WAAmB;QACzC,OAAO,IAAI,cAAc,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,0BAA0B,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC;IAC/F,CAAC;IAEO,UAAU,CAAC,OAA0B;QAC5C,IAAI,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QACnD,IAAI,MAAM,EAAE,IAAI,KAAK,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,IAAI,CAAC,OAAO,EAAE,eAAe,EAAE,CAAC;YAChF,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;YACpC,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;YACxC,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;YAC1D,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;YAC9C,MAAM,GAAG,GAAG,IAAI,aAAa,CAAC,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC;YAClE,mBAAmB,CAAC,GAAG,EAAE;gBACxB,GAAG,CAAC,UAAU,EAAE,CAAC;gBACjB,GAAG,CAAC,aAAa,EAAE,CAAC;YACrB,CAAC,CAAC,CAAC;YACH,MAAM,SAAS,GAAG,IAAI,sBAAsB,CAC3C,OAAO,CAAC,QAAQ,EAChB,OAAO,CAAC,UAAU,EAClB,OAAO,CAAC,IAAI,EACZ,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,eAAe,EAAE,OAAO,CAAC,eAAe,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,EAC1G,OAAO,CAAC,eAAe,CAAC,oBAAoB,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,EAAE,CACnE,OAAO,CAAC,iBAAiB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAC3C,EACD,GAAG,EACH,OAAO,CAAC,cAAc,CAAC,MAAM,EAAE,CAC/B,CAAC;YACF,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;YACxB,MAAM,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,eAAe,EAAE,CAAC;YACrF,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QAC/C,CAAC;aAAM,CAAC;YACP,MAAM,CAAC,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;YACrD,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAC1C,MAAM,CAAC,SAAS,CAAC,aAAa,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;YACnD,MAAM,CAAC,SAAS,CAAC,kBAAkB,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;YAC7D,MAAM,CAAC,SAAS,CAAC,YAAY,CAAC,OAAO,CAAC,SAAS,IAAI,CAAC,CAAC,CAAC;QACvD,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,CAAC,SAAS,CAAC;QAC9B,IAAI,OAAO,CAAC,gBAAgB;YAAE,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAC1D,IAAI,OAAO,CAAC,YAAY;YAAE,IAAI,CAAC,eAAe,EAAE,CAAC;QACjD,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACnC,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACpB,IAAI,CAAC,YAAY,CAChB,OAAO,CAAC,MAA0E,EAClF,OAAO,CAAC,SAAS,CACjB,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACnC,CAAC;IAEO,aAAa,CAAC,OAA6B;QAClD,IAAI,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QACnD,IAAI,MAAM,EAAE,IAAI,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,KAAK,IAAI,CAAC,OAAO,EAAE,eAAe,EAAE,CAAC;YACnF,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;YACpC,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;YACxC,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;YAC1D,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;YAC9C,MAAM,GAAG,GAAG,IAAI,aAAa,CAAC,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC;YAClE,mBAAmB,CAAC,GAAG,EAAE;gBACxB,GAAG,CAAC,UAAU,EAAE,CAAC;gBACjB,GAAG,CAAC,aAAa,EAAE,CAAC;YACrB,CAAC,CAAC,CAAC;YACH,MAAM,OAAO,GAAG,OAAO,CAAC,OAA2C,CAAC;YACpE,MAAM,SAAS,GAAG,IAAI,sBAAsB,CAC3C,OAAO,EACP,OAAO,CAAC,eAAe,CAAC,kBAAkB,CAAC,OAAO,CAAC,UAAU,CAAC,EAC9D,SAAS,EACT,OAAO,CAAC,SAAS,CACjB,CAAC;YACF,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;YACxB,MAAM,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,eAAe,EAAE,CAAC;YACxF,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QAC/C,CAAC;aAAM,CAAC;YACP,MAAM,CAAC,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;QACtD,CAAC;QACD,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC/C,MAAM,CAAC,SAAS,CAAC,YAAY,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACjD,OAAO,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC/C,CAAC;IAEO,aAAa,CAAC,EAAU;QAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACpC,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACxB,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM;YAAE,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;QACvD,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;IACnB,CAAC;IAEO,IAAI,CAAC,OAAiC;QAC7C,IAAI,CAAC,KAAK,CAAC,iCAAiC,CAAC,OAAO,CAAC,CAAC,CAAC;IACxD,CAAC;CACD","sourcesContent":["import type { Terminal } from \"@earendil-works/pi-tui\";\nimport { type TUI, TuiMainScreen } from \"@earendil-works/pi-tui\";\nimport { getAgentDir } from \"../../config.js\";\nimport type { AgentSession } from \"../../core/agent-session.js\";\nimport { runCallback } from \"../../core/callback-activity.ts\";\nimport type { CustomMessage } from \"../../core/messages.ts\";\nimport { ensurePngTranscoder } from \"../../utils/image-convert.ts\";\nimport { CustomMessageComponent } from \"../interactive/components/custom-message.ts\";\nimport { ToolExecutionComponent } from \"../interactive/components/tool-execution.ts\";\nimport {\n\ttype InteractiveEngineCommand,\n\ttype InteractiveEngineMessage,\n\tparseInteractiveEngineCommand,\n\tserializeInteractiveEngineMessage,\n} from \"./protocol.ts\";\n\ntype ToolRenderCommand = Extract<InteractiveEngineCommand, { type: \"engine_tool_render\" }>;\ntype MessageRenderCommand = Extract<InteractiveEngineCommand, { type: \"engine_message_render\" }>;\n\n/**\n * Cached per-componentId render state. Records are REUSED across render\n * requests: recreating (and re-seeding) the component on every request made\n * the seeding setters call `ui.requestRender()` each time, whose eventual\n * terminal write emitted `engine_custom_invalidate` back to the host, which\n * re-sent the render request — a self-sustaining host⇄engine render\n * ping-pong that presented as full-screen TUI flicker (quit/pause while a\n * workflow stage streams). Reuse plus idempotent seeding setters lets the\n * loop converge after at most one round trip while still forwarding\n * legitimate async invalidations (e.g. image conversion). A record is rebuilt\n * once the session's extension runner changes, so rows rendered before\n * deferred resources loaded pick up the extensions' renderers.\n */\ntype RenderRecord = { runner: AgentSession[\"extensionRunner\"]; terminal: RenderTerminal; tui: TUI } & (\n\t| { kind: \"message\"; component: CustomMessageComponent }\n\t| { kind: \"tool\"; component: ToolExecutionComponent }\n);\n\nclass RenderTerminal implements Terminal {\n\tcolumns = 80;\n\trows = 24;\n\tkittyProtocolActive = false;\n\tprivate readonly requestRender: () => void;\n\tconstructor(requestRender: () => void) {\n\t\tthis.requestRender = requestRender;\n\t}\n\tstart(): void {}\n\tstop(): void {}\n\tasync drainInput(): Promise<void> {}\n\twrite(): void {\n\t\tthis.requestRender();\n\t}\n\tmoveBy(): void {}\n\thideCursor(): void {}\n\tshowCursor(): void {}\n\tclearLine(): void {}\n\tclearFromCursor(): void {}\n\tclearScreen(): void {}\n\tsetTitle(): void {}\n\tsetProgress(): void {}\n\tsetProgramStatus(): void {}\n}\n\n/**\n * How many disposed row ids the engine remembers. A render request can reach the engine after its row's disposal:\n * renders are coalesced behind the host's ordered writes, and a render's callback runs a turn after its line is read.\n * Without the tombstone that late render would rebuild the record the disposal released, for the rest of the session.\n * Row ids are never reused, and a render this many disposals late does not happen in practice.\n */\nexport const DISPOSED_RENDER_TOMBSTONE_LIMIT = 10_000;\n\nexport class EngineRenderService {\n\tprivate readonly records = new Map<string, RenderRecord>();\n\tprivate readonly disposedIds = new Set<string>();\n\tprivate session: AgentSession | undefined;\n\tprivate readonly write: (line: string) => void;\n\n\tconstructor(write: (line: string) => void) {\n\t\tthis.write = write;\n\t}\n\n\tbindSession(session: AgentSession): void {\n\t\tthis.dispose();\n\t\tthis.session = session;\n\t}\n\n\thandleLine(line: string): boolean {\n\t\tconst command = parseInteractiveEngineCommand(line);\n\t\tif (!command?.type.startsWith(\"engine_\") || command.type.startsWith(\"engine_custom_\")) return false;\n\t\tif (command.type === \"engine_render_dispose\") {\n\t\t\tthis.disposeRecord(command.componentId);\n\t\t\tthis.disposedIds.add(command.componentId);\n\t\t\tif (this.disposedIds.size > DISPOSED_RENDER_TOMBSTONE_LIMIT) {\n\t\t\t\tconst oldest = this.disposedIds.values().next().value;\n\t\t\t\tif (oldest !== undefined) this.disposedIds.delete(oldest);\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\tif (command.type !== \"engine_tool_render\" && command.type !== \"engine_message_render\") return false;\n\t\tconst name =\n\t\t\tcommand.type === \"engine_tool_render\"\n\t\t\t\t? `tool:${command.toolName}`\n\t\t\t\t: `message:${command.message.customType ?? \"custom\"}`;\n\t\tvoid runCallback({ kind: \"renderer\", name }, async () => {\n\t\t\t// The row may have been disposed before this render arrived, or while it waited for its turn: rendering it\n\t\t\t// now would rebuild the record its disposal released.\n\t\t\tif (this.disposedIds.has(command.componentId)) return undefined;\n\t\t\treturn command.type === \"engine_tool_render\" ? this.renderTool(command) : this.renderMessage(command);\n\t\t})\n\t\t\t.then((lines) => {\n\t\t\t\tif (lines === undefined) return;\n\t\t\t\tthis.send({\n\t\t\t\t\ttype: \"engine_custom_frame\",\n\t\t\t\t\tcomponentId: command.componentId,\n\t\t\t\t\trequestId: command.requestId,\n\t\t\t\t\tlines,\n\t\t\t\t});\n\t\t\t})\n\t\t\t.catch((error: Error) =>\n\t\t\t\tthis.send({\n\t\t\t\t\ttype: \"engine_custom_frame\",\n\t\t\t\t\tcomponentId: command.componentId,\n\t\t\t\t\trequestId: command.requestId,\n\t\t\t\t\tlines: [`Remote renderer failed: ${error.message}`],\n\t\t\t\t}),\n\t\t\t);\n\t\treturn true;\n\t}\n\n\tdispose(): void {\n\t\tfor (const id of [...this.records.keys()]) this.disposeRecord(id);\n\t}\n\n\tprivate boundSession(): AgentSession {\n\t\tconst session = this.session;\n\t\tif (!session) throw new Error(\"Renderer session is not bound\");\n\t\treturn session;\n\t}\n\n\tprivate createTerminal(componentId: string): RenderTerminal {\n\t\treturn new RenderTerminal(() => this.send({ type: \"engine_custom_invalidate\", componentId }));\n\t}\n\n\tprivate renderTool(command: ToolRenderCommand): string[] {\n\t\tlet record = this.records.get(command.componentId);\n\t\tif (record?.kind !== \"tool\" || record.runner !== this.session?.extensionRunner) {\n\t\t\tconst session = this.boundSession();\n\t\t\tthis.disposeRecord(command.componentId);\n\t\t\tconst terminal = this.createTerminal(command.componentId);\n\t\t\tterminal.columns = Math.max(1, command.width);\n\t\t\tconst tui = new TuiMainScreen(terminal, undefined, getAgentDir());\n\t\t\tensurePngTranscoder(() => {\n\t\t\t\ttui.invalidate();\n\t\t\t\ttui.requestRender();\n\t\t\t});\n\t\t\tconst component = new ToolExecutionComponent(\n\t\t\t\tcommand.toolName,\n\t\t\t\tcommand.toolCallId,\n\t\t\t\tcommand.args,\n\t\t\t\t{ showImages: command.showImages, imageWidthCells: command.imageWidthCells, outputPad: command.outputPad },\n\t\t\t\tsession.extensionRunner.resolveToolRenderers(command.toolName, () =>\n\t\t\t\t\tsession.getToolDefinition(command.toolName),\n\t\t\t\t),\n\t\t\t\ttui,\n\t\t\t\tsession.sessionManager.getCwd(),\n\t\t\t);\n\t\t\ttui.addChild(component);\n\t\t\trecord = { kind: \"tool\", component, terminal, tui, runner: session.extensionRunner };\n\t\t\tthis.records.set(command.componentId, record);\n\t\t} else {\n\t\t\trecord.terminal.columns = Math.max(1, command.width);\n\t\t\trecord.component.updateArgs(command.args);\n\t\t\trecord.component.setShowImages(command.showImages);\n\t\t\trecord.component.setImageWidthCells(command.imageWidthCells);\n\t\t\trecord.component.setOutputPad(command.outputPad ?? 1);\n\t\t}\n\t\tconst tool = record.component;\n\t\tif (command.executionStarted) tool.markExecutionStarted();\n\t\tif (command.argsComplete) tool.setArgsComplete();\n\t\ttool.setExpanded(command.expanded);\n\t\tif (command.result) {\n\t\t\ttool.updateResult(\n\t\t\t\tcommand.result as unknown as Parameters<ToolExecutionComponent[\"updateResult\"]>[0],\n\t\t\t\tcommand.isPartial,\n\t\t\t);\n\t\t}\n\t\treturn tool.render(command.width);\n\t}\n\n\tprivate renderMessage(command: MessageRenderCommand): string[] {\n\t\tlet record = this.records.get(command.componentId);\n\t\tif (record?.kind !== \"message\" || record.runner !== this.session?.extensionRunner) {\n\t\t\tconst session = this.boundSession();\n\t\t\tthis.disposeRecord(command.componentId);\n\t\t\tconst terminal = this.createTerminal(command.componentId);\n\t\t\tterminal.columns = Math.max(1, command.width);\n\t\t\tconst tui = new TuiMainScreen(terminal, undefined, getAgentDir());\n\t\t\tensurePngTranscoder(() => {\n\t\t\t\ttui.invalidate();\n\t\t\t\ttui.requestRender();\n\t\t\t});\n\t\t\tconst message = command.message as unknown as CustomMessage<object>;\n\t\t\tconst component = new CustomMessageComponent(\n\t\t\t\tmessage,\n\t\t\t\tsession.extensionRunner.getMessageRenderer(message.customType),\n\t\t\t\tundefined,\n\t\t\t\tcommand.outputPad,\n\t\t\t);\n\t\t\ttui.addChild(component);\n\t\t\trecord = { kind: \"message\", component, terminal, tui, runner: session.extensionRunner };\n\t\t\tthis.records.set(command.componentId, record);\n\t\t} else {\n\t\t\trecord.terminal.columns = Math.max(1, command.width);\n\t\t}\n\t\trecord.component.setExpanded(command.expanded);\n\t\trecord.component.setOutputPad(command.outputPad);\n\t\treturn record.component.render(command.width);\n\t}\n\n\tprivate disposeRecord(id: string): void {\n\t\tconst record = this.records.get(id);\n\t\tif (!record) return;\n\t\tthis.records.delete(id);\n\t\tif (record.kind === \"tool\") record.component.dispose();\n\t\trecord.tui.stop();\n\t}\n\n\tprivate send(message: InteractiveEngineMessage): void {\n\t\tthis.write(serializeInteractiveEngineMessage(message));\n\t}\n}\n"]}
@@ -24,6 +24,12 @@ declare abstract class RemoteRenderer implements Component {
24
24
  private disposed;
25
25
  private readonly frameClamp;
26
26
  private readonly routes;
27
+ /**
28
+ * What each unanswered request showed (its frame identity, expansion included), by request id. A frame is
29
+ * remembered under the identity of the request it answers, not under the row's state when it arrives: a row
30
+ * collapsed while its expanded frame was in flight must not have that frame remembered as its collapsed one.
31
+ */
32
+ private readonly requestIdentities;
27
33
  protected readonly runtime: IsolatedInteractiveRuntime;
28
34
  private readonly requestRender;
29
35
  constructor(runtime: IsolatedInteractiveRuntime, requestRender: () => void);
@@ -1 +1 @@
1
- {"version":3,"file":"remote-renderer.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive-engine/remote-renderer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"remote-renderer.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive-engine/remote-renderer.ts"],"names":[],"mappings":"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"}
@@ -50,6 +50,12 @@ class RemoteRenderer {
50
50
  disposed = false;
51
51
  frameClamp = new RemoteFrameWidthClamp();
52
52
  routes;
53
+ /**
54
+ * What each unanswered request showed (its frame identity, expansion included), by request id. A frame is
55
+ * remembered under the identity of the request it answers, not under the row's state when it arrives: a row
56
+ * collapsed while its expanded frame was in flight must not have that frame remembered as its collapsed one.
57
+ */
58
+ requestIdentities = new Map();
53
59
  runtime;
54
60
  requestRender;
55
61
  constructor(runtime, requestRender) {
@@ -65,7 +71,12 @@ class RemoteRenderer {
65
71
  return;
66
72
  this.appliedRequestId = message.requestId;
67
73
  this.lines = message.lines;
68
- rememberFrame(this.frameIdentity(), message.lines);
74
+ const identity = this.requestIdentities.get(message.requestId);
75
+ for (const requestId of this.requestIdentities.keys())
76
+ if (requestId <= message.requestId)
77
+ this.requestIdentities.delete(requestId);
78
+ if (identity !== undefined)
79
+ rememberFrame(identity, message.lines);
69
80
  this.requestRender();
70
81
  }
71
82
  else if (message.type === "engine_custom_invalidate") {
@@ -77,9 +88,11 @@ class RemoteRenderer {
77
88
  this.width = width;
78
89
  this.dirty = false;
79
90
  // A row rebuilt for a row the engine already rendered paints its last frame until the fresh one arrives.
91
+ const identity = this.frameIdentity();
80
92
  if (this.lines === LOADING_LINES)
81
- this.lines = recentFrames.get(this.frameIdentity()) ?? LOADING_LINES;
82
- this.runtime.sendEngineCommand(this.command(++this.requestId, width));
93
+ this.lines = recentFrames.get(identity) ?? LOADING_LINES;
94
+ this.requestIdentities.set(++this.requestId, identity);
95
+ this.runtime.sendEngineCommand(this.command(this.requestId, width));
83
96
  }
84
97
  // The engine child re-renders asynchronously; until the fresh frame
85
98
  // arrives, the previous frame may be wrapped for an older terminal
@@ -96,6 +109,7 @@ class RemoteRenderer {
96
109
  this.disposed = true;
97
110
  if (this.routes.get(this.componentId) === this)
98
111
  this.routes.delete(this.componentId);
112
+ this.requestIdentities.clear();
99
113
  this.runtime.sendEngineCommand({ type: "engine_render_dispose", componentId: this.componentId });
100
114
  }
101
115
  changed() {
@@ -1 +1 @@
1
- {"version":3,"file":"remote-renderer.js","sourceRoot":"","sources":["../../../src/modes/interactive-engine/remote-renderer.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { Component } from \"@earendil-works/pi-tui\";\nimport type { CustomMessage } from \"../../core/messages.ts\";\nimport type { ToolExecutionComponent } from \"../interactive/components/tool-execution.ts\";\nimport type { IsolatedInteractiveRuntime } from \"./isolated-runtime.js\";\nimport type { InteractiveEngineCommand, InteractiveEngineMessage, JsonObject, JsonValue } from \"./protocol.ts\";\nimport { RemoteFrameWidthClamp } from \"./remote-frame-clamp.ts\";\n\ntype RenderableToolResult = Parameters<ToolExecutionComponent[\"updateResult\"]>[0];\ntype RemoteRenderCommand = Extract<InteractiveEngineCommand, { type: \"engine_tool_render\" | \"engine_message_render\" }>;\n\nlet nextRemoteRendererId = 0;\n\nconst LOADING_LINES = [\"Loading isolated renderer…\"];\n\n/**\n * How many rows' last frames are kept, by row identity (a tool call, a custom message), to repaint rebuilt rows. A chat\n * rebuild (compaction, `/reload`, a switched session) recreates every row, and each new row waits for the engine to\n * render it. Showing the row's last frame meanwhile keeps the rebuilt chat painted, instead of a screen of placeholders\n * that lasts as long as the engine is busy.\n */\nexport const REMOTE_FRAME_CACHE_LIMIT = 2_000;\nconst recentFrames = new Map<string, string[]>();\n\nfunction rememberFrame(key: string, lines: string[]): void {\n\trecentFrames.delete(key);\n\trecentFrames.set(key, lines);\n\tif (recentFrames.size > REMOTE_FRAME_CACHE_LIMIT) {\n\t\tconst oldest = recentFrames.keys().next().value;\n\t\tif (oldest !== undefined) recentFrames.delete(oldest);\n\t}\n}\n\n/** One engine-message subscription per runtime, routed by component id, however many rows the chat holds. */\nconst routesByRuntime = new WeakMap<IsolatedInteractiveRuntime, Map<string, RemoteRenderer>>();\n\nfunction rendererRoutes(runtime: IsolatedInteractiveRuntime): Map<string, RemoteRenderer> {\n\tlet routes = routesByRuntime.get(runtime);\n\tif (routes === undefined) {\n\t\tconst created = new Map<string, RemoteRenderer>();\n\t\truntime.onEngineMessage((message) => {\n\t\t\tif (\"componentId\" in message) created.get(message.componentId)?.receive(message);\n\t\t});\n\t\troutesByRuntime.set(runtime, created);\n\t\troutes = created;\n\t}\n\treturn routes;\n}\n\nfunction jsonValue(value: unknown): JsonValue {\n\tif (value === undefined) return null;\n\tconst encoded = JSON.stringify(value);\n\treturn encoded === undefined ? null : (JSON.parse(encoded) as JsonValue);\n}\n\nabstract class RemoteRenderer implements Component {\n\tprotected readonly componentId = `remote_renderer_${++nextRemoteRendererId}`;\n\tprivate lines = LOADING_LINES;\n\tprivate width = 0;\n\tprivate requestId = 0;\n\tprivate appliedRequestId = 0;\n\tprivate dirty = true;\n\tprivate disposed = false;\n\tprivate readonly frameClamp = new RemoteFrameWidthClamp();\n\tprivate readonly routes: Map<string, RemoteRenderer>;\n\n\tprotected readonly runtime: IsolatedInteractiveRuntime;\n\tprivate readonly requestRender: () => void;\n\n\tconstructor(runtime: IsolatedInteractiveRuntime, requestRender: () => void) {\n\t\tthis.runtime = runtime;\n\t\tthis.requestRender = requestRender;\n\t\tthis.routes = rendererRoutes(runtime);\n\t\tthis.routes.set(this.componentId, this);\n\t}\n\n\t/** An engine message addressed to this row. */\n\treceive(message: InteractiveEngineMessage): void {\n\t\tif (message.type === \"engine_custom_frame\") {\n\t\t\tif (message.requestId < this.appliedRequestId) return;\n\t\t\tthis.appliedRequestId = message.requestId;\n\t\t\tthis.lines = message.lines;\n\t\t\trememberFrame(this.frameIdentity(), message.lines);\n\t\t\tthis.requestRender();\n\t\t} else if (message.type === \"engine_custom_invalidate\") {\n\t\t\tthis.changed();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tif (!this.disposed && (this.dirty || width !== this.width)) {\n\t\t\tthis.width = width;\n\t\t\tthis.dirty = false;\n\t\t\t// A row rebuilt for a row the engine already rendered paints its last frame until the fresh one arrives.\n\t\t\tif (this.lines === LOADING_LINES) this.lines = recentFrames.get(this.frameIdentity()) ?? LOADING_LINES;\n\t\t\tthis.runtime.sendEngineCommand(this.command(++this.requestId, width));\n\t\t}\n\t\t// The engine child re-renders asynchronously; until the fresh frame\n\t\t// arrives, the previous frame may be wrapped for an older terminal\n\t\t// width. Clamp so a resize never replays overflowing lines (pi-tui\n\t\t// crashes on any rendered line wider than the terminal).\n\t\treturn this.frameClamp.clamp(this.lines, width);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.markDirty();\n\t}\n\n\tdispose(): void {\n\t\tif (this.disposed) return;\n\t\tthis.disposed = true;\n\t\tif (this.routes.get(this.componentId) === this) this.routes.delete(this.componentId);\n\t\tthis.runtime.sendEngineCommand({ type: \"engine_render_dispose\", componentId: this.componentId });\n\t}\n\n\tprotected changed(): void {\n\t\tthis.markDirty();\n\t\tthis.requestRender();\n\t}\n\n\tprotected abstract command(requestId: number, width: number): RemoteRenderCommand;\n\n\t/** Names what this row shows, the same for every row recreated for it. */\n\tprotected abstract frameIdentity(): string;\n\n\tprivate markDirty(): void {\n\t\tthis.dirty = true;\n\t}\n}\n\n/**\n * Disposes the rows rendered by the engine among `components`, which a cleared chat no longer shows: each one\n * otherwise keeps its renderer alive in the engine for the rest of the session.\n */\nexport function disposeRemoteRenderers(components: readonly Component[]): void {\n\tfor (const component of components) if (component instanceof RemoteRenderer) component.dispose();\n}\n\nexport class RemoteToolExecutionComponent extends RemoteRenderer {\n\tprivate readonly toolName: string;\n\tprivate readonly toolCallId: string;\n\tprivate args: unknown;\n\tprivate result: RenderableToolResult | undefined;\n\tprivate executionStarted = false;\n\tprivate argsComplete = false;\n\tprivate isPartial = true;\n\tprivate expanded = false;\n\tprivate showImages: boolean;\n\tprivate imageWidthCells: number;\n\tprivate outputPad: number;\n\n\tconstructor(\n\t\ttoolName: string,\n\t\ttoolCallId: string,\n\t\targs: unknown,\n\t\toptions: { showImages?: boolean; imageWidthCells?: number; outputPad?: number },\n\t\truntime: IsolatedInteractiveRuntime,\n\t\trequestRender: () => void,\n\t) {\n\t\tsuper(runtime, requestRender);\n\t\tthis.toolName = toolName;\n\t\tthis.toolCallId = toolCallId;\n\t\tthis.args = args;\n\t\tthis.showImages = options.showImages ?? true;\n\t\tthis.imageWidthCells = options.imageWidthCells ?? 60;\n\t\tthis.outputPad = options.outputPad ?? 1;\n\t}\n\n\tsetOutputPad(outputPad: number): void {\n\t\tthis.outputPad = outputPad;\n\t\tthis.changed();\n\t}\n\tupdateArgs(args: unknown): void {\n\t\tthis.args = args;\n\t\tthis.changed();\n\t}\n\tmarkExecutionStarted(): void {\n\t\tthis.executionStarted = true;\n\t\tthis.changed();\n\t}\n\tsetArgsComplete(): void {\n\t\tthis.argsComplete = true;\n\t\tthis.changed();\n\t}\n\tupdateResult(result: RenderableToolResult, isPartial = false): void {\n\t\tthis.result = result;\n\t\tthis.isPartial = isPartial;\n\t\tthis.changed();\n\t}\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.changed();\n\t}\n\tsetShowImages(show: boolean): void {\n\t\tthis.showImages = show;\n\t\tthis.changed();\n\t}\n\tsetImageWidthCells(width: number): void {\n\t\tthis.imageWidthCells = Math.max(1, Math.floor(width));\n\t\tthis.changed();\n\t}\n\n\tprotected command(requestId: number, width: number): RemoteRenderCommand {\n\t\treturn {\n\t\t\ttype: \"engine_tool_render\",\n\t\t\tcomponentId: this.componentId,\n\t\t\trequestId,\n\t\t\twidth,\n\t\t\ttoolName: this.toolName,\n\t\t\ttoolCallId: this.toolCallId,\n\t\t\targs: jsonValue(this.args),\n\t\t\tresult: this.result ? (jsonValue(this.result) as JsonObject) : undefined,\n\t\t\texecutionStarted: this.executionStarted,\n\t\t\targsComplete: this.argsComplete,\n\t\t\tisPartial: this.isPartial,\n\t\t\texpanded: this.expanded,\n\t\t\tshowImages: this.showImages,\n\t\t\timageWidthCells: this.imageWidthCells,\n\t\t\toutputPad: this.outputPad,\n\t\t};\n\t}\n\n\tprotected frameIdentity(): string {\n\t\treturn JSON.stringify([\"tool\", this.toolName, this.toolCallId, this.expanded]);\n\t}\n}\n\nexport class RemoteCustomMessageComponent extends RemoteRenderer {\n\tprivate readonly message: CustomMessage<unknown>;\n\tprivate contentDigest: string | undefined;\n\tprivate expanded = false;\n\tprivate outputPad: number;\n\tconstructor(\n\t\tmessage: CustomMessage<unknown>,\n\t\truntime: IsolatedInteractiveRuntime,\n\t\trequestRender: () => void,\n\t\toutputPad = 1,\n\t) {\n\t\tsuper(runtime, requestRender);\n\t\tthis.message = message;\n\t\tthis.outputPad = outputPad;\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.changed();\n\t}\n\tsetOutputPad(outputPad: number): void {\n\t\tthis.outputPad = outputPad;\n\t\tthis.changed();\n\t}\n\n\tprotected command(requestId: number, width: number): RemoteRenderCommand {\n\t\treturn {\n\t\t\ttype: \"engine_message_render\",\n\t\t\tcomponentId: this.componentId,\n\t\t\trequestId,\n\t\t\twidth,\n\t\t\tmessage: jsonValue(this.message) as JsonObject,\n\t\t\texpanded: this.expanded,\n\t\t\toutputPad: this.outputPad,\n\t\t};\n\t}\n\n\tprotected frameIdentity(): string {\n\t\t// Not the timestamp: a message shown live and the same message restored from the session can differ in it.\n\t\tthis.contentDigest ??= createHash(\"sha1\")\n\t\t\t.update(JSON.stringify([this.message.content, this.message.details]) ?? \"\")\n\t\t\t.digest(\"base64\");\n\t\treturn JSON.stringify([\"message\", this.message.customType, this.contentDigest, this.expanded]);\n\t}\n}\n"]}
1
+ {"version":3,"file":"remote-renderer.js","sourceRoot":"","sources":["../../../src/modes/interactive-engine/remote-renderer.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { Component } from \"@earendil-works/pi-tui\";\nimport type { CustomMessage } from \"../../core/messages.ts\";\nimport type { ToolExecutionComponent } from \"../interactive/components/tool-execution.ts\";\nimport type { IsolatedInteractiveRuntime } from \"./isolated-runtime.js\";\nimport type { InteractiveEngineCommand, InteractiveEngineMessage, JsonObject, JsonValue } from \"./protocol.ts\";\nimport { RemoteFrameWidthClamp } from \"./remote-frame-clamp.ts\";\n\ntype RenderableToolResult = Parameters<ToolExecutionComponent[\"updateResult\"]>[0];\ntype RemoteRenderCommand = Extract<InteractiveEngineCommand, { type: \"engine_tool_render\" | \"engine_message_render\" }>;\n\nlet nextRemoteRendererId = 0;\n\nconst LOADING_LINES = [\"Loading isolated renderer…\"];\n\n/**\n * How many rows' last frames are kept, by row identity (a tool call, a custom message), to repaint rebuilt rows. A chat\n * rebuild (compaction, `/reload`, a switched session) recreates every row, and each new row waits for the engine to\n * render it. Showing the row's last frame meanwhile keeps the rebuilt chat painted, instead of a screen of placeholders\n * that lasts as long as the engine is busy.\n */\nexport const REMOTE_FRAME_CACHE_LIMIT = 2_000;\nconst recentFrames = new Map<string, string[]>();\n\nfunction rememberFrame(key: string, lines: string[]): void {\n\trecentFrames.delete(key);\n\trecentFrames.set(key, lines);\n\tif (recentFrames.size > REMOTE_FRAME_CACHE_LIMIT) {\n\t\tconst oldest = recentFrames.keys().next().value;\n\t\tif (oldest !== undefined) recentFrames.delete(oldest);\n\t}\n}\n\n/** One engine-message subscription per runtime, routed by component id, however many rows the chat holds. */\nconst routesByRuntime = new WeakMap<IsolatedInteractiveRuntime, Map<string, RemoteRenderer>>();\n\nfunction rendererRoutes(runtime: IsolatedInteractiveRuntime): Map<string, RemoteRenderer> {\n\tlet routes = routesByRuntime.get(runtime);\n\tif (routes === undefined) {\n\t\tconst created = new Map<string, RemoteRenderer>();\n\t\truntime.onEngineMessage((message) => {\n\t\t\tif (\"componentId\" in message) created.get(message.componentId)?.receive(message);\n\t\t});\n\t\troutesByRuntime.set(runtime, created);\n\t\troutes = created;\n\t}\n\treturn routes;\n}\n\nfunction jsonValue(value: unknown): JsonValue {\n\tif (value === undefined) return null;\n\tconst encoded = JSON.stringify(value);\n\treturn encoded === undefined ? null : (JSON.parse(encoded) as JsonValue);\n}\n\nabstract class RemoteRenderer implements Component {\n\tprotected readonly componentId = `remote_renderer_${++nextRemoteRendererId}`;\n\tprivate lines = LOADING_LINES;\n\tprivate width = 0;\n\tprivate requestId = 0;\n\tprivate appliedRequestId = 0;\n\tprivate dirty = true;\n\tprivate disposed = false;\n\tprivate readonly frameClamp = new RemoteFrameWidthClamp();\n\tprivate readonly routes: Map<string, RemoteRenderer>;\n\t/**\n\t * What each unanswered request showed (its frame identity, expansion included), by request id. A frame is\n\t * remembered under the identity of the request it answers, not under the row's state when it arrives: a row\n\t * collapsed while its expanded frame was in flight must not have that frame remembered as its collapsed one.\n\t */\n\tprivate readonly requestIdentities = new Map<number, string>();\n\n\tprotected readonly runtime: IsolatedInteractiveRuntime;\n\tprivate readonly requestRender: () => void;\n\n\tconstructor(runtime: IsolatedInteractiveRuntime, requestRender: () => void) {\n\t\tthis.runtime = runtime;\n\t\tthis.requestRender = requestRender;\n\t\tthis.routes = rendererRoutes(runtime);\n\t\tthis.routes.set(this.componentId, this);\n\t}\n\n\t/** An engine message addressed to this row. */\n\treceive(message: InteractiveEngineMessage): void {\n\t\tif (message.type === \"engine_custom_frame\") {\n\t\t\tif (message.requestId < this.appliedRequestId) return;\n\t\t\tthis.appliedRequestId = message.requestId;\n\t\t\tthis.lines = message.lines;\n\t\t\tconst identity = this.requestIdentities.get(message.requestId);\n\t\t\tfor (const requestId of this.requestIdentities.keys())\n\t\t\t\tif (requestId <= message.requestId) this.requestIdentities.delete(requestId);\n\t\t\tif (identity !== undefined) rememberFrame(identity, message.lines);\n\t\t\tthis.requestRender();\n\t\t} else if (message.type === \"engine_custom_invalidate\") {\n\t\t\tthis.changed();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tif (!this.disposed && (this.dirty || width !== this.width)) {\n\t\t\tthis.width = width;\n\t\t\tthis.dirty = false;\n\t\t\t// A row rebuilt for a row the engine already rendered paints its last frame until the fresh one arrives.\n\t\t\tconst identity = this.frameIdentity();\n\t\t\tif (this.lines === LOADING_LINES) this.lines = recentFrames.get(identity) ?? LOADING_LINES;\n\t\t\tthis.requestIdentities.set(++this.requestId, identity);\n\t\t\tthis.runtime.sendEngineCommand(this.command(this.requestId, width));\n\t\t}\n\t\t// The engine child re-renders asynchronously; until the fresh frame\n\t\t// arrives, the previous frame may be wrapped for an older terminal\n\t\t// width. Clamp so a resize never replays overflowing lines (pi-tui\n\t\t// crashes on any rendered line wider than the terminal).\n\t\treturn this.frameClamp.clamp(this.lines, width);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.markDirty();\n\t}\n\n\tdispose(): void {\n\t\tif (this.disposed) return;\n\t\tthis.disposed = true;\n\t\tif (this.routes.get(this.componentId) === this) this.routes.delete(this.componentId);\n\t\tthis.requestIdentities.clear();\n\t\tthis.runtime.sendEngineCommand({ type: \"engine_render_dispose\", componentId: this.componentId });\n\t}\n\n\tprotected changed(): void {\n\t\tthis.markDirty();\n\t\tthis.requestRender();\n\t}\n\n\tprotected abstract command(requestId: number, width: number): RemoteRenderCommand;\n\n\t/** Names what this row shows, the same for every row recreated for it. */\n\tprotected abstract frameIdentity(): string;\n\n\tprivate markDirty(): void {\n\t\tthis.dirty = true;\n\t}\n}\n\n/**\n * Disposes the rows rendered by the engine among `components`, which a cleared chat no longer shows: each one\n * otherwise keeps its renderer alive in the engine for the rest of the session.\n */\nexport function disposeRemoteRenderers(components: readonly Component[]): void {\n\tfor (const component of components) if (component instanceof RemoteRenderer) component.dispose();\n}\n\nexport class RemoteToolExecutionComponent extends RemoteRenderer {\n\tprivate readonly toolName: string;\n\tprivate readonly toolCallId: string;\n\tprivate args: unknown;\n\tprivate result: RenderableToolResult | undefined;\n\tprivate executionStarted = false;\n\tprivate argsComplete = false;\n\tprivate isPartial = true;\n\tprivate expanded = false;\n\tprivate showImages: boolean;\n\tprivate imageWidthCells: number;\n\tprivate outputPad: number;\n\n\tconstructor(\n\t\ttoolName: string,\n\t\ttoolCallId: string,\n\t\targs: unknown,\n\t\toptions: { showImages?: boolean; imageWidthCells?: number; outputPad?: number },\n\t\truntime: IsolatedInteractiveRuntime,\n\t\trequestRender: () => void,\n\t) {\n\t\tsuper(runtime, requestRender);\n\t\tthis.toolName = toolName;\n\t\tthis.toolCallId = toolCallId;\n\t\tthis.args = args;\n\t\tthis.showImages = options.showImages ?? true;\n\t\tthis.imageWidthCells = options.imageWidthCells ?? 60;\n\t\tthis.outputPad = options.outputPad ?? 1;\n\t}\n\n\tsetOutputPad(outputPad: number): void {\n\t\tthis.outputPad = outputPad;\n\t\tthis.changed();\n\t}\n\tupdateArgs(args: unknown): void {\n\t\tthis.args = args;\n\t\tthis.changed();\n\t}\n\tmarkExecutionStarted(): void {\n\t\tthis.executionStarted = true;\n\t\tthis.changed();\n\t}\n\tsetArgsComplete(): void {\n\t\tthis.argsComplete = true;\n\t\tthis.changed();\n\t}\n\tupdateResult(result: RenderableToolResult, isPartial = false): void {\n\t\tthis.result = result;\n\t\tthis.isPartial = isPartial;\n\t\tthis.changed();\n\t}\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.changed();\n\t}\n\tsetShowImages(show: boolean): void {\n\t\tthis.showImages = show;\n\t\tthis.changed();\n\t}\n\tsetImageWidthCells(width: number): void {\n\t\tthis.imageWidthCells = Math.max(1, Math.floor(width));\n\t\tthis.changed();\n\t}\n\n\tprotected command(requestId: number, width: number): RemoteRenderCommand {\n\t\treturn {\n\t\t\ttype: \"engine_tool_render\",\n\t\t\tcomponentId: this.componentId,\n\t\t\trequestId,\n\t\t\twidth,\n\t\t\ttoolName: this.toolName,\n\t\t\ttoolCallId: this.toolCallId,\n\t\t\targs: jsonValue(this.args),\n\t\t\tresult: this.result ? (jsonValue(this.result) as JsonObject) : undefined,\n\t\t\texecutionStarted: this.executionStarted,\n\t\t\targsComplete: this.argsComplete,\n\t\t\tisPartial: this.isPartial,\n\t\t\texpanded: this.expanded,\n\t\t\tshowImages: this.showImages,\n\t\t\timageWidthCells: this.imageWidthCells,\n\t\t\toutputPad: this.outputPad,\n\t\t};\n\t}\n\n\tprotected frameIdentity(): string {\n\t\treturn JSON.stringify([\"tool\", this.toolName, this.toolCallId, this.expanded]);\n\t}\n}\n\nexport class RemoteCustomMessageComponent extends RemoteRenderer {\n\tprivate readonly message: CustomMessage<unknown>;\n\tprivate contentDigest: string | undefined;\n\tprivate expanded = false;\n\tprivate outputPad: number;\n\tconstructor(\n\t\tmessage: CustomMessage<unknown>,\n\t\truntime: IsolatedInteractiveRuntime,\n\t\trequestRender: () => void,\n\t\toutputPad = 1,\n\t) {\n\t\tsuper(runtime, requestRender);\n\t\tthis.message = message;\n\t\tthis.outputPad = outputPad;\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.changed();\n\t}\n\tsetOutputPad(outputPad: number): void {\n\t\tthis.outputPad = outputPad;\n\t\tthis.changed();\n\t}\n\n\tprotected command(requestId: number, width: number): RemoteRenderCommand {\n\t\treturn {\n\t\t\ttype: \"engine_message_render\",\n\t\t\tcomponentId: this.componentId,\n\t\t\trequestId,\n\t\t\twidth,\n\t\t\tmessage: jsonValue(this.message) as JsonObject,\n\t\t\texpanded: this.expanded,\n\t\t\toutputPad: this.outputPad,\n\t\t};\n\t}\n\n\tprotected frameIdentity(): string {\n\t\t// Not the timestamp: a message shown live and the same message restored from the session can differ in it.\n\t\tthis.contentDigest ??= createHash(\"sha1\")\n\t\t\t.update(JSON.stringify([this.message.content, this.message.details]) ?? \"\")\n\t\t\t.digest(\"base64\");\n\t\treturn JSON.stringify([\"message\", this.message.customType, this.contentDigest, this.expanded]);\n\t}\n}\n"]}
@@ -18,6 +18,8 @@ export declare class QueuedWriter {
18
18
  get pendingBytes(): number;
19
19
  write(text: string): Promise<void>;
20
20
  offerLatest(key: string, text: string): boolean;
21
+ /** Drops the frame `offerLatest` holds under `key`, if it has not started writing. */
22
+ discardLatest(key: string): void;
21
23
  /**
22
24
  * Reject the in-flight frame and everything queued behind it.
23
25
  *
@@ -1 +1 @@
1
- {"version":3,"file":"queued-writer.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/queued-writer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAS5C,oEAAoE;AACpE,qBAAa,YAAY;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsB;IAC/C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmC;IAC7D;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAAoB;IAEvC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAElC,YAAY,MAAM,EAAE,QAAQ,EAE3B;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAEK,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQvC;IAED,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAS9C;IAED;;;;;;OAMG;IACH,KAAK,CAAC,KAAK,GAAE,KAA8C,GAAG,IAAI,CAWjE;IAED,OAAO,CAAC,IAAI;IASZ,OAAO,CAAC,IAAI;YAME,OAAO;CAyBrB"}
1
+ {"version":3,"file":"queued-writer.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/queued-writer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAS5C,oEAAoE;AACpE,qBAAa,YAAY;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsB;IAC/C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmC;IAC7D;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAAoB;IAEvC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAElC,YAAY,MAAM,EAAE,QAAQ,EAE3B;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAEK,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQvC;IAED,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAS9C;IAED,sFAAsF;IACtF,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAK/B;IAED;;;;;;OAMG;IACH,KAAK,CAAC,KAAK,GAAE,KAA8C,GAAG,IAAI,CAWjE;IAED,OAAO,CAAC,IAAI;IASZ,OAAO,CAAC,IAAI;YAME,OAAO;CAyBrB"}
@@ -42,6 +42,14 @@ export class QueuedWriter {
42
42
  this.pump();
43
43
  return true;
44
44
  }
45
+ /** Drops the frame `offerLatest` holds under `key`, if it has not started writing. */
46
+ discardLatest(key) {
47
+ const pending = this.coalesced.get(key);
48
+ if (!pending)
49
+ return;
50
+ this.coalesced.delete(key);
51
+ this.queuedBytes -= pending.bytes.length;
52
+ }
45
53
  /**
46
54
  * Reject the in-flight frame and everything queued behind it.
47
55
  *