@markusylisiurunen/tau 0.3.41 → 0.3.43

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 (104) hide show
  1. package/README.md +14 -6
  2. package/dist/core/agent/agent_runtime.js +32 -44
  3. package/dist/core/agent/agent_runtime.js.map +1 -1
  4. package/dist/core/auth/auth_manager.js +25 -9
  5. package/dist/core/auth/auth_manager.js.map +1 -1
  6. package/dist/core/auth/auth_storage.js +1 -0
  7. package/dist/core/auth/auth_storage.js.map +1 -1
  8. package/dist/core/auth/cli.js +49 -35
  9. package/dist/core/auth/cli.js.map +1 -1
  10. package/dist/core/auth/credential_store.js +54 -22
  11. package/dist/core/auth/credential_store.js.map +1 -1
  12. package/dist/core/auth/index.js +1 -1
  13. package/dist/core/auth/index.js.map +1 -1
  14. package/dist/core/auth/providers/openai_codex.js +71 -23
  15. package/dist/core/auth/providers/openai_codex.js.map +1 -1
  16. package/dist/core/cli.js +4 -2
  17. package/dist/core/cli.js.map +1 -1
  18. package/dist/core/config/builtin_themes.js +62 -155
  19. package/dist/core/config/builtin_themes.js.map +1 -1
  20. package/dist/core/index.js +1 -1
  21. package/dist/core/index.js.map +1 -1
  22. package/dist/core/models/tau_model_overrides.js +1 -66
  23. package/dist/core/models/tau_model_overrides.js.map +1 -1
  24. package/dist/core/session/direct_bash.js +1 -0
  25. package/dist/core/session/direct_bash.js.map +1 -1
  26. package/dist/core/session/runner.js +21 -14
  27. package/dist/core/session/runner.js.map +1 -1
  28. package/dist/core/static/code_mode/capabilities.mjs +25 -0
  29. package/dist/core/static/code_mode/history/documentation.md +2 -0
  30. package/dist/core/static/code_mode/history/sandbox_runner.mjs +4 -1
  31. package/dist/core/static/code_mode/nook/documentation.md +2 -0
  32. package/dist/core/static/code_mode/nook/sandbox_runner.mjs +4 -1
  33. package/dist/core/static/code_mode/web/documentation.md +2 -0
  34. package/dist/core/static/code_mode/web/sandbox_runner.mjs +4 -1
  35. package/dist/core/subagents/agent_supervisor.js +3 -3
  36. package/dist/core/subagents/agent_supervisor.js.map +1 -1
  37. package/dist/core/telegram/adapter.js +0 -23
  38. package/dist/core/telegram/adapter.js.map +1 -1
  39. package/dist/core/telegram/session_manager.js +34 -7
  40. package/dist/core/telegram/session_manager.js.map +1 -1
  41. package/dist/core/tools/bash.js +10 -2
  42. package/dist/core/tools/bash.js.map +1 -1
  43. package/dist/core/tools/code_mode.js +1 -0
  44. package/dist/core/tools/code_mode.js.map +1 -1
  45. package/dist/core/tools/presentation.js +1 -1
  46. package/dist/core/utils/auto_retry.js +1 -1
  47. package/dist/core/utils/auto_retry.js.map +1 -1
  48. package/dist/core/version.js +1 -1
  49. package/dist/host/local_session_host.js +319 -198
  50. package/dist/host/local_session_host.js.map +1 -1
  51. package/dist/host/session_protocol_handler.js +1 -1
  52. package/dist/host/session_protocol_handler.js.map +1 -1
  53. package/dist/main.js +16 -2
  54. package/dist/main.js.map +1 -1
  55. package/dist/protocol/index.d.ts +2 -2
  56. package/dist/protocol/index.js +1 -1
  57. package/dist/protocol/index.js.map +1 -1
  58. package/dist/protocol/session_protocol.d.ts +115 -40
  59. package/dist/protocol/session_protocol.js +377 -98
  60. package/dist/protocol/session_protocol.js.map +1 -1
  61. package/dist/sdk/index.d.ts +1 -2
  62. package/dist/sdk/index.js +0 -1
  63. package/dist/sdk/index.js.map +1 -1
  64. package/dist/store/session_snapshot_migrations.js +311 -4
  65. package/dist/store/session_snapshot_migrations.js.map +1 -1
  66. package/dist/tui/chat_controller/assistant_clipboard.js +12 -8
  67. package/dist/tui/chat_controller/assistant_clipboard.js.map +1 -1
  68. package/dist/tui/chat_controller/diff_review_service.js +13 -9
  69. package/dist/tui/chat_controller/diff_review_service.js.map +1 -1
  70. package/dist/tui/chat_controller/history_message_model.js +3 -3
  71. package/dist/tui/chat_controller/history_message_model.js.map +1 -1
  72. package/dist/tui/chat_controller/status_format.js +18 -0
  73. package/dist/tui/chat_controller/status_format.js.map +1 -1
  74. package/dist/tui/chat_view.js +39 -38
  75. package/dist/tui/chat_view.js.map +1 -1
  76. package/dist/tui/session_chat_app.js +1 -3
  77. package/dist/tui/session_chat_app.js.map +1 -1
  78. package/dist/tui/session_chat_controller.js +375 -241
  79. package/dist/tui/session_chat_controller.js.map +1 -1
  80. package/dist/tui/speech_playback.js +4 -4
  81. package/dist/tui/speech_playback.js.map +1 -1
  82. package/dist/tui/ui/assistant_message.js +1 -15
  83. package/dist/tui/ui/assistant_message.js.map +1 -1
  84. package/dist/tui/ui/chat_message_model.js +26 -7
  85. package/dist/tui/ui/chat_message_model.js.map +1 -1
  86. package/dist/tui/ui/custom_editor.js +1 -1
  87. package/dist/tui/ui/custom_editor.js.map +1 -1
  88. package/dist/tui/ui/footer.js +161 -47
  89. package/dist/tui/ui/footer.js.map +1 -1
  90. package/dist/tui/ui/theme/palette.js +10 -16
  91. package/dist/tui/ui/theme/palette.js.map +1 -1
  92. package/dist/tui/ui/theme/theme.js.map +1 -1
  93. package/dist/tui/ui/tool_card.js +12 -9
  94. package/dist/tui/ui/tool_card.js.map +1 -1
  95. package/dist/tui/ui/transcript_notice.js +44 -0
  96. package/dist/tui/ui/transcript_notice.js.map +1 -0
  97. package/dist/tui/ui/transcript_text.js +14 -0
  98. package/dist/tui/ui/transcript_text.js.map +1 -0
  99. package/package.json +10 -12
  100. package/dist/sdk/turn_outcome.d.ts +0 -2
  101. package/dist/sdk/turn_outcome.js +0 -4
  102. package/dist/sdk/turn_outcome.js.map +0 -1
  103. package/dist/tui/ui/system_message.js +0 -22
  104. package/dist/tui/ui/system_message.js.map +0 -1
@@ -9,9 +9,9 @@ import { SUBAGENT_ACTIVITY_FACET_KIND } from "../core/subagents/types.js";
9
9
  import { buildToolRunPresentation, TOOL_UI_FACET_VERSION } from "../core/tools/presentation.js";
10
10
  import { appendUsageLogEntry, getUsageCostTotal, getUsageTotals, } from "../core/usage/logs.js";
11
11
  import { filterProjectPathAutocompleteEntries, loadProjectPathAutocompleteEntriesWithBackend, } from "../core/utils/project_files.js";
12
- import { hasAutoCompactionContinuationMetadata, hasGoalTurnMetadata, isTauUserMessageHidden, } from "../core/utils/user_metadata.js";
13
- import { applySessionProtocolDelta, createSessionProtocolDeltaMessage, createSessionProtocolEphemeralMessage, } from "../protocol/session_protocol.js";
14
- import { LEGACY_SESSION_CONTEXT_EPOCH } from "../store/session_snapshot_migrations.js";
12
+ import { hasGoalTurnMetadata } from "../core/utils/user_metadata.js";
13
+ import { applySessionProtocolDelta, createSessionProtocolDeltaMessage, createSessionProtocolEphemeralMessage, projectSessionProtocolNoticePresentation, } from "../protocol/session_protocol.js";
14
+ import { LEGACY_SESSION_MODEL_CONTEXT_KEY } from "../store/session_snapshot_migrations.js";
15
15
  import { ClientToolBroker } from "./client_tool_broker.js";
16
16
  import { createExecutionEnvironmentSubagentRuntimeResolver } from "./execution_runtime.js";
17
17
  import { HostedEphemeralAgentSession } from "./hosted_ephemeral_agent_session.js";
@@ -76,7 +76,7 @@ export class LocalSessionHost {
76
76
  const agentRecovery = hostedSession.runtime.restoreState({
77
77
  agentId: recovered.snapshot.sessionId,
78
78
  revision: recovered.snapshot.agentState.revision,
79
- contextEpoch: recovered.snapshot.agentState.contextEpoch,
79
+ modelContextKey: recovered.snapshot.agentState.modelContextKey,
80
80
  historyEntries: recovered.snapshot.messages.flatMap((entry) => entry.modelVisible && isCoreMessage(entry.message)
81
81
  ? [{ id: entry.id, message: entry.message }]
82
82
  : []),
@@ -344,6 +344,9 @@ export class LocalSessionHost {
344
344
  return false;
345
345
  }
346
346
  }
347
+ function normalizeDeltaCause(cause) {
348
+ return typeof cause === "string" ? { type: cause } : cause;
349
+ }
347
350
  class LocalHostedSessionHandle {
348
351
  runtime;
349
352
  promptBootstrap;
@@ -364,11 +367,9 @@ class LocalHostedSessionHandle {
364
367
  persistedSnapshot;
365
368
  draftAssistantMessage;
366
369
  messageStates = new Map();
367
- turnOutcomes = new Map();
368
- restoredMessageIds;
369
- restoredTimelineMessageIds;
370
- timelineExtras = [];
370
+ timeline = { epoch: 1, sequence: 0, items: [] };
371
371
  tools = new Map();
372
+ operations = new Map();
372
373
  agents = new Map();
373
374
  agentCostTotals = new Map();
374
375
  facets = new Map();
@@ -490,7 +491,7 @@ class LocalHostedSessionHandle {
490
491
  if (logicalTurn.cancellationRequested) {
491
492
  return {
492
493
  userHistoryEntryId,
493
- turn: await this.cancelLogicalTurn(userHistoryEntryId),
494
+ turn: await this.cancelLogicalTurn(),
494
495
  };
495
496
  }
496
497
  return {
@@ -529,7 +530,7 @@ class LocalHostedSessionHandle {
529
530
  text: buildGoalContinuationText(goal),
530
531
  });
531
532
  if (logicalTurn.cancellationRequested) {
532
- return { turn: await this.cancelLogicalTurn(userHistoryEntryId) };
533
+ return { turn: await this.cancelLogicalTurn() };
533
534
  }
534
535
  return { turn: await this.runTurnNow(logicalTurn, userHistoryEntryId) };
535
536
  }
@@ -602,7 +603,7 @@ class LocalHostedSessionHandle {
602
603
  let goalRootHistoryEntryId = this.goal?.status === "active" ? rootUserMessage.id : undefined;
603
604
  while (true) {
604
605
  if (logicalTurn.cancellationRequested) {
605
- return await this.cancelLogicalTurn(rootUserMessage.id);
606
+ return await this.cancelLogicalTurn();
606
607
  }
607
608
  const committedSteering = [];
608
609
  try {
@@ -610,11 +611,7 @@ class LocalHostedSessionHandle {
610
611
  committedSteering.push(await this.commitBufferedLogicalSteering(logicalTurn));
611
612
  }
612
613
  if (logicalTurn.cancellationRequested) {
613
- return await this.cancelLogicalTurn(rootUserMessage.id, committedSteering);
614
- }
615
- const userMessage = this.session.rawHistoryEntries.findLast((entry) => entry.message.role === "user");
616
- if (!userMessage) {
617
- throw new Error("cannot run a turn without a user message");
614
+ return await this.cancelLogicalTurn(committedSteering);
618
615
  }
619
616
  const result = await this.runtime.runTurn();
620
617
  const terminalResult = result.terminalResult;
@@ -624,16 +621,9 @@ class LocalHostedSessionHandle {
624
621
  const initialOutcome = turnOutcomeFromResult(result, result.finalMessage);
625
622
  const terminalOutcome = turnOutcomeFromResult(terminalResult, terminalResult.finalMessage);
626
623
  await this.enqueueMutation(async () => {
627
- this.turnOutcomes.set(userMessage.id, initialOutcome);
628
- for (const steering of committedSteering) {
629
- this.turnOutcomes.set(steering.historyEntryId, initialOutcome);
630
- }
631
624
  if (this.goal?.status === "active") {
632
625
  goalRootHistoryEntryId ??= rootUserMessage.id;
633
626
  }
634
- if (goalRootHistoryEntryId) {
635
- this.turnOutcomes.set(goalRootHistoryEntryId, terminalOutcome);
636
- }
637
627
  if (terminalOutcome.status !== "completed" && this.goal?.status === "active") {
638
628
  this.goal = { ...this.goal, status: "blocked" };
639
629
  }
@@ -641,7 +631,7 @@ class LocalHostedSessionHandle {
641
631
  });
642
632
  this.resolveCommittedLogicalSteering(committedSteering, initialOutcome);
643
633
  if (logicalTurn.cancellationRequested) {
644
- return await this.cancelLogicalTurn(rootUserMessage.id);
634
+ return await this.cancelLogicalTurn();
645
635
  }
646
636
  if (terminalOutcome.status !== "completed" || this.goal?.status !== "active") {
647
637
  return goalRootHistoryEntryId ? terminalOutcome : initialOutcome;
@@ -652,9 +642,15 @@ class LocalHostedSessionHandle {
652
642
  await this.session.commitUserText(buildGoalContinuationText(this.goal));
653
643
  }
654
644
  catch (error) {
655
- this.rejectCommittedLogicalSteering(committedSteering, error);
656
- await this.cleanupFailedTurn(rootUserMessage.id, committedSteering, error).catch(() => undefined);
657
- throw error;
645
+ try {
646
+ const outcome = await this.cleanupFailedTurn(rootUserMessage.id, error);
647
+ this.resolveCommittedLogicalSteering(committedSteering, outcome);
648
+ return outcome;
649
+ }
650
+ catch {
651
+ this.rejectCommittedLogicalSteering(committedSteering, error);
652
+ throw error;
653
+ }
658
654
  }
659
655
  }
660
656
  }
@@ -704,14 +700,10 @@ class LocalHostedSessionHandle {
704
700
  applied: submission.applied.then((association) => ({
705
701
  userHistoryEntryId: association.historyEntryId,
706
702
  })),
707
- result: submission.result.then(async (association) => {
708
- const turn = turnOutcomeFromResult(association.result, association.result.finalMessage);
709
- await this.commitSteeringTurnOutcome(association.historyEntryId, turn);
710
- return {
711
- userHistoryEntryId: association.historyEntryId,
712
- turn,
713
- };
714
- }),
703
+ result: submission.result.then((association) => ({
704
+ userHistoryEntryId: association.historyEntryId,
705
+ turn: turnOutcomeFromResult(association.result, association.result.finalMessage),
706
+ })),
715
707
  };
716
708
  }
717
709
  const logicalTurn = this.activeLogicalTurn;
@@ -784,7 +776,7 @@ class LocalHostedSessionHandle {
784
776
  sessionId: snapshot.sessionId,
785
777
  fromRevision,
786
778
  toRevision: snapshot.revision,
787
- reason: "configuration",
779
+ cause: { type: "configuration" },
788
780
  delta: {
789
781
  type: "snapshot.patch",
790
782
  changes: [{ type: "settings.set", settings: snapshot.settings }],
@@ -926,16 +918,11 @@ class LocalHostedSessionHandle {
926
918
  signal,
927
919
  });
928
920
  signal.throwIfAborted();
929
- return await this.enqueueMutation(async () => {
930
- this.reconcileProjections();
931
- const snapshot = await this.commitSnapshot();
932
- this.emitSnapshotReset("maintenance", snapshot);
933
- return {
934
- snapshot,
935
- compactionMessage: result.compactionMessage,
936
- includedLastAssistant: result.includedLastAssistant,
937
- };
938
- });
921
+ return {
922
+ snapshot: await this.snapshot(),
923
+ compactionMessage: result.compactionMessage,
924
+ includedLastAssistant: result.includedLastAssistant,
925
+ };
939
926
  });
940
927
  }
941
928
  async runMaintenance(operation) {
@@ -979,6 +966,9 @@ class LocalHostedSessionHandle {
979
966
  }
980
967
  async rewindToHistoryEntryId(historyEntryId) {
981
968
  this.assertActive();
969
+ if (!this.timeline.items.some((item) => item.type === "message" && item.messageId === historyEntryId)) {
970
+ throw new Error("rewind failed");
971
+ }
982
972
  const result = await this.session.rewindToHistoryEntryId(historyEntryId);
983
973
  if (!result) {
984
974
  throw new Error("rewind failed");
@@ -1216,13 +1206,9 @@ class LocalHostedSessionHandle {
1216
1206
  submission.rejectResult(error);
1217
1207
  }
1218
1208
  }
1219
- async cancelLogicalTurn(rootHistoryEntryId, committedSteering = []) {
1209
+ async cancelLogicalTurn(committedSteering = []) {
1220
1210
  const outcome = abortedTurnOutcome();
1221
1211
  await this.enqueueMutation(async () => {
1222
- this.turnOutcomes.set(rootHistoryEntryId, outcome);
1223
- for (const steering of committedSteering) {
1224
- this.turnOutcomes.set(steering.historyEntryId, outcome);
1225
- }
1226
1212
  if (this.goal?.status === "active") {
1227
1213
  this.goal = { ...this.goal, status: "blocked" };
1228
1214
  }
@@ -1233,6 +1219,7 @@ class LocalHostedSessionHandle {
1233
1219
  }
1234
1220
  async commitSnapshot() {
1235
1221
  this.assertNotDisposed();
1222
+ this.reconcileProjections();
1236
1223
  const draft = this.buildSnapshotDraft();
1237
1224
  await this.switchSnapshotSession(draft.sessionId);
1238
1225
  const snapshot = {
@@ -1265,7 +1252,7 @@ class LocalHostedSessionHandle {
1265
1252
  this.persistedSnapshot = cloneSessionProtocolSnapshot(snapshot);
1266
1253
  return snapshot;
1267
1254
  }
1268
- async commitSnapshotPatch(reason, changes) {
1255
+ async commitSnapshotPatch(cause, changes) {
1269
1256
  this.assertNotDisposed();
1270
1257
  const current = this.committedSnapshot;
1271
1258
  if (!current) {
@@ -1275,7 +1262,7 @@ class LocalHostedSessionHandle {
1275
1262
  sessionId: current.sessionId,
1276
1263
  fromRevision: current.revision,
1277
1264
  toRevision: current.revision + 1,
1278
- reason,
1265
+ cause: normalizeDeltaCause(cause),
1279
1266
  delta: { type: "snapshot.patch", changes },
1280
1267
  });
1281
1268
  const snapshot = applySessionProtocolDelta(current, delta);
@@ -1300,10 +1287,8 @@ class LocalHostedSessionHandle {
1300
1287
  reconcileProjections(options = {}) {
1301
1288
  const messageIds = new Set(this.session.rawHistoryEntries.map((entry) => entry.id));
1302
1289
  messageIds.add("system");
1303
- for (const id of this.turnOutcomes.keys()) {
1304
- if (!messageIds.has(id)) {
1305
- this.turnOutcomes.delete(id);
1306
- }
1290
+ if (this.draftAssistantMessage) {
1291
+ messageIds.add(this.draftAssistantMessage.id);
1307
1292
  }
1308
1293
  for (const [id, tool] of this.tools) {
1309
1294
  const messageId = tool.status === "streaming" ? tool.origin.messageId : tool.call.messageId;
@@ -1322,13 +1307,36 @@ class LocalHostedSessionHandle {
1322
1307
  }
1323
1308
  }
1324
1309
  }
1325
- const operationIds = new Set(this.timelineExtras.filter((item) => item.type === "operation").map((item) => item.id));
1310
+ for (const id of this.operations.keys()) {
1311
+ const hasTimelineItem = this.timeline.items.some((item) => item.type === "operation" && item.operationId === id);
1312
+ if (!hasTimelineItem) {
1313
+ this.operations.delete(id);
1314
+ }
1315
+ }
1316
+ this.timeline.items = this.timeline.items.filter((item) => {
1317
+ switch (item.type) {
1318
+ case "message":
1319
+ return messageIds.has(item.messageId);
1320
+ case "tool":
1321
+ return this.tools.has(item.toolId);
1322
+ case "notice":
1323
+ return (item.notice.subject.type === "session" ||
1324
+ (item.notice.subject.type === "message" && messageIds.has(item.notice.subject.id)) ||
1325
+ (item.notice.subject.type === "tool" && this.tools.has(item.notice.subject.id)) ||
1326
+ (item.notice.subject.type === "agent" && this.agents.has(item.notice.subject.id)) ||
1327
+ (item.notice.subject.type === "operation" &&
1328
+ this.operations.has(item.notice.subject.id)));
1329
+ case "operation":
1330
+ return this.operations.has(item.operationId);
1331
+ }
1332
+ return false;
1333
+ });
1326
1334
  for (const [id, facet] of this.facets) {
1327
1335
  const subjectExists = facet.subject.type === "session" ||
1328
1336
  (facet.subject.type === "message" && messageIds.has(facet.subject.id)) ||
1329
1337
  (facet.subject.type === "tool" && this.tools.has(facet.subject.id)) ||
1330
1338
  (facet.subject.type === "agent" && this.agents.has(facet.subject.id)) ||
1331
- (facet.subject.type === "operation" && operationIds.has(facet.subject.id));
1339
+ (facet.subject.type === "operation" && this.operations.has(facet.subject.id));
1332
1340
  if (!subjectExists) {
1333
1341
  this.facets.delete(id);
1334
1342
  }
@@ -1343,7 +1351,7 @@ class LocalHostedSessionHandle {
1343
1351
  createdAt: this.createdAt,
1344
1352
  agentState: {
1345
1353
  revision: agentState.revision,
1346
- contextEpoch: agentState.contextEpoch,
1354
+ modelContextKey: agentState.modelContextKey,
1347
1355
  ...(agentState.usageCheckpoint
1348
1356
  ? { usageCheckpoint: { ...agentState.usageCheckpoint } }
1349
1357
  : {}),
@@ -1370,8 +1378,9 @@ class LocalHostedSessionHandle {
1370
1378
  catalog: structuredClone(this.catalog),
1371
1379
  executionEnvironment: this.executionEnvironment.snapshot(),
1372
1380
  messages,
1373
- timeline: this.buildTimeline(messages),
1381
+ timeline: structuredClone(this.timeline),
1374
1382
  tools: Object.fromEntries(this.tools),
1383
+ operations: Object.fromEntries(this.operations),
1375
1384
  agents: Object.fromEntries(this.agents),
1376
1385
  facets: Object.fromEntries(this.facets),
1377
1386
  };
@@ -1387,64 +1396,86 @@ class LocalHostedSessionHandle {
1387
1396
  timestamp: 0,
1388
1397
  },
1389
1398
  };
1390
- const historyMessages = this.session.rawHistoryEntries.map((entry) => {
1391
- const turn = this.turnOutcomes.get(entry.id);
1392
- return {
1393
- id: entry.id,
1394
- state: this.messageStates.get(entry.id) ?? "committed",
1395
- modelVisible: true,
1396
- message: entry.message,
1397
- ...(turn ? { turn } : {}),
1398
- };
1399
- });
1399
+ const historyMessages = this.session.rawHistoryEntries.map((entry) => ({
1400
+ id: entry.id,
1401
+ state: this.messageStates.get(entry.id) ?? "committed",
1402
+ modelVisible: true,
1403
+ message: entry.message,
1404
+ }));
1400
1405
  return [
1401
1406
  systemMessage,
1402
1407
  ...historyMessages,
1403
1408
  ...(this.draftAssistantMessage ? [structuredClone(this.draftAssistantMessage)] : []),
1404
1409
  ];
1405
1410
  }
1406
- buildTimeline(messages) {
1407
- const messageItems = messages
1408
- .filter((message) => this.shouldIncludeMessageInTimeline(message))
1409
- .map((message) => ({
1411
+ nextTimelineItemBase(id, createdAt) {
1412
+ this.timeline.sequence += 1;
1413
+ return { id, sequence: this.timeline.sequence, createdAt };
1414
+ }
1415
+ appendMessageTimelineItem(messageId, createdAt) {
1416
+ const item = {
1417
+ ...this.nextTimelineItemBase(`timeline-message-${messageId}`, createdAt),
1410
1418
  type: "message",
1411
- id: `timeline-${message.id}`,
1412
- messageId: message.id,
1413
- }));
1414
- return [...messageItems, ...structuredClone(this.timelineExtras)];
1419
+ messageId,
1420
+ };
1421
+ this.timeline.items.push(item);
1422
+ return item;
1415
1423
  }
1416
- shouldIncludeMessageInTimeline(message) {
1417
- if (message.id === "system") {
1418
- return false;
1419
- }
1420
- if (isCoreMessage(message.message) &&
1421
- (isTauUserMessageHidden(message.message) ||
1422
- hasAutoCompactionContinuationMetadata(message.message))) {
1423
- return false;
1424
- }
1425
- if (!this.restoredTimelineMessageIds || !this.restoredMessageIds) {
1426
- return true;
1427
- }
1428
- return (this.restoredTimelineMessageIds.has(message.id) || !this.restoredMessageIds.has(message.id));
1424
+ appendToolTimelineItem(toolId, createdAt) {
1425
+ const item = {
1426
+ ...this.nextTimelineItemBase(`timeline-tool-${toolId}`, createdAt),
1427
+ type: "tool",
1428
+ toolId,
1429
+ };
1430
+ this.timeline.items.push(item);
1431
+ return item;
1429
1432
  }
1430
- clearRunningAutoCompactionOperations() {
1431
- this.timelineExtras.splice(0, this.timelineExtras.length, ...this.timelineExtras.filter((item) => item.type !== "operation" ||
1432
- item.operation.kind !== "auto-compaction" ||
1433
- item.operation.status !== "running"));
1433
+ appendNoticeTimelineItem(id, createdAt, notice) {
1434
+ const item = {
1435
+ ...this.nextTimelineItemBase(id, createdAt),
1436
+ type: "notice",
1437
+ notice,
1438
+ };
1439
+ this.timeline.items.push(item);
1440
+ return item;
1441
+ }
1442
+ appendTurnFailureTimelineItem(historyEntryId, failure) {
1443
+ const blocked = failure.reason === "auto-compaction-failed";
1444
+ return this.appendNoticeTimelineItem(`turn-${blocked ? "blocked" : "failed"}-${randomUUID()}`, Date.now(), {
1445
+ kind: blocked ? "tau.turn.blocked" : "tau.turn.failed",
1446
+ version: 1,
1447
+ severity: "error",
1448
+ subject: { type: "message", id: historyEntryId },
1449
+ presentation: projectSessionProtocolNoticePresentation(blocked ? "turn blocked" : "turn failed", [failure.message]),
1450
+ data: { reason: failure.reason },
1451
+ });
1452
+ }
1453
+ appendOperationTimelineItem(operation) {
1454
+ const item = {
1455
+ ...this.nextTimelineItemBase(`timeline-operation-${operation.id}`, operation.startedAt),
1456
+ type: "operation",
1457
+ operationId: operation.id,
1458
+ };
1459
+ this.operations.set(operation.id, operation);
1460
+ this.timeline.items.push(item);
1461
+ return item;
1462
+ }
1463
+ removeTimelineItem(id) {
1464
+ this.timeline.items = this.timeline.items.filter((item) => item.id !== id);
1434
1465
  }
1435
1466
  restoreProtocolState(snapshot) {
1436
1467
  if (!snapshot) {
1437
1468
  return;
1438
1469
  }
1439
- this.restoredMessageIds = new Set(snapshot.messages.map((message) => message.id));
1440
- this.restoredTimelineMessageIds = new Set(snapshot.timeline.filter((item) => item.type === "message").map((item) => item.messageId));
1441
- this.timelineExtras.splice(0, this.timelineExtras.length, ...snapshot.timeline
1442
- .filter((item) => item.type !== "message")
1443
- .map((item) => structuredClone(item)));
1470
+ this.timeline = structuredClone(snapshot.timeline);
1444
1471
  this.tools.clear();
1445
1472
  for (const [id, tool] of Object.entries(snapshot.tools)) {
1446
1473
  this.tools.set(id, structuredClone(tool));
1447
1474
  }
1475
+ this.operations.clear();
1476
+ for (const [id, operation] of Object.entries(snapshot.operations)) {
1477
+ this.operations.set(id, structuredClone(operation));
1478
+ }
1448
1479
  this.agents.clear();
1449
1480
  this.agentCostTotals.clear();
1450
1481
  for (const [id, agent] of Object.entries(snapshot.agents)) {
@@ -1459,14 +1490,10 @@ class LocalHostedSessionHandle {
1459
1490
  const historyEntryIds = new Set(this.session.rawHistoryEntries.map((entry) => entry.id));
1460
1491
  this.draftAssistantMessage = snapshot.messages.find((message) => message.state === "draft" && !historyEntryIds.has(message.id));
1461
1492
  this.messageStates.clear();
1462
- this.turnOutcomes.clear();
1463
1493
  for (const message of snapshot.messages) {
1464
1494
  if (message.state !== "committed" && message.state !== "draft") {
1465
1495
  this.messageStates.set(message.id, message.state);
1466
1496
  }
1467
- if (message.turn) {
1468
- this.turnOutcomes.set(message.id, message.turn);
1469
- }
1470
1497
  }
1471
1498
  }
1472
1499
  enqueueMutation(mutation) {
@@ -1514,47 +1541,34 @@ class LocalHostedSessionHandle {
1514
1541
  async enqueueRuntimeEvent(event) {
1515
1542
  await this.enqueueMutation(() => this.recordRuntimeEvent(event));
1516
1543
  }
1517
- async commitSteeringTurnOutcome(historyEntryId, turn) {
1518
- await this.enqueueMutation(async () => {
1519
- this.turnOutcomes.set(historyEntryId, turn);
1520
- const message = this.committedSnapshot?.messages.find((candidate) => candidate.id === historyEntryId);
1521
- if (!message) {
1522
- throw new Error(`missing steering user message '${historyEntryId}'`);
1523
- }
1524
- if (isDeepStrictEqual(message.turn, turn)) {
1525
- return;
1526
- }
1527
- await this.emitPatch("assistant-message", [
1528
- {
1529
- type: "message.replace",
1530
- message: { ...structuredClone(message), turn },
1531
- },
1532
- ]);
1533
- });
1534
- }
1535
1544
  removeToolRun(tool, changes) {
1536
1545
  this.tools.delete(tool.id);
1546
+ const timelineItemId = `timeline-tool-${tool.id}`;
1547
+ this.removeTimelineItem(timelineItemId);
1537
1548
  for (const facetId of tool.facetIds) {
1538
1549
  this.facets.delete(facetId);
1539
1550
  changes.push({ type: "facet.remove", id: facetId });
1540
1551
  }
1541
- changes.push({ type: "tool.remove", id: tool.id });
1552
+ changes.push({ type: "tool.remove", id: tool.id }, { type: "timeline.remove", id: timelineItemId });
1542
1553
  }
1543
1554
  async recordHistoryFailure(reason) {
1544
- if (!reason || this.timelineExtras.some((item) => item.id === HISTORY_UNAVAILABLE_NOTICE_ID)) {
1555
+ if (!reason || this.timeline.items.some((item) => item.id === HISTORY_UNAVAILABLE_NOTICE_ID)) {
1545
1556
  return;
1546
1557
  }
1547
- const item = {
1548
- type: "notice",
1549
- id: HISTORY_UNAVAILABLE_NOTICE_ID,
1550
- notice: {
1551
- severity: "warn",
1552
- text: `Session history is unavailable: ${reason}. This session will continue without durable history recording or recall.`,
1553
- timestamp: Date.now(),
1554
- },
1555
- };
1556
- this.timelineExtras.push(item);
1557
- await this.emitPatch("notice", [{ type: "timeline.append", item: structuredClone(item) }]);
1558
+ const item = this.appendNoticeTimelineItem(HISTORY_UNAVAILABLE_NOTICE_ID, Date.now(), {
1559
+ kind: "tau.history.unavailable",
1560
+ version: 1,
1561
+ severity: "warn",
1562
+ subject: { type: "session" },
1563
+ presentation: projectSessionProtocolNoticePresentation("session history is unavailable", [
1564
+ reason,
1565
+ "this session will continue without durable history recording or recall",
1566
+ ]),
1567
+ data: {},
1568
+ });
1569
+ await this.emitPatch({ type: "notice" }, [
1570
+ { type: "timeline.append", item: structuredClone(item) },
1571
+ ]);
1558
1572
  }
1559
1573
  async appendToolHistory(tool, resultHistoryEntryId, result) {
1560
1574
  const callMessage = this.committedSnapshot?.messages.find((message) => message.id === tool.call.messageId);
@@ -1583,7 +1597,7 @@ class LocalHostedSessionHandle {
1583
1597
  type: "agent-state.set",
1584
1598
  agentState: {
1585
1599
  revision: state.revision,
1586
- contextEpoch: state.contextEpoch,
1600
+ modelContextKey: state.modelContextKey,
1587
1601
  ...(state.usageCheckpoint ? { usageCheckpoint: { ...state.usageCheckpoint } } : {}),
1588
1602
  },
1589
1603
  };
@@ -1597,13 +1611,13 @@ class LocalHostedSessionHandle {
1597
1611
  modelVisible: false,
1598
1612
  message: { role: "assistant", content: [], timestamp: Date.now() },
1599
1613
  };
1600
- const timelineItem = timelineItemForMessage(this.draftAssistantMessage.id);
1614
+ const timelineItem = this.appendMessageTimelineItem(this.draftAssistantMessage.id, this.draftAssistantMessage.message.timestamp);
1601
1615
  await this.emitPatch("assistant-stream", [
1602
1616
  {
1603
1617
  type: "message.append",
1604
1618
  message: structuredClone(this.draftAssistantMessage),
1605
- timelineItem,
1606
1619
  },
1620
+ { type: "timeline.append", item: structuredClone(timelineItem) },
1607
1621
  { type: "lifecycle.set", lifecycle: "running" },
1608
1622
  ], { persist: false });
1609
1623
  return;
@@ -1651,7 +1665,8 @@ class LocalHostedSessionHandle {
1651
1665
  };
1652
1666
  this.tools.set(tool.id, tool);
1653
1667
  this.facets.set(facet.id, facet);
1654
- changes.push({ type: "tool.set", tool: structuredClone(tool) }, { type: "facet.set", facet: structuredClone(facet) });
1668
+ const timelineItem = this.appendToolTimelineItem(tool.id, Date.now());
1669
+ changes.push({ type: "tool.set", tool: structuredClone(tool) }, { type: "facet.set", facet: structuredClone(facet) }, { type: "timeline.append", item: structuredClone(timelineItem) });
1655
1670
  await this.emitPatch("tool-run", changes, { persist: false });
1656
1671
  return;
1657
1672
  }
@@ -1706,6 +1721,7 @@ class LocalHostedSessionHandle {
1706
1721
  }
1707
1722
  case "user_message": {
1708
1723
  const pendingGoal = this.pendingGoalCommit;
1724
+ const timelineItem = this.appendMessageTimelineItem(event.historyEntryId, event.message.timestamp);
1709
1725
  await this.emitPatch("user-message", [
1710
1726
  this.agentStateChange(),
1711
1727
  ...(pendingGoal
@@ -1719,8 +1735,8 @@ class LocalHostedSessionHandle {
1719
1735
  modelVisible: true,
1720
1736
  message: event.message,
1721
1737
  },
1722
- timelineItem: timelineItemForMessage(event.historyEntryId),
1723
1738
  },
1739
+ { type: "timeline.append", item: structuredClone(timelineItem) },
1724
1740
  ], { persist: true });
1725
1741
  await this.recordHistoryFailure(this.history.append(this.sessionId, [userHistoryEntry(event.historyEntryId, event.message)], this.historyRemote));
1726
1742
  if (pendingGoal && this.pendingGoalCommit === pendingGoal) {
@@ -1728,11 +1744,19 @@ class LocalHostedSessionHandle {
1728
1744
  }
1729
1745
  return;
1730
1746
  }
1731
- case "history_rewound":
1747
+ case "history_rewound": {
1748
+ const rewindItem = this.timeline.items.find((item) => item.type === "message" && item.messageId === event.historyEntryId);
1749
+ if (!rewindItem) {
1750
+ throw new Error(`missing timeline item for rewind message '${event.historyEntryId}'`);
1751
+ }
1752
+ const cutoffSequence = rewindItem.sequence - 1;
1753
+ this.timeline.items = this.timeline.items.filter((item) => item.sequence <= cutoffSequence);
1732
1754
  this.reconcileProjections({ removeMissingAgents: true });
1733
- await this.emitSnapshotResetIfChanged("maintenance");
1755
+ const snapshot = await this.commitSnapshot();
1756
+ this.emitSnapshotReset({ type: "rewind", epoch: this.timeline.epoch, cutoffSequence }, snapshot);
1734
1757
  await this.recordHistoryFailure(this.history.truncateFromSources(this.sessionId, event.removedEntryIds, this.historyRemote));
1735
1758
  return;
1759
+ }
1736
1760
  case "assistant_final": {
1737
1761
  this.draftAssistantMessage = undefined;
1738
1762
  const messageState = event.message.stopReason === "aborted" ? "interrupted" : "committed";
@@ -1833,7 +1857,6 @@ class LocalHostedSessionHandle {
1833
1857
  modelVisible: true,
1834
1858
  message: event.message,
1835
1859
  },
1836
- timelineItem: timelineItemForMessage(event.historyEntryId),
1837
1860
  },
1838
1861
  { type: "tool.set", tool: structuredClone(nextTool) },
1839
1862
  ]);
@@ -1850,7 +1873,6 @@ class LocalHostedSessionHandle {
1850
1873
  modelVisible: true,
1851
1874
  message: event.message,
1852
1875
  },
1853
- timelineItem: timelineItemForMessage(event.historyEntryId),
1854
1876
  },
1855
1877
  ]);
1856
1878
  return;
@@ -1884,52 +1906,137 @@ class LocalHostedSessionHandle {
1884
1906
  changes.push({ type: "tool.set", tool: structuredClone(nextTool) });
1885
1907
  historyTools.push({ tool: existing, result: toolResult });
1886
1908
  }
1887
- await this.emitPatch("recovery", changes);
1909
+ await this.emitPatch({ type: "resync" }, changes);
1888
1910
  for (const { tool, result } of historyTools) {
1889
1911
  await this.appendToolHistory(tool, event.historyEntryId, result);
1890
1912
  }
1891
1913
  return;
1892
1914
  }
1893
- case "notice": {
1915
+ case "feedback": {
1916
+ if (event.presentation === "footer") {
1917
+ this.emitEphemeral(createSessionProtocolEphemeralMessage({
1918
+ sessionId: this.sessionId,
1919
+ event: {
1920
+ type: "feedback.notice",
1921
+ title: projectSessionProtocolNoticePresentation(event.title).title,
1922
+ tone: event.tone,
1923
+ presentation: "footer",
1924
+ durationMs: event.durationMs,
1925
+ },
1926
+ }));
1927
+ return;
1928
+ }
1929
+ const createdAt = Date.now();
1894
1930
  const item = {
1931
+ ...this.nextTimelineItemBase(`ephemeral-notice-${randomUUID()}`, createdAt),
1895
1932
  type: "notice",
1896
- id: `notice-${Date.now()}-${this.timelineExtras.length}`,
1897
1933
  notice: {
1898
- severity: event.severity,
1899
- text: event.text,
1900
- timestamp: Date.now(),
1934
+ kind: "tau.runtime.feedback",
1935
+ version: 1,
1936
+ severity: event.tone === "error" ? "error" : "info",
1937
+ subject: { type: "session" },
1938
+ presentation: projectSessionProtocolNoticePresentation(event.title, event.content),
1939
+ data: {},
1901
1940
  },
1902
1941
  };
1903
- this.timelineExtras.push(item);
1904
- await this.emitPatch("notice", [{ type: "timeline.append", item: structuredClone(item) }]);
1942
+ await this.emitPatch("notice", [
1943
+ {
1944
+ type: "timeline.advance",
1945
+ epoch: this.timeline.epoch,
1946
+ sequence: item.sequence,
1947
+ },
1948
+ ]);
1949
+ this.emitEphemeral(createSessionProtocolEphemeralMessage({
1950
+ sessionId: this.sessionId,
1951
+ event: { type: "timeline.item", epoch: this.timeline.epoch, item },
1952
+ }));
1905
1953
  return;
1906
1954
  }
1907
1955
  case "compaction_start": {
1908
- const item = {
1909
- type: "operation",
1910
- id: `operation-auto-compaction-${Date.now()}`,
1911
- operation: {
1912
- kind: "auto-compaction",
1913
- status: "running",
1914
- startedAt: Date.now(),
1915
- },
1956
+ const kind = event.reason === "manual" ? "manual-compaction" : "auto-compaction";
1957
+ const operation = {
1958
+ id: `${kind}-${randomUUID()}`,
1959
+ kind,
1960
+ status: "running",
1961
+ startedAt: Date.now(),
1916
1962
  };
1917
- this.timelineExtras.push(item);
1963
+ const item = this.appendOperationTimelineItem(operation);
1918
1964
  await this.emitPatch("maintenance", [
1965
+ { type: "operation.set", operation: structuredClone(operation) },
1919
1966
  { type: "timeline.append", item: structuredClone(item) },
1920
1967
  ]);
1921
1968
  return;
1922
1969
  }
1923
- case "compaction_end":
1924
- this.clearRunningAutoCompactionOperations();
1925
- this.reconcileProjections();
1926
- await this.emitSnapshotReset("maintenance", await this.commitSnapshot());
1970
+ case "compaction_end": {
1971
+ const operation = [...this.operations.values()].findLast((candidate) => (candidate.kind === "auto-compaction" || candidate.kind === "manual-compaction") &&
1972
+ candidate.status === "running");
1973
+ if (operation) {
1974
+ const finishedAt = Date.now();
1975
+ const finishedOperation = event.outcome === "compacted"
1976
+ ? { ...operation, status: "succeeded", finishedAt }
1977
+ : event.outcome === "failed"
1978
+ ? {
1979
+ ...operation,
1980
+ status: "failed",
1981
+ finishedAt,
1982
+ error: event.errorMessage,
1983
+ }
1984
+ : event.outcome === "skipped"
1985
+ ? {
1986
+ ...operation,
1987
+ status: "skipped",
1988
+ finishedAt,
1989
+ reason: "no-eligible-history",
1990
+ }
1991
+ : {
1992
+ ...operation,
1993
+ status: "cancelled",
1994
+ finishedAt,
1995
+ reason: "interrupted",
1996
+ };
1997
+ this.operations.set(operation.id, finishedOperation);
1998
+ await this.emitPatch("maintenance", [
1999
+ { type: "operation.set", operation: structuredClone(finishedOperation) },
2000
+ ]);
2001
+ }
2002
+ if (event.outcome !== "compacted") {
2003
+ return;
2004
+ }
2005
+ const previousEpoch = this.timeline.epoch;
2006
+ this.timeline = { epoch: previousEpoch + 1, sequence: 0, items: [] };
2007
+ this.tools.clear();
2008
+ this.operations.clear();
2009
+ this.facets.clear();
2010
+ this.draftAssistantMessage = undefined;
2011
+ this.reconcileProjections({ removeMissingAgents: true });
2012
+ const summary = this.session.rawHistoryEntries.find((entry) => entry.id === event.result.summaryHistoryEntryId);
2013
+ if (!summary) {
2014
+ throw new Error(`missing compaction summary '${event.result.summaryHistoryEntryId}' in runtime history`);
2015
+ }
2016
+ this.appendMessageTimelineItem(summary.id, summary.message.timestamp);
2017
+ const snapshot = await this.commitSnapshot();
2018
+ this.emitSnapshotReset({
2019
+ type: "compaction",
2020
+ previousEpoch,
2021
+ epoch: this.timeline.epoch,
2022
+ kind: event.reason === "manual" ? "manual" : "auto",
2023
+ cutType: event.result.cutType,
2024
+ retainedMessageCount: event.result.retainedMessageCount,
2025
+ }, snapshot);
1927
2026
  return;
2027
+ }
1928
2028
  case "tool_activity":
1929
2029
  await this.recordToolUiEvent(event.activity);
1930
2030
  return;
2031
+ case "turn_finished": {
2032
+ if (!event.failure) {
2033
+ return;
2034
+ }
2035
+ const item = this.appendTurnFailureTimelineItem(event.historyEntryId, event.failure);
2036
+ await this.emitPatch("notice", [{ type: "timeline.append", item: structuredClone(item) }]);
2037
+ return;
2038
+ }
1931
2039
  case "turn_started":
1932
- case "turn_finished":
1933
2040
  case "tool_call_admitted":
1934
2041
  case "model_retry_scheduled":
1935
2042
  case "model_retry_started":
@@ -2001,12 +2108,13 @@ class LocalHostedSessionHandle {
2001
2108
  }
2002
2109
  buildAssistantPartialChanges(previousDraft, nextDraft) {
2003
2110
  if (!previousDraft) {
2111
+ const timelineItem = this.appendMessageTimelineItem(nextDraft.id, nextDraft.message.timestamp);
2004
2112
  return [
2005
2113
  {
2006
2114
  type: "message.append",
2007
2115
  message: structuredClone(nextDraft),
2008
- timelineItem: timelineItemForMessage(nextDraft.id),
2009
2116
  },
2117
+ { type: "timeline.append", item: structuredClone(timelineItem) },
2010
2118
  ];
2011
2119
  }
2012
2120
  const appendChange = buildAssistantContentAppendChange(previousDraft, nextDraft);
@@ -2018,7 +2126,7 @@ class LocalHostedSessionHandle {
2018
2126
  }
2019
2127
  return [{ type: "message.replace", message: structuredClone(nextDraft) }];
2020
2128
  }
2021
- async emitPatch(reason, changes, options = {}) {
2129
+ async emitPatch(cause, changes, options = {}) {
2022
2130
  if (changes.length === 0) {
2023
2131
  return;
2024
2132
  }
@@ -2026,44 +2134,44 @@ class LocalHostedSessionHandle {
2026
2134
  if (options.persist === false) {
2027
2135
  const snapshot = { ...this.buildSnapshotDraft(), revision: 1 };
2028
2136
  this.committedSnapshot = snapshot;
2029
- this.emitSnapshotReset(reason, snapshot);
2137
+ this.emitSnapshotReset(cause, snapshot);
2030
2138
  return;
2031
2139
  }
2032
2140
  const snapshot = await this.commitSnapshot();
2033
- this.emitSnapshotReset(reason, snapshot);
2141
+ this.emitSnapshotReset(cause, snapshot);
2034
2142
  return;
2035
2143
  }
2036
2144
  if (options.persist !== false) {
2037
- this.emitDelta(await this.commitSnapshotPatch(reason, changes));
2145
+ this.emitDelta(await this.commitSnapshotPatch(cause, changes));
2038
2146
  return;
2039
2147
  }
2040
2148
  const delta = createSessionProtocolDeltaMessage({
2041
2149
  sessionId: this.committedSnapshot.sessionId,
2042
2150
  fromRevision: this.committedSnapshot.revision,
2043
2151
  toRevision: this.committedSnapshot.revision + 1,
2044
- reason,
2152
+ cause: normalizeDeltaCause(cause),
2045
2153
  delta: { type: "snapshot.patch", changes },
2046
2154
  });
2047
2155
  this.committedSnapshot = applySessionProtocolDelta(this.committedSnapshot, delta);
2048
2156
  this.emitDelta(delta);
2049
2157
  }
2050
- emitSnapshotReset(reason, snapshot) {
2158
+ emitSnapshotReset(cause, snapshot) {
2051
2159
  this.emitDelta(createSessionProtocolDeltaMessage({
2052
2160
  sessionId: snapshot.sessionId,
2053
2161
  fromRevision: null,
2054
2162
  toRevision: snapshot.revision,
2055
- reason,
2163
+ cause: normalizeDeltaCause(cause),
2056
2164
  delta: { type: "snapshot.reset", snapshot },
2057
2165
  }));
2058
2166
  }
2059
- async enqueueSnapshotResetIfChanged(reason) {
2060
- await this.enqueueMutation(() => this.emitSnapshotResetIfChanged(reason));
2167
+ async enqueueSnapshotResetIfChanged(cause) {
2168
+ await this.enqueueMutation(() => this.emitSnapshotResetIfChanged(cause));
2061
2169
  }
2062
- async emitSnapshotResetIfChanged(reason) {
2170
+ async emitSnapshotResetIfChanged(cause) {
2063
2171
  const previousRevision = this.committedSnapshot?.revision;
2064
2172
  const snapshot = await this.commitSnapshot();
2065
2173
  if (snapshot.revision !== previousRevision) {
2066
- this.emitSnapshotReset(reason, snapshot);
2174
+ this.emitSnapshotReset(cause, snapshot);
2067
2175
  }
2068
2176
  }
2069
2177
  emitDelta(delta) {
@@ -2113,6 +2221,10 @@ class LocalHostedSessionHandle {
2113
2221
  };
2114
2222
  this.tools.set(content.id, nextTool);
2115
2223
  changes.push({ type: "tool.set", tool: structuredClone(nextTool) });
2224
+ if (!existing) {
2225
+ const timelineItem = this.appendToolTimelineItem(content.id, Date.now());
2226
+ changes.push({ type: "timeline.append", item: structuredClone(timelineItem) });
2227
+ }
2116
2228
  }
2117
2229
  return changes;
2118
2230
  }
@@ -2124,21 +2236,16 @@ class LocalHostedSessionHandle {
2124
2236
  const interruptedMessage = createInterruptedAssistantMessage(draft, this.runtime.persona.model);
2125
2237
  await this.session.commitInterruptedAssistant(interruptedMessage, draft.id);
2126
2238
  }
2127
- async cleanupFailedTurn(rootHistoryEntryId, committedSteering, error) {
2239
+ async cleanupFailedTurn(rootHistoryEntryId, error) {
2128
2240
  if (this.draftAssistantMessage) {
2129
2241
  await this.interruptDraftAssistantMessage().catch(() => undefined);
2130
2242
  }
2131
2243
  const diagnostic = formatErrorDiagnostic(error);
2132
2244
  await this.enqueueMutation(async () => {
2133
- const outcome = {
2134
- status: "failed",
2135
- stopReason: "error",
2136
- errorMessage: diagnostic,
2137
- };
2138
- this.turnOutcomes.set(rootHistoryEntryId, outcome);
2139
- for (const steering of committedSteering) {
2140
- this.turnOutcomes.set(steering.historyEntryId, outcome);
2141
- }
2245
+ this.appendTurnFailureTimelineItem(rootHistoryEntryId, {
2246
+ reason: "runtime-error",
2247
+ message: diagnostic,
2248
+ });
2142
2249
  if (this.goal?.status === "active") {
2143
2250
  this.goal = { ...this.goal, status: "blocked" };
2144
2251
  }
@@ -2156,6 +2263,7 @@ class LocalHostedSessionHandle {
2156
2263
  }
2157
2264
  await this.emitSnapshotResetIfChanged("assistant-message");
2158
2265
  });
2266
+ return { status: "failed", stopReason: "error", errorMessage: diagnostic };
2159
2267
  }
2160
2268
  }
2161
2269
  function formatErrorDiagnostic(error) {
@@ -2197,6 +2305,9 @@ function turnOutcomeFromResult(result, assistantMessage) {
2197
2305
  if (assistantMessage.stopReason === "pending") {
2198
2306
  throw new Error("session turn completed with a pending assistant message");
2199
2307
  }
2308
+ if (assistantMessage.stopReason === "deferred") {
2309
+ throw new Error("session turn completed with a deferred assistant message");
2310
+ }
2200
2311
  return { status: "completed", stopReason: assistantMessage.stopReason };
2201
2312
  }
2202
2313
  function cloneSessionProtocolSnapshot(snapshot) {
@@ -2257,7 +2368,7 @@ function getAssistantDraftBlockText(message, type) {
2257
2368
  }
2258
2369
  function normalizeRecoveredSnapshot(snapshot) {
2259
2370
  const recovered = cloneSessionProtocolSnapshot(snapshot);
2260
- const legacyAgentState = recovered.agentState.contextEpoch === LEGACY_SESSION_CONTEXT_EPOCH;
2371
+ const legacyAgentState = recovered.agentState.modelContextKey === LEGACY_SESSION_MODEL_CONTEXT_KEY;
2261
2372
  let changed = recovered.lifecycle !== "idle" || legacyAgentState;
2262
2373
  recovered.lifecycle = "idle";
2263
2374
  if (recovered.goal?.status === "active") {
@@ -2269,6 +2380,19 @@ function normalizeRecoveredSnapshot(snapshot) {
2269
2380
  recovered.agents = {};
2270
2381
  recovered.facets = Object.fromEntries(Object.entries(recovered.facets).filter(([, facet]) => facet.subject.type !== "agent"));
2271
2382
  }
2383
+ const recoveredAt = Date.now();
2384
+ for (const [id, operation] of Object.entries(recovered.operations)) {
2385
+ if (operation.status !== "running") {
2386
+ continue;
2387
+ }
2388
+ changed = true;
2389
+ recovered.operations[id] = {
2390
+ ...operation,
2391
+ status: "cancelled",
2392
+ finishedAt: recoveredAt,
2393
+ reason: "session-recovered",
2394
+ };
2395
+ }
2272
2396
  const streamingToolIds = new Set(Object.values(recovered.tools)
2273
2397
  .filter((tool) => tool.status === "streaming")
2274
2398
  .map((tool) => tool.id));
@@ -2276,6 +2400,10 @@ function normalizeRecoveredSnapshot(snapshot) {
2276
2400
  changed = true;
2277
2401
  recovered.tools = Object.fromEntries(Object.entries(recovered.tools).filter(([id]) => !streamingToolIds.has(id)));
2278
2402
  recovered.facets = Object.fromEntries(Object.entries(recovered.facets).filter(([, facet]) => facet.subject.type !== "tool" || !streamingToolIds.has(facet.subject.id)));
2403
+ recovered.timeline.items = recovered.timeline.items.filter((item) => !(item.type === "tool" && streamingToolIds.has(item.toolId)) &&
2404
+ !(item.type === "notice" &&
2405
+ item.notice.subject.type === "tool" &&
2406
+ streamingToolIds.has(item.notice.subject.id)));
2279
2407
  }
2280
2408
  let recoveredAgentHistory = false;
2281
2409
  recovered.messages = recovered.messages.map((message) => {
@@ -2294,7 +2422,7 @@ function normalizeRecoveredSnapshot(snapshot) {
2294
2422
  if (recoveredAgentHistory) {
2295
2423
  recovered.agentState = {
2296
2424
  revision: recovered.agentState.revision + 1,
2297
- contextEpoch: recovered.agentState.contextEpoch,
2425
+ modelContextKey: recovered.agentState.modelContextKey,
2298
2426
  ...(recovered.agentState.usageCheckpoint
2299
2427
  ? { usageCheckpoint: { ...recovered.agentState.usageCheckpoint } }
2300
2428
  : {}),
@@ -2316,13 +2444,6 @@ function promptCompositionFromSnapshot(snapshot) {
2316
2444
  function cloneSubagentPrompts(subagentPrompts) {
2317
2445
  return { ...subagentPrompts };
2318
2446
  }
2319
- function timelineItemForMessage(messageId) {
2320
- return {
2321
- type: "message",
2322
- id: `timeline-${messageId}`,
2323
- messageId,
2324
- };
2325
- }
2326
2447
  function isCoreMessage(message) {
2327
2448
  switch (message.role) {
2328
2449
  case "user":