@markusylisiurunen/tau 0.3.21 → 0.3.22

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 (58) hide show
  1. package/dist/core/auth/auth_storage.js +249 -23
  2. package/dist/core/auth/auth_storage.js.map +1 -1
  3. package/dist/core/auth/cli.js +66 -0
  4. package/dist/core/auth/cli.js.map +1 -1
  5. package/dist/core/auth/credential_store.js +42 -10
  6. package/dist/core/auth/credential_store.js.map +1 -1
  7. package/dist/core/auth/index.js +1 -1
  8. package/dist/core/auth/index.js.map +1 -1
  9. package/dist/core/auth/providers/openai_codex.js +46 -22
  10. package/dist/core/auth/providers/openai_codex.js.map +1 -1
  11. package/dist/core/events/types.js +1 -7
  12. package/dist/core/events/types.js.map +1 -1
  13. package/dist/core/index.js +1 -2
  14. package/dist/core/index.js.map +1 -1
  15. package/dist/core/models/catalog.js +20 -24
  16. package/dist/core/models/catalog.js.map +1 -1
  17. package/dist/core/session/compaction.js +0 -4
  18. package/dist/core/session/compaction.js.map +1 -1
  19. package/dist/core/session/core_session.js +3 -0
  20. package/dist/core/session/core_session.js.map +1 -1
  21. package/dist/core/session/pruning.js +8 -17
  22. package/dist/core/session/pruning.js.map +1 -1
  23. package/dist/core/session/runner.js +12 -2
  24. package/dist/core/session/runner.js.map +1 -1
  25. package/dist/core/session/session_engine.js +62 -38
  26. package/dist/core/session/session_engine.js.map +1 -1
  27. package/dist/core/subagents/control_plane.js +8 -9
  28. package/dist/core/subagents/control_plane.js.map +1 -1
  29. package/dist/core/version.js +1 -1
  30. package/dist/execution/cloudflare_sandbox_execution_environment.js +54 -6
  31. package/dist/execution/cloudflare_sandbox_execution_environment.js.map +1 -1
  32. package/dist/host/hosted_ephemeral_agent_session.js +19 -14
  33. package/dist/host/hosted_ephemeral_agent_session.js.map +1 -1
  34. package/dist/host/local_session_host.js +269 -97
  35. package/dist/host/local_session_host.js.map +1 -1
  36. package/dist/host/session_host.js +2 -1
  37. package/dist/host/session_host.js.map +1 -1
  38. package/dist/host/session_protocol_handler.js +34 -5
  39. package/dist/host/session_protocol_handler.js.map +1 -1
  40. package/dist/main.js +17 -15
  41. package/dist/main.js.map +1 -1
  42. package/dist/protocol/session_protocol.d.ts +0 -1
  43. package/dist/protocol/session_protocol.js +103 -5
  44. package/dist/protocol/session_protocol.js.map +1 -1
  45. package/dist/store/file_session_store.js +223 -41
  46. package/dist/store/file_session_store.js.map +1 -1
  47. package/dist/transport/stdio_session_transport.d.ts +2 -0
  48. package/dist/transport/stdio_session_transport.js +30 -3
  49. package/dist/transport/stdio_session_transport.js.map +1 -1
  50. package/dist/tui/chat_controller/diff_review_service.js +8 -5
  51. package/dist/tui/chat_controller/diff_review_service.js.map +1 -1
  52. package/dist/tui/session_chat_controller.js +113 -46
  53. package/dist/tui/session_chat_controller.js.map +1 -1
  54. package/package.json +3 -3
  55. package/dist/core/events/index.js +0 -3
  56. package/dist/core/events/index.js.map +0 -1
  57. package/dist/core/events/parser.js +0 -334
  58. package/dist/core/events/parser.js.map +0 -1
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { isDeepStrictEqual } from "node:util";
2
3
  import { resolvePromptTemplateWithBackend } from "../core/config/index.js";
3
4
  import { ChatRuntime } from "../core/runtime/chat_runtime.js";
4
5
  import { filterProjectPathAutocompleteEntries, loadProjectPathAutocompleteEntriesWithBackend, } from "../core/utils/project_files.js";
@@ -55,15 +56,27 @@ export class LocalSessionHost {
55
56
  return hostedSession;
56
57
  }
57
58
  async createRecoveredSession(snapshot, executionEnvironment) {
58
- const recovered = normalizeRecoveredSnapshot(snapshot);
59
- const hostedSession = await this.createLocalSessionHandle(executionEnvironment, recovered.snapshot, undefined, recovered.changed);
60
- hostedSession.session.restoreState({
61
- sessionId: recovered.snapshot.sessionId,
62
- historyEntries: recovered.snapshot.messages.flatMap((entry) => entry.modelVisible && isCoreMessage(entry.message)
63
- ? [{ id: entry.id, message: entry.message }]
64
- : []),
65
- });
66
- return hostedSession;
59
+ let hostedSession;
60
+ try {
61
+ const recovered = normalizeRecoveredSnapshot(snapshot);
62
+ hostedSession = await this.createLocalSessionHandle(executionEnvironment, recovered.snapshot, undefined, recovered.changed);
63
+ hostedSession.session.restoreState({
64
+ sessionId: recovered.snapshot.sessionId,
65
+ historyEntries: recovered.snapshot.messages.flatMap((entry) => entry.modelVisible && isCoreMessage(entry.message)
66
+ ? [{ id: entry.id, message: entry.message }]
67
+ : []),
68
+ });
69
+ return hostedSession;
70
+ }
71
+ catch (error) {
72
+ if (hostedSession) {
73
+ await hostedSession.dispose();
74
+ }
75
+ else {
76
+ await executionEnvironment.dispose();
77
+ }
78
+ throw error;
79
+ }
67
80
  }
68
81
  async createLocalSessionHandle(executionEnvironment, committedSnapshot, createParams, forceNextSnapshotRevision = false) {
69
82
  const bootstrap = await this.resolveNewSessionBootstrap(executionEnvironment);
@@ -235,6 +248,16 @@ export class LocalSessionHost {
235
248
  errors.push(result.reason);
236
249
  }
237
250
  }
251
+ for (const session of this.sessions) {
252
+ session.interruptTurn();
253
+ session.interruptMaintenance();
254
+ }
255
+ const settlementResults = await Promise.allSettled([...this.sessions].map((session) => session.waitForActiveWork()));
256
+ for (const result of settlementResults) {
257
+ if (result.status === "rejected") {
258
+ errors.push(result.reason);
259
+ }
260
+ }
238
261
  for (const session of this.sessions) {
239
262
  try {
240
263
  await session.snapshot();
@@ -285,7 +308,7 @@ class LocalHostedSessionHandle {
285
308
  session;
286
309
  committedSessionId;
287
310
  committedSnapshot;
288
- persistedSnapshotRevision;
311
+ persistedSnapshot;
289
312
  draftAssistantMessage;
290
313
  messageStates = new Map();
291
314
  restoredMessageIds;
@@ -293,6 +316,7 @@ class LocalHostedSessionHandle {
293
316
  timelineExtras = [];
294
317
  tools = new Map();
295
318
  agents = new Map();
319
+ agentCostTotals = new Map();
296
320
  facets = new Map();
297
321
  deltaListeners = new Set();
298
322
  ephemeralListeners = new Set();
@@ -302,6 +326,12 @@ class LocalHostedSessionHandle {
302
326
  unsubscribeSubagentEvent;
303
327
  runtimeEventQueue = Promise.resolve();
304
328
  snapshotQueue = Promise.resolve();
329
+ snapshotGeneration = 0;
330
+ maintenanceAbortControllers = new Set();
331
+ maintenancePromises = new Set();
332
+ activeTurnPromise;
333
+ disposePromise;
334
+ disposing = false;
305
335
  costTotal = 0;
306
336
  forceNextSnapshotRevision;
307
337
  disposed = false;
@@ -319,7 +349,9 @@ class LocalHostedSessionHandle {
319
349
  this.committedSnapshot = committedSnapshot
320
350
  ? cloneSessionProtocolSnapshot(committedSnapshot)
321
351
  : undefined;
322
- this.persistedSnapshotRevision = committedSnapshot?.revision;
352
+ this.persistedSnapshot = committedSnapshot
353
+ ? cloneSessionProtocolSnapshot(committedSnapshot)
354
+ : undefined;
323
355
  this.forceNextSnapshotRevision = forceNextSnapshotRevision;
324
356
  this.restoreProtocolState(committedSnapshot);
325
357
  this.unsubscribeSubagentEvent = this.session.onSubagentEvent((event) => {
@@ -362,6 +394,18 @@ class LocalHostedSessionHandle {
362
394
  }
363
395
  async runTurn() {
364
396
  this.assertActive();
397
+ const run = this.runTurnNow();
398
+ this.activeTurnPromise = run;
399
+ try {
400
+ return await run;
401
+ }
402
+ finally {
403
+ if (this.activeTurnPromise === run) {
404
+ this.activeTurnPromise = undefined;
405
+ }
406
+ }
407
+ }
408
+ async runTurnNow() {
365
409
  try {
366
410
  const result = await this.runtime.runTurn({
367
411
  onEvent: (event) => this.recordTurnRuntimeEvent(event),
@@ -386,6 +430,23 @@ class LocalHostedSessionHandle {
386
430
  this.assertActive();
387
431
  return this.runtime.interruptTurn();
388
432
  }
433
+ interruptMaintenance() {
434
+ let interrupted = false;
435
+ for (const abortController of this.maintenanceAbortControllers) {
436
+ if (!abortController.signal.aborted) {
437
+ abortController.abort();
438
+ interrupted = true;
439
+ }
440
+ }
441
+ return interrupted;
442
+ }
443
+ async waitForActiveWork() {
444
+ await Promise.allSettled([
445
+ ...(this.activeTurnPromise ? [this.activeTurnPromise] : []),
446
+ ...this.maintenancePromises,
447
+ ]);
448
+ await this.runtimeEventQueue;
449
+ }
389
450
  requestTurnBoundaryStop() {
390
451
  this.assertActive();
391
452
  return this.runtime.requestTurnBoundaryStop();
@@ -415,7 +476,7 @@ class LocalHostedSessionHandle {
415
476
  else if (snapshot.revision !== fromRevision) {
416
477
  this.emitDelta(createSessionProtocolDeltaMessage({
417
478
  sessionId: snapshot.sessionId,
418
- fromRevision,
479
+ fromRevision: snapshot.revision - 1,
419
480
  toRevision: snapshot.revision,
420
481
  reason: "configuration",
421
482
  delta: {
@@ -547,37 +608,60 @@ class LocalHostedSessionHandle {
547
608
  return await this.pathAutocompleteLoad;
548
609
  }
549
610
  async compact(options) {
550
- this.assertActive();
551
- const result = await this.session.compact({
552
- mode: options.mode === "summary-only" ? "only-summary" : "with-last-assistant",
553
- ...(options.guidance !== undefined ? { guidance: options.guidance } : {}),
611
+ return await this.runMaintenance(async (signal) => {
612
+ const result = await this.session.compact({
613
+ mode: options.mode === "summary-only" ? "only-summary" : "with-last-assistant",
614
+ ...(options.guidance !== undefined ? { guidance: options.guidance } : {}),
615
+ signal,
616
+ });
617
+ signal.throwIfAborted();
618
+ await this.runtimeEventQueue;
619
+ this.reconcileProjections();
620
+ const snapshot = await this.commitSnapshot();
621
+ this.emitSnapshotReset("maintenance", snapshot);
622
+ return {
623
+ snapshot,
624
+ compactionMessage: result.compactionMessage,
625
+ includedLastAssistant: result.includedLastAssistant,
626
+ };
554
627
  });
555
- const snapshot = await this.commitSnapshot();
556
- this.emitSnapshotReset("maintenance", snapshot);
557
- return {
558
- snapshot,
559
- compactionMessage: result.compactionMessage,
560
- includedLastAssistant: result.includedLastAssistant,
561
- };
562
628
  }
563
629
  async pruneToolResults(options) {
564
- this.assertActive();
565
- const result = await this.session.pruneToolResults({
566
- strategy: options.strategy,
567
- fraction: options.fraction,
568
- ...(options.guidance !== undefined ? { guidance: options.guidance } : {}),
630
+ return await this.runMaintenance(async (signal) => {
631
+ const result = await this.session.pruneToolResults({
632
+ strategy: options.strategy,
633
+ fraction: options.fraction,
634
+ ...(options.guidance !== undefined ? { guidance: options.guidance } : {}),
635
+ signal,
636
+ });
637
+ signal.throwIfAborted();
638
+ this.reconcileProjections({ prunedToolResults: result.prunedToolResults });
639
+ const snapshot = await this.commitSnapshot();
640
+ this.emitSnapshotReset("maintenance", snapshot);
641
+ return {
642
+ snapshot,
643
+ message: result.message,
644
+ noop: result.noop,
645
+ bashResultsPruned: result.bashResultsPruned,
646
+ editCallsPruned: result.editCallsPruned,
647
+ editResultsPruned: result.editResultsPruned,
648
+ bytesPruned: result.bytesPruned,
649
+ };
569
650
  });
570
- const snapshot = await this.commitSnapshot();
571
- this.emitSnapshotReset("maintenance", snapshot);
572
- return {
573
- snapshot,
574
- message: result.message,
575
- noop: result.noop,
576
- bashResultsPruned: result.bashResultsPruned,
577
- editCallsPruned: result.editCallsPruned,
578
- editResultsPruned: result.editResultsPruned,
579
- bytesPruned: result.bytesPruned,
580
- };
651
+ }
652
+ async runMaintenance(operation) {
653
+ this.assertActive();
654
+ const abortController = new AbortController();
655
+ this.maintenanceAbortControllers.add(abortController);
656
+ const promise = operation(abortController.signal);
657
+ this.maintenancePromises.add(promise);
658
+ try {
659
+ return await promise;
660
+ }
661
+ finally {
662
+ this.maintenanceAbortControllers.delete(abortController);
663
+ this.maintenancePromises.delete(promise);
664
+ }
581
665
  }
582
666
  async rewindToHistoryEntryId(historyEntryId) {
583
667
  this.assertActive();
@@ -585,6 +669,8 @@ class LocalHostedSessionHandle {
585
669
  if (!result) {
586
670
  throw new Error("rewind failed");
587
671
  }
672
+ await this.runtimeEventQueue;
673
+ this.reconcileProjections({ removeMissingAgents: true });
588
674
  const snapshot = await this.commitSnapshot();
589
675
  this.emitSnapshotReset("maintenance", snapshot);
590
676
  return {
@@ -646,95 +732,172 @@ class LocalHostedSessionHandle {
646
732
  return await this.commitSnapshot();
647
733
  }
648
734
  async dispose() {
735
+ if (!this.disposePromise) {
736
+ this.disposing = true;
737
+ this.disposePromise = this.disposeNow();
738
+ }
739
+ return await this.disposePromise;
740
+ }
741
+ async disposeNow() {
649
742
  if (this.disposed) {
650
743
  return;
651
744
  }
745
+ const errors = [];
746
+ this.runtime.interruptTurn();
747
+ this.interruptMaintenance();
748
+ try {
749
+ await this.waitForActiveWork();
750
+ }
751
+ catch (error) {
752
+ errors.push(error);
753
+ }
652
754
  this.disposed = true;
653
755
  try {
654
756
  this.unsubscribeSubagentEvent();
655
- for (const session of this.ephemeralAgentSessions.values()) {
757
+ }
758
+ catch (error) {
759
+ errors.push(error);
760
+ }
761
+ for (const session of this.ephemeralAgentSessions.values()) {
762
+ try {
656
763
  session.dispose();
657
764
  }
658
- this.ephemeralAgentSessions.clear();
765
+ catch (error) {
766
+ errors.push(error);
767
+ }
768
+ }
769
+ this.ephemeralAgentSessions.clear();
770
+ try {
659
771
  this.session.dispose();
772
+ }
773
+ catch (error) {
774
+ errors.push(error);
775
+ }
776
+ try {
660
777
  await this.executionEnvironment.dispose();
661
778
  }
779
+ catch (error) {
780
+ errors.push(error);
781
+ }
662
782
  finally {
663
783
  this.removeFromHost(this);
664
784
  }
785
+ if (errors.length > 0) {
786
+ throw new AggregateError(errors, "failed to dispose hosted session");
787
+ }
665
788
  }
666
789
  async commitSnapshot() {
667
790
  const write = this.snapshotQueue.catch(() => undefined).then(() => this.writeSnapshot());
668
791
  this.snapshotQueue = write.catch(() => undefined);
669
792
  return await write;
670
793
  }
671
- async commitSnapshotWithRevision(revision, options = {}) {
794
+ async commitSnapshotWithRevision(revision) {
672
795
  const write = this.snapshotQueue
673
796
  .catch(() => undefined)
674
- .then(() => this.writeSnapshotWithRevision(revision, options));
797
+ .then(() => this.writeSnapshotWithRevision(revision));
675
798
  this.snapshotQueue = write.catch(() => undefined);
676
799
  return await write;
677
800
  }
678
801
  async writeSnapshot() {
679
- this.assertActive();
802
+ this.assertNotDisposed();
803
+ const generation = this.snapshotGeneration;
680
804
  const draft = this.buildSnapshotDraft();
681
- if (this.committedSessionId !== draft.sessionId) {
682
- await this.store.deleteSession(this.committedSessionId, {
683
- ...(this.persistedSnapshotRevision !== undefined
684
- ? { expectedRevision: this.persistedSnapshotRevision }
685
- : {}),
686
- });
687
- this.committedSessionId = draft.sessionId;
688
- this.committedSnapshot = undefined;
689
- this.persistedSnapshotRevision = undefined;
690
- }
805
+ await this.switchSnapshotSession(draft.sessionId);
691
806
  const snapshot = {
692
807
  ...draft,
693
808
  revision: this.nextSnapshotRevision(draft),
694
809
  };
695
- if (this.committedSnapshot &&
696
- snapshot.revision === this.committedSnapshot.revision &&
697
- this.persistedSnapshotRevision === snapshot.revision) {
698
- return cloneSessionProtocolSnapshot(this.committedSnapshot);
810
+ if (this.persistedSnapshot && isDeepStrictEqual(this.persistedSnapshot, snapshot)) {
811
+ return cloneSessionProtocolSnapshot(this.committedSnapshot ?? snapshot);
699
812
  }
700
813
  await this.store.commitSessionSnapshot(snapshot, {
701
- expectedRevision: this.persistedSnapshotRevision ?? 0,
814
+ expectedRevision: this.persistedSnapshot?.revision ?? 0,
702
815
  });
703
- this.persistedSnapshotRevision = snapshot.revision;
816
+ this.persistedSnapshot = cloneSessionProtocolSnapshot(snapshot);
817
+ if (generation !== this.snapshotGeneration) {
818
+ return await this.writeSnapshot();
819
+ }
704
820
  return cloneSessionProtocolSnapshot(this.updateCommittedSnapshotAfterWrite(snapshot));
705
821
  }
706
- async writeSnapshotWithRevision(revision, options = {}) {
707
- this.assertActive();
822
+ async writeSnapshotWithRevision(revision) {
823
+ this.assertNotDisposed();
824
+ const generation = this.snapshotGeneration;
708
825
  const draft = this.buildSnapshotDraft();
709
- const persist = options.persist ?? true;
710
- if (this.committedSessionId !== draft.sessionId) {
711
- await this.store.deleteSession(this.committedSessionId, {
712
- ...(this.persistedSnapshotRevision !== undefined
713
- ? { expectedRevision: this.persistedSnapshotRevision }
714
- : {}),
715
- });
716
- this.committedSessionId = draft.sessionId;
717
- this.committedSnapshot = undefined;
718
- this.persistedSnapshotRevision = undefined;
719
- }
826
+ await this.switchSnapshotSession(draft.sessionId);
720
827
  const snapshot = {
721
828
  ...draft,
722
829
  revision,
723
830
  };
724
- if (persist) {
725
- await this.store.commitSessionSnapshot(snapshot, {
726
- expectedRevision: this.persistedSnapshotRevision ?? 0,
727
- });
728
- this.persistedSnapshotRevision = snapshot.revision;
831
+ await this.store.commitSessionSnapshot(snapshot, {
832
+ expectedRevision: this.persistedSnapshot?.revision ?? 0,
833
+ });
834
+ this.persistedSnapshot = cloneSessionProtocolSnapshot(snapshot);
835
+ if (generation !== this.snapshotGeneration) {
836
+ return await this.writeSnapshot();
729
837
  }
730
838
  return cloneSessionProtocolSnapshot(this.updateCommittedSnapshotAfterWrite(snapshot));
731
839
  }
840
+ async switchSnapshotSession(sessionId) {
841
+ if (this.committedSessionId === sessionId) {
842
+ return;
843
+ }
844
+ await this.store.deleteSession(this.committedSessionId, {
845
+ ...(this.persistedSnapshot ? { expectedRevision: this.persistedSnapshot.revision } : {}),
846
+ });
847
+ this.committedSessionId = sessionId;
848
+ this.committedSnapshot = undefined;
849
+ this.persistedSnapshot = undefined;
850
+ }
732
851
  updateCommittedSnapshotAfterWrite(snapshot) {
733
- if (!this.committedSnapshot || this.committedSnapshot.revision <= snapshot.revision) {
852
+ if (!this.committedSnapshot || this.committedSnapshot.revision < snapshot.revision) {
734
853
  this.committedSnapshot = cloneSessionProtocolSnapshot(snapshot);
854
+ this.snapshotGeneration += 1;
735
855
  }
736
856
  return this.committedSnapshot;
737
857
  }
858
+ reconcileProjections(options = {}) {
859
+ const messageIds = new Set(this.session.rawHistoryEntries.map((entry) => entry.id));
860
+ messageIds.add("system");
861
+ for (const [id, tool] of this.tools) {
862
+ if (!messageIds.has(tool.call.messageId) ||
863
+ (tool.resultMessageId !== undefined && !messageIds.has(tool.resultMessageId))) {
864
+ this.tools.delete(id);
865
+ }
866
+ }
867
+ if (options.removeMissingAgents) {
868
+ for (const id of this.agents.keys()) {
869
+ if (!this.session.hasSubagent(id)) {
870
+ this.agents.delete(id);
871
+ this.agentCostTotals.delete(id);
872
+ }
873
+ }
874
+ }
875
+ const operationIds = new Set(this.timelineExtras.filter((item) => item.type === "operation").map((item) => item.id));
876
+ const prunedByToolId = new Map((options.prunedToolResults ?? []).map((result) => [result.toolCallId, result.content]));
877
+ for (const [id, facet] of this.facets) {
878
+ const subjectExists = facet.subject.type === "session" ||
879
+ (facet.subject.type === "message" && messageIds.has(facet.subject.id)) ||
880
+ (facet.subject.type === "tool" && this.tools.has(facet.subject.id)) ||
881
+ (facet.subject.type === "agent" && this.agents.has(facet.subject.id)) ||
882
+ (facet.subject.type === "operation" && operationIds.has(facet.subject.id));
883
+ if (!subjectExists) {
884
+ this.facets.delete(id);
885
+ continue;
886
+ }
887
+ if (facet.subject.type === "tool") {
888
+ const toolCallId = facet.subject.id;
889
+ const content = prunedByToolId.get(toolCallId);
890
+ if (content !== undefined && facet.kind === "tau.tool-ui-events") {
891
+ this.facets.set(id, {
892
+ ...facet,
893
+ data: {
894
+ events: [{ type: "tool_pruned", toolCallId, content }],
895
+ },
896
+ });
897
+ }
898
+ }
899
+ }
900
+ }
738
901
  buildSnapshotDraft() {
739
902
  const messages = this.buildProtocolMessages();
740
903
  return {
@@ -832,8 +995,10 @@ class LocalHostedSessionHandle {
832
995
  this.tools.set(id, structuredClone(tool));
833
996
  }
834
997
  this.agents.clear();
998
+ this.agentCostTotals.clear();
835
999
  for (const [id, agent] of Object.entries(snapshot.agents)) {
836
1000
  this.agents.set(id, structuredClone(agent));
1001
+ this.agentCostTotals.set(id, agent.costTotal);
837
1002
  }
838
1003
  this.costTotal = snapshot.costTotal;
839
1004
  this.facets.clear();
@@ -849,6 +1014,11 @@ class LocalHostedSessionHandle {
849
1014
  }
850
1015
  }
851
1016
  assertActive() {
1017
+ if (this.disposing || this.disposed) {
1018
+ throw new Error(`session is shut down: ${this.committedSessionId}`);
1019
+ }
1020
+ }
1021
+ assertNotDisposed() {
852
1022
  if (this.disposed) {
853
1023
  throw new Error(`session is shut down: ${this.committedSessionId}`);
854
1024
  }
@@ -1047,13 +1217,14 @@ class LocalHostedSessionHandle {
1047
1217
  }
1048
1218
  case "compaction_end":
1049
1219
  this.clearRunningAutoCompactionOperations();
1220
+ this.reconcileProjections();
1050
1221
  await this.emitSnapshotReset("maintenance", await this.commitSnapshot());
1051
1222
  return;
1052
1223
  case "tool_ui":
1053
1224
  await this.recordToolUiEvent(event.uiEvent);
1054
1225
  return;
1055
1226
  case "subagent_ui":
1056
- await this.recordSubagentUiEvent(event.event, event.originHistoryEntryId);
1227
+ await this.recordSubagentUiEvent(event.event);
1057
1228
  return;
1058
1229
  }
1059
1230
  }
@@ -1062,6 +1233,10 @@ class LocalHostedSessionHandle {
1062
1233
  }
1063
1234
  async recordToolUiEvent(event) {
1064
1235
  const toolCallId = event.toolCallId;
1236
+ const existingTool = this.tools.get(toolCallId);
1237
+ if (!existingTool) {
1238
+ return;
1239
+ }
1065
1240
  const facetId = `tool-ui-${toolCallId}`;
1066
1241
  const existing = this.facets.get(facetId);
1067
1242
  const events = Array.isArray(existing?.data.events) ? existing.data.events : [];
@@ -1073,18 +1248,10 @@ class LocalHostedSessionHandle {
1073
1248
  data: { events: [...events, structuredClone(event)] },
1074
1249
  };
1075
1250
  this.facets.set(facet.id, facet);
1076
- const existingTool = this.tools.get(toolCallId);
1077
- const toolChange = existingTool
1078
- ? {
1079
- type: "tool.set",
1080
- tool: this.updateToolRunFromUiEvent(existingTool, event),
1081
- }
1082
- : undefined;
1083
- if (toolChange) {
1084
- this.tools.set(toolChange.tool.id, toolChange.tool);
1085
- }
1251
+ const tool = this.updateToolRunFromUiEvent(existingTool, event);
1252
+ this.tools.set(tool.id, tool);
1086
1253
  await this.emitPatch("tool-run", [
1087
- ...(toolChange ? [toolChange] : []),
1254
+ { type: "tool.set", tool },
1088
1255
  { type: "facet.set", facet: structuredClone(facet) },
1089
1256
  ]);
1090
1257
  }
@@ -1113,13 +1280,18 @@ class LocalHostedSessionHandle {
1113
1280
  }
1114
1281
  return next;
1115
1282
  }
1116
- async recordSubagentUiEvent(event, originHistoryEntryId) {
1283
+ async recordSubagentUiEvent(event) {
1117
1284
  const existing = "id" in event ? this.agents.get(event.id) : this.agents.get(event.state.id);
1118
- const agent = agentRunFromSubagentEvent(event, originHistoryEntryId, existing);
1285
+ const agent = agentRunFromSubagentEvent(event, existing);
1119
1286
  if (!agent) {
1120
1287
  return;
1121
1288
  }
1122
- this.costTotal += Math.max(0, agent.costTotal - (existing?.costTotal ?? 0));
1289
+ const previousCost = this.agentCostTotals.get(agent.id) ?? 0;
1290
+ this.costTotal += Math.max(0, agent.costTotal - previousCost);
1291
+ this.agentCostTotals.set(agent.id, agent.costTotal);
1292
+ if (!this.session.hasSubagent(agent.id)) {
1293
+ return;
1294
+ }
1123
1295
  this.agents.set(agent.id, agent);
1124
1296
  await this.emitPatch("agent-run", [
1125
1297
  { type: "cost.set", costTotal: this.costTotal },
@@ -1165,10 +1337,11 @@ class LocalHostedSessionHandle {
1165
1337
  });
1166
1338
  if (options.persist === false) {
1167
1339
  this.committedSnapshot = applySessionProtocolDelta(this.committedSnapshot, delta);
1340
+ this.snapshotGeneration += 1;
1168
1341
  this.emitDelta(delta);
1169
1342
  return;
1170
1343
  }
1171
- const snapshot = await this.commitSnapshotWithRevision(toRevision, options);
1344
+ const snapshot = await this.commitSnapshotWithRevision(toRevision);
1172
1345
  this.emitDelta(createSessionProtocolDeltaMessage({
1173
1346
  sessionId: delta.sessionId,
1174
1347
  fromRevision,
@@ -1433,7 +1606,7 @@ function createInterruptedAssistantMessageFromModelSnapshot(draft, model) {
1433
1606
  timestamp: draft.message.timestamp,
1434
1607
  };
1435
1608
  }
1436
- function agentRunFromSubagentEvent(event, originHistoryEntryId, existing) {
1609
+ function agentRunFromSubagentEvent(event, existing) {
1437
1610
  const state = event.type === "subagent_spawned" || event.type === "subagent_finished"
1438
1611
  ? event.state
1439
1612
  : undefined;
@@ -1467,7 +1640,6 @@ function agentRunFromSubagentEvent(event, originHistoryEntryId, existing) {
1467
1640
  : state.status === "aborted"
1468
1641
  ? "cancelled"
1469
1642
  : "running",
1470
- originMessageId: originHistoryEntryId,
1471
1643
  ...(state.modelLabel !== undefined ? { modelLabel: state.modelLabel } : {}),
1472
1644
  costTotal: state.costTotal,
1473
1645
  turns: state.turns,