@markusylisiurunen/tau 0.3.42 → 0.3.44

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 (90) hide show
  1. package/README.md +9 -11
  2. package/dist/core/agent/agent_runtime.js +55 -52
  3. package/dist/core/agent/agent_runtime.js.map +1 -1
  4. package/dist/core/config/builtin_themes.js +62 -155
  5. package/dist/core/config/builtin_themes.js.map +1 -1
  6. package/dist/core/config/content_loader.js +1 -63
  7. package/dist/core/config/content_loader.js.map +1 -1
  8. package/dist/core/runtime/chat_runtime.js +4 -4
  9. package/dist/core/session/runner.js +21 -14
  10. package/dist/core/session/runner.js.map +1 -1
  11. package/dist/core/static/code_mode/capabilities.mjs +25 -0
  12. package/dist/core/static/code_mode/history/documentation.md +2 -0
  13. package/dist/core/static/code_mode/history/sandbox_runner.mjs +4 -1
  14. package/dist/core/static/code_mode/nook/documentation.md +2 -0
  15. package/dist/core/static/code_mode/nook/sandbox_runner.mjs +4 -1
  16. package/dist/core/static/code_mode/web/documentation.md +2 -0
  17. package/dist/core/static/code_mode/web/sandbox_runner.mjs +4 -1
  18. package/dist/core/subagents/agent_supervisor.js +4 -4
  19. package/dist/core/subagents/agent_supervisor.js.map +1 -1
  20. package/dist/core/subagents/registry.js +5 -9
  21. package/dist/core/subagents/registry.js.map +1 -1
  22. package/dist/core/subagents/types.js.map +1 -1
  23. package/dist/core/telegram/adapter.js +0 -23
  24. package/dist/core/telegram/adapter.js.map +1 -1
  25. package/dist/core/telegram/session_manager.js +34 -7
  26. package/dist/core/telegram/session_manager.js.map +1 -1
  27. package/dist/core/tools/bash.js +9 -2
  28. package/dist/core/tools/bash.js.map +1 -1
  29. package/dist/core/tools/catalog.js +2 -2
  30. package/dist/core/tools/code_mode.js +1 -0
  31. package/dist/core/tools/code_mode.js.map +1 -1
  32. package/dist/core/tools/spawn_agent.js +30 -40
  33. package/dist/core/tools/spawn_agent.js.map +1 -1
  34. package/dist/core/utils/auto_retry.js +1 -1
  35. package/dist/core/utils/auto_retry.js.map +1 -1
  36. package/dist/core/version.js +1 -1
  37. package/dist/host/execution_runtime.js +8 -18
  38. package/dist/host/execution_runtime.js.map +1 -1
  39. package/dist/host/local_session_host.js +446 -216
  40. package/dist/host/local_session_host.js.map +1 -1
  41. package/dist/host/session_host.js.map +1 -1
  42. package/dist/host/session_protocol_handler.js +3 -8
  43. package/dist/host/session_protocol_handler.js.map +1 -1
  44. package/dist/protocol/index.d.ts +2 -2
  45. package/dist/protocol/index.js +1 -1
  46. package/dist/protocol/index.js.map +1 -1
  47. package/dist/protocol/session_protocol.d.ts +127 -40
  48. package/dist/protocol/session_protocol.js +431 -98
  49. package/dist/protocol/session_protocol.js.map +1 -1
  50. package/dist/sdk/index.d.ts +4 -3
  51. package/dist/sdk/index.js +9 -1
  52. package/dist/sdk/index.js.map +1 -1
  53. package/dist/sdk/types.d.ts +2 -1
  54. package/dist/store/session_snapshot_migrations.js +331 -4
  55. package/dist/store/session_snapshot_migrations.js.map +1 -1
  56. package/dist/tui/chat_controller/assistant_clipboard.js +12 -8
  57. package/dist/tui/chat_controller/assistant_clipboard.js.map +1 -1
  58. package/dist/tui/chat_controller/diff_review_service.js +13 -9
  59. package/dist/tui/chat_controller/diff_review_service.js.map +1 -1
  60. package/dist/tui/chat_controller/history_message_model.js +3 -3
  61. package/dist/tui/chat_controller/history_message_model.js.map +1 -1
  62. package/dist/tui/chat_view.js +35 -35
  63. package/dist/tui/chat_view.js.map +1 -1
  64. package/dist/tui/session_chat_app.js +1 -3
  65. package/dist/tui/session_chat_app.js.map +1 -1
  66. package/dist/tui/session_chat_controller.js +373 -239
  67. package/dist/tui/session_chat_controller.js.map +1 -1
  68. package/dist/tui/speech_playback.js +4 -4
  69. package/dist/tui/speech_playback.js.map +1 -1
  70. package/dist/tui/ui/assistant_message.js +1 -15
  71. package/dist/tui/ui/assistant_message.js.map +1 -1
  72. package/dist/tui/ui/chat_message_model.js +26 -7
  73. package/dist/tui/ui/chat_message_model.js.map +1 -1
  74. package/dist/tui/ui/footer.js +100 -66
  75. package/dist/tui/ui/footer.js.map +1 -1
  76. package/dist/tui/ui/theme/palette.js +10 -16
  77. package/dist/tui/ui/theme/palette.js.map +1 -1
  78. package/dist/tui/ui/theme/theme.js.map +1 -1
  79. package/dist/tui/ui/tool_card.js +1 -2
  80. package/dist/tui/ui/tool_card.js.map +1 -1
  81. package/dist/tui/ui/transcript_notice.js +44 -0
  82. package/dist/tui/ui/transcript_notice.js.map +1 -0
  83. package/dist/tui/ui/transcript_text.js +14 -0
  84. package/dist/tui/ui/transcript_text.js.map +1 -0
  85. package/package.json +5 -7
  86. package/dist/sdk/turn_outcome.d.ts +0 -2
  87. package/dist/sdk/turn_outcome.js +0 -4
  88. package/dist/sdk/turn_outcome.js.map +0 -1
  89. package/dist/tui/ui/system_message.js +0 -22
  90. package/dist/tui/ui/system_message.js.map +0 -1
@@ -1,7 +1,10 @@
1
1
  import { z } from "zod";
2
- export const SESSION_PROTOCOL_VERSION = 8;
2
+ export const SESSION_PROTOCOL_VERSION = 10;
3
3
  export const SESSION_PROTOCOL_MAX_EXEC_CAPTURE_BYTES = 24 * 1024 * 1024;
4
4
  export const SESSION_PROTOCOL_MAX_EXEC_STDIN_BYTES = 16 * 1024 * 1024;
5
+ const SESSION_PROTOCOL_NOTICE_TITLE_MAX_CHARS = 512;
6
+ const SESSION_PROTOCOL_NOTICE_CONTENT_MAX_ENTRIES = 16;
7
+ const SESSION_PROTOCOL_NOTICE_CONTENT_ENTRY_MAX_CHARS = 4_096;
5
8
  export const SESSION_PROTOCOL_METHODS = [
6
9
  "initialize",
7
10
  "session.create",
@@ -65,6 +68,22 @@ const SESSION_PROTOCOL_ERROR_CODE_VALUES = [
65
68
  SESSION_PROTOCOL_ERROR_CODES.cancelled,
66
69
  SESSION_PROTOCOL_ERROR_CODES.internalError,
67
70
  ];
71
+ export function projectSessionProtocolNoticePresentation(title, content) {
72
+ const projectedContent = content
73
+ ? content
74
+ .slice(0, SESSION_PROTOCOL_NOTICE_CONTENT_MAX_ENTRIES)
75
+ .map((entry) => truncateNoticeText(entry, SESSION_PROTOCOL_NOTICE_CONTENT_ENTRY_MAX_CHARS))
76
+ : [];
77
+ return {
78
+ title: truncateNoticeText(title, SESSION_PROTOCOL_NOTICE_TITLE_MAX_CHARS),
79
+ ...(projectedContent.length > 0 ? { content: projectedContent } : {}),
80
+ };
81
+ }
82
+ function truncateNoticeText(value, maxChars) {
83
+ if (value.length <= maxChars)
84
+ return value;
85
+ return `${value.slice(0, maxChars - 1)}…`;
86
+ }
68
87
  const sessionProtocolMethodSchema = z.enum(SESSION_PROTOCOL_METHODS);
69
88
  const sessionProtocolMethodListSchema = z
70
89
  .array(sessionProtocolMethodSchema)
@@ -86,6 +105,10 @@ const nonEmptyStringSchema = z
86
105
  .string()
87
106
  .min(1)
88
107
  .refine((value) => value.trim().length > 0);
108
+ const historyEntryIdSchema = nonEmptyStringSchema.transform((value) => value.trim());
109
+ const noticeTitleSchema = nonEmptyStringSchema
110
+ .max(SESSION_PROTOCOL_NOTICE_TITLE_MAX_CHARS)
111
+ .refine((value) => !value.includes("\n") && !value.includes("\r"), "notice title must be one line");
89
112
  const sessionProtocolRequestIdSchema = nonEmptyStringSchema;
90
113
  const nullableSessionProtocolRequestIdSchema = sessionProtocolRequestIdSchema.nullable();
91
114
  const absolutePathSchema = nonEmptyStringSchema.refine((value) => value.startsWith("/"));
@@ -348,7 +371,7 @@ const sessionProtocolUserMessageParamsSchema = z
348
371
  .object({
349
372
  sessionId: nonEmptyStringSchema,
350
373
  text: nonEmptyStringSchema,
351
- historyEntryId: nonEmptyStringSchema.optional(),
374
+ historyEntryId: historyEntryIdSchema.optional(),
352
375
  })
353
376
  .strip();
354
377
  const sessionProtocolStartGoalParamsSchema = z
@@ -462,7 +485,7 @@ const sessionProtocolCompactParamsSchema = z
462
485
  const sessionProtocolRewindParamsSchema = z
463
486
  .object({
464
487
  sessionId: nonEmptyStringSchema,
465
- historyEntryId: nonEmptyStringSchema,
488
+ historyEntryId: historyEntryIdSchema,
466
489
  })
467
490
  .strip();
468
491
  const sessionProtocolInterruptSubagentParamsSchema = z
@@ -753,50 +776,136 @@ const sessionProtocolMessageSchema = z
753
776
  state: z.enum(["draft", "committed", "interrupted", "discarded"]),
754
777
  modelVisible: z.boolean(),
755
778
  message: sessionProtocolMessagePayloadSchema,
756
- turn: sessionProtocolTurnOutcomeSchema.optional(),
757
779
  })
758
780
  .strip();
781
+ const sessionProtocolSubjectSchema = z.discriminatedUnion("type", [
782
+ z.object({ type: z.literal("session") }).strip(),
783
+ z.object({ type: z.literal("message"), id: nonEmptyStringSchema }).strip(),
784
+ z.object({ type: z.literal("tool"), id: nonEmptyStringSchema }).strip(),
785
+ z.object({ type: z.literal("agent"), id: nonEmptyStringSchema }).strip(),
786
+ z.object({ type: z.literal("operation"), id: nonEmptyStringSchema }).strip(),
787
+ ]);
788
+ const timelineNoticeKindSchema = nonEmptyStringSchema.regex(/^[a-z0-9-]+(?:\.[a-z0-9-]+)+$/, "timeline notice kind must be a lowercase dotted identifier");
759
789
  const sessionProtocolNoticeSchema = z
760
790
  .object({
791
+ kind: timelineNoticeKindSchema,
792
+ version: z.number().int().positive(),
761
793
  severity: z.enum(["info", "warn", "error"]),
762
- text: z.string(),
763
- timestamp: z.number().finite(),
794
+ subject: sessionProtocolSubjectSchema,
795
+ presentation: z
796
+ .object({
797
+ title: noticeTitleSchema,
798
+ content: z
799
+ .array(z.string().max(SESSION_PROTOCOL_NOTICE_CONTENT_ENTRY_MAX_CHARS))
800
+ .min(1)
801
+ .max(SESSION_PROTOCOL_NOTICE_CONTENT_MAX_ENTRIES)
802
+ .optional(),
803
+ })
804
+ .strip(),
805
+ data: z.record(z.string(), z.unknown()),
764
806
  })
765
807
  .strip();
766
- const sessionProtocolOperationSchema = z
767
- .object({
808
+ const sessionProtocolOperationBaseSchema = {
809
+ id: nonEmptyStringSchema,
768
810
  kind: z.enum(["auto-compaction", "manual-compaction", "reload", "rewind"]),
769
- status: z.enum(["running", "succeeded", "failed", "cancelled", "skipped"]),
770
811
  startedAt: z.number().finite(),
771
- finishedAt: z.number().finite().optional(),
772
- summary: z.string().optional(),
773
- error: z.string().optional(),
774
- data: z.record(z.string(), z.unknown()).optional(),
775
- })
776
- .strip();
812
+ };
813
+ const sessionProtocolOperationSchema = z.discriminatedUnion("status", [
814
+ z.object({ ...sessionProtocolOperationBaseSchema, status: z.literal("running") }).strip(),
815
+ z
816
+ .object({
817
+ ...sessionProtocolOperationBaseSchema,
818
+ status: z.literal("succeeded"),
819
+ finishedAt: z.number().finite(),
820
+ })
821
+ .strip(),
822
+ z
823
+ .object({
824
+ ...sessionProtocolOperationBaseSchema,
825
+ status: z.literal("failed"),
826
+ finishedAt: z.number().finite(),
827
+ error: nonEmptyStringSchema,
828
+ })
829
+ .strip(),
830
+ z
831
+ .object({
832
+ ...sessionProtocolOperationBaseSchema,
833
+ status: z.literal("skipped"),
834
+ finishedAt: z.number().finite(),
835
+ reason: z.literal("no-eligible-history"),
836
+ })
837
+ .strip(),
838
+ z
839
+ .object({
840
+ ...sessionProtocolOperationBaseSchema,
841
+ status: z.literal("cancelled"),
842
+ finishedAt: z.number().finite(),
843
+ reason: z.enum(["interrupted", "session-recovered"]),
844
+ })
845
+ .strip(),
846
+ ]);
847
+ const sessionProtocolTimelineItemBaseSchema = {
848
+ id: nonEmptyStringSchema,
849
+ sequence: z.number().int().positive(),
850
+ createdAt: z.number().finite().nonnegative(),
851
+ };
777
852
  const sessionProtocolTimelineItemSchema = z.discriminatedUnion("type", [
778
853
  z
779
854
  .object({
855
+ ...sessionProtocolTimelineItemBaseSchema,
780
856
  type: z.literal("message"),
781
- id: nonEmptyStringSchema,
782
857
  messageId: nonEmptyStringSchema,
783
858
  })
784
859
  .strip(),
785
860
  z
786
861
  .object({
862
+ ...sessionProtocolTimelineItemBaseSchema,
863
+ type: z.literal("tool"),
864
+ toolId: nonEmptyStringSchema,
865
+ })
866
+ .strip(),
867
+ z
868
+ .object({
869
+ ...sessionProtocolTimelineItemBaseSchema,
787
870
  type: z.literal("notice"),
788
- id: nonEmptyStringSchema,
789
871
  notice: sessionProtocolNoticeSchema,
790
872
  })
791
873
  .strip(),
792
874
  z
793
875
  .object({
876
+ ...sessionProtocolTimelineItemBaseSchema,
794
877
  type: z.literal("operation"),
795
- id: nonEmptyStringSchema,
796
- operation: sessionProtocolOperationSchema,
878
+ operationId: nonEmptyStringSchema,
797
879
  })
798
880
  .strip(),
799
881
  ]);
882
+ const sessionProtocolTimelineSchema = z
883
+ .object({
884
+ epoch: z.number().int().positive(),
885
+ sequence: z.number().int().nonnegative(),
886
+ items: z.array(sessionProtocolTimelineItemSchema),
887
+ })
888
+ .strip()
889
+ .superRefine((timeline, ctx) => {
890
+ let previousSequence = 0;
891
+ for (const [index, item] of timeline.items.entries()) {
892
+ if (item.sequence <= previousSequence) {
893
+ ctx.addIssue({
894
+ code: "custom",
895
+ path: ["items", index, "sequence"],
896
+ message: "timeline item sequences must be strictly increasing",
897
+ });
898
+ }
899
+ if (item.sequence > timeline.sequence) {
900
+ ctx.addIssue({
901
+ code: "custom",
902
+ path: ["items", index, "sequence"],
903
+ message: "timeline item sequence exceeds the timeline high-water sequence",
904
+ });
905
+ }
906
+ previousSequence = item.sequence;
907
+ }
908
+ });
800
909
  const sessionProtocolToolRunBaseSchema = z.object({
801
910
  id: nonEmptyStringSchema,
802
911
  toolCallId: nonEmptyStringSchema,
@@ -918,17 +1027,10 @@ const sessionProtocolAgentRunSchema = z
918
1027
  });
919
1028
  }
920
1029
  });
921
- const sessionProtocolFacetSubjectSchema = z.discriminatedUnion("type", [
922
- z.object({ type: z.literal("session") }).strip(),
923
- z.object({ type: z.literal("message"), id: nonEmptyStringSchema }).strip(),
924
- z.object({ type: z.literal("tool"), id: nonEmptyStringSchema }).strip(),
925
- z.object({ type: z.literal("agent"), id: nonEmptyStringSchema }).strip(),
926
- z.object({ type: z.literal("operation"), id: nonEmptyStringSchema }).strip(),
927
- ]);
928
1030
  const sessionProtocolFacetSchema = z
929
1031
  .object({
930
1032
  id: nonEmptyStringSchema,
931
- subject: sessionProtocolFacetSubjectSchema,
1033
+ subject: sessionProtocolSubjectSchema,
932
1034
  kind: nonEmptyStringSchema,
933
1035
  version: z.number().int().positive(),
934
1036
  data: z.record(z.string(), z.unknown()),
@@ -943,17 +1045,27 @@ const sessionProtocolGoalSchema = z
943
1045
  const sessionProtocolAgentStateSnapshotSchema = z
944
1046
  .object({
945
1047
  revision: z.number().int().nonnegative(),
946
- contextEpoch: nonEmptyStringSchema,
1048
+ modelContextKey: nonEmptyStringSchema,
947
1049
  usageCheckpoint: z
948
1050
  .object({
949
1051
  historyEntryId: nonEmptyStringSchema,
950
- contextEpoch: nonEmptyStringSchema,
1052
+ modelContextKey: nonEmptyStringSchema,
951
1053
  tokens: z.number().int().nonnegative(),
952
1054
  })
953
1055
  .strip()
954
1056
  .optional(),
955
1057
  })
956
1058
  .strip();
1059
+ const sessionProtocolTurnRecordSchema = z.discriminatedUnion("state", [
1060
+ z.object({ userHistoryEntryId: nonEmptyStringSchema, state: z.literal("running") }).strip(),
1061
+ z
1062
+ .object({
1063
+ userHistoryEntryId: nonEmptyStringSchema,
1064
+ state: z.literal("settled"),
1065
+ outcome: sessionProtocolTurnOutcomeSchema,
1066
+ })
1067
+ .strip(),
1068
+ ]);
957
1069
  const sessionProtocolSnapshotSchema = z
958
1070
  .object({
959
1071
  sessionId: nonEmptyStringSchema,
@@ -969,8 +1081,10 @@ const sessionProtocolSnapshotSchema = z
969
1081
  catalog: sessionProtocolContentCatalogSnapshotSchema,
970
1082
  executionEnvironment: sessionProtocolExecutionEnvironmentSnapshotSchema,
971
1083
  messages: z.array(sessionProtocolMessageSchema),
972
- timeline: z.array(sessionProtocolTimelineItemSchema),
1084
+ turns: z.record(nonEmptyStringSchema, sessionProtocolTurnRecordSchema),
1085
+ timeline: sessionProtocolTimelineSchema,
973
1086
  tools: z.record(nonEmptyStringSchema, sessionProtocolToolRunSchema),
1087
+ operations: z.record(nonEmptyStringSchema, sessionProtocolOperationSchema),
974
1088
  agents: z.record(nonEmptyStringSchema, sessionProtocolAgentRunSchema),
975
1089
  facets: z.record(nonEmptyStringSchema, sessionProtocolFacetSchema),
976
1090
  })
@@ -985,22 +1099,15 @@ const sessionProtocolSnapshotSchema = z
985
1099
  message: `duplicate message id '${message.id}'`,
986
1100
  });
987
1101
  }
988
- if (message.turn && message.message.role !== "user") {
989
- ctx.addIssue({
990
- code: "custom",
991
- path: ["messages"],
992
- message: `turn outcome belongs to non-user message '${message.id}'`,
993
- });
994
- }
995
1102
  messagesById.set(message.id, message);
996
1103
  }
997
1104
  const checkpoint = snapshot.agentState.usageCheckpoint;
998
1105
  if (checkpoint) {
999
- if (checkpoint.contextEpoch !== snapshot.agentState.contextEpoch) {
1106
+ if (checkpoint.modelContextKey !== snapshot.agentState.modelContextKey) {
1000
1107
  ctx.addIssue({
1001
1108
  code: "custom",
1002
- path: ["agentState", "usageCheckpoint", "contextEpoch"],
1003
- message: "usage checkpoint context epoch must match agent state",
1109
+ path: ["agentState", "usageCheckpoint", "modelContextKey"],
1110
+ message: "usage checkpoint model context key must match agent state",
1004
1111
  });
1005
1112
  }
1006
1113
  const checkpointMessage = messagesById.get(checkpoint.historyEntryId);
@@ -1019,29 +1126,109 @@ const sessionProtocolSnapshotSchema = z
1019
1126
  });
1020
1127
  }
1021
1128
  }
1129
+ const subjectExists = (subject) => subject.type === "session" ||
1130
+ (subject.type === "message" && messagesById.has(subject.id)) ||
1131
+ (subject.type === "tool" && snapshot.tools[subject.id] !== undefined) ||
1132
+ (subject.type === "agent" && snapshot.agents[subject.id] !== undefined) ||
1133
+ (subject.type === "operation" && snapshot.operations[subject.id] !== undefined);
1022
1134
  const timelineIds = new Set();
1023
- const operationIds = new Set();
1024
- for (const item of snapshot.timeline) {
1135
+ const timelineSequences = new Set();
1136
+ const timelineToolIds = new Set();
1137
+ const timelineOperationIds = new Set();
1138
+ let previousTimelineSequence = 0;
1139
+ for (const item of snapshot.timeline.items) {
1025
1140
  if (timelineIds.has(item.id)) {
1026
1141
  ctx.addIssue({
1027
1142
  code: "custom",
1028
- path: ["timeline"],
1143
+ path: ["timeline", "items"],
1029
1144
  message: `duplicate timeline item id '${item.id}'`,
1030
1145
  });
1031
1146
  }
1032
1147
  timelineIds.add(item.id);
1148
+ if (timelineSequences.has(item.sequence) ||
1149
+ item.sequence <= previousTimelineSequence ||
1150
+ item.sequence > snapshot.timeline.sequence) {
1151
+ ctx.addIssue({
1152
+ code: "custom",
1153
+ path: ["timeline", "items"],
1154
+ message: `timeline item '${item.id}' has invalid sequence '${item.sequence}'`,
1155
+ });
1156
+ }
1157
+ timelineSequences.add(item.sequence);
1158
+ previousTimelineSequence = item.sequence;
1033
1159
  if (item.type === "message" && !messagesById.has(item.messageId)) {
1034
1160
  ctx.addIssue({
1035
1161
  code: "custom",
1036
- path: ["timeline"],
1162
+ path: ["timeline", "items"],
1037
1163
  message: `timeline message item '${item.id}' references unknown message '${item.messageId}'`,
1038
1164
  });
1039
1165
  }
1166
+ if (item.type === "tool") {
1167
+ if (timelineToolIds.has(item.toolId)) {
1168
+ ctx.addIssue({
1169
+ code: "custom",
1170
+ path: ["timeline", "items"],
1171
+ message: `tool '${item.toolId}' has multiple timeline items`,
1172
+ });
1173
+ }
1174
+ timelineToolIds.add(item.toolId);
1175
+ if (snapshot.tools[item.toolId] === undefined) {
1176
+ ctx.addIssue({
1177
+ code: "custom",
1178
+ path: ["timeline", "items"],
1179
+ message: `timeline tool item '${item.id}' references unknown tool '${item.toolId}'`,
1180
+ });
1181
+ }
1182
+ }
1040
1183
  if (item.type === "operation") {
1041
- operationIds.add(item.id);
1184
+ if (timelineOperationIds.has(item.operationId)) {
1185
+ ctx.addIssue({
1186
+ code: "custom",
1187
+ path: ["timeline", "items"],
1188
+ message: `operation '${item.operationId}' has multiple timeline items`,
1189
+ });
1190
+ }
1191
+ timelineOperationIds.add(item.operationId);
1192
+ if (snapshot.operations[item.operationId] === undefined) {
1193
+ ctx.addIssue({
1194
+ code: "custom",
1195
+ path: ["timeline", "items"],
1196
+ message: `timeline operation item '${item.id}' references unknown operation '${item.operationId}'`,
1197
+ });
1198
+ }
1199
+ }
1200
+ if (item.type === "notice" && !subjectExists(item.notice.subject)) {
1201
+ ctx.addIssue({
1202
+ code: "custom",
1203
+ path: ["timeline", "items"],
1204
+ message: `timeline notice '${item.id}' references an unknown ${item.notice.subject.type} subject`,
1205
+ });
1206
+ }
1207
+ }
1208
+ for (const [id, operation] of Object.entries(snapshot.operations)) {
1209
+ if (!timelineOperationIds.has(id)) {
1210
+ ctx.addIssue({
1211
+ code: "custom",
1212
+ path: ["operations", id],
1213
+ message: `operation '${id}' has no timeline item`,
1214
+ });
1215
+ }
1216
+ if (id !== operation.id) {
1217
+ ctx.addIssue({
1218
+ code: "custom",
1219
+ path: ["operations", id],
1220
+ message: `operation map key '${id}' does not match embedded id '${operation.id}'`,
1221
+ });
1042
1222
  }
1043
1223
  }
1044
1224
  for (const [id, tool] of Object.entries(snapshot.tools)) {
1225
+ if (!timelineToolIds.has(id)) {
1226
+ ctx.addIssue({
1227
+ code: "custom",
1228
+ path: ["tools", id],
1229
+ message: `tool '${id}' has no timeline item`,
1230
+ });
1231
+ }
1045
1232
  if (id !== tool.id || id !== tool.toolCallId) {
1046
1233
  ctx.addIssue({
1047
1234
  code: "custom",
@@ -1119,6 +1306,15 @@ const sessionProtocolSnapshotSchema = z
1119
1306
  }
1120
1307
  }
1121
1308
  }
1309
+ for (const [id, turn] of Object.entries(snapshot.turns)) {
1310
+ if (id !== turn.userHistoryEntryId) {
1311
+ ctx.addIssue({
1312
+ code: "custom",
1313
+ path: ["turns", id],
1314
+ message: `turn map key '${id}' does not match embedded user history entry id '${turn.userHistoryEntryId}'`,
1315
+ });
1316
+ }
1317
+ }
1122
1318
  for (const [id, agent] of Object.entries(snapshot.agents)) {
1123
1319
  if (id !== agent.id) {
1124
1320
  ctx.addIssue({
@@ -1136,12 +1332,7 @@ const sessionProtocolSnapshotSchema = z
1136
1332
  message: `facet map key '${id}' does not match embedded id '${facet.id}'`,
1137
1333
  });
1138
1334
  }
1139
- const subjectExists = facet.subject.type === "session" ||
1140
- (facet.subject.type === "message" && messagesById.has(facet.subject.id)) ||
1141
- (facet.subject.type === "tool" && snapshot.tools[facet.subject.id] !== undefined) ||
1142
- (facet.subject.type === "agent" && snapshot.agents[facet.subject.id] !== undefined) ||
1143
- (facet.subject.type === "operation" && operationIds.has(facet.subject.id));
1144
- if (!subjectExists) {
1335
+ if (!subjectExists(facet.subject)) {
1145
1336
  ctx.addIssue({
1146
1337
  code: "custom",
1147
1338
  path: ["facets", id, "subject"],
@@ -1150,18 +1341,41 @@ const sessionProtocolSnapshotSchema = z
1150
1341
  }
1151
1342
  }
1152
1343
  });
1153
- const sessionProtocolDeltaReasonSchema = z.enum([
1154
- "user-message",
1155
- "assistant-stream",
1156
- "assistant-message",
1157
- "tool-run",
1158
- "tool-result",
1159
- "notice",
1160
- "agent-run",
1161
- "maintenance",
1162
- "configuration",
1163
- "goal",
1164
- "recovery",
1344
+ const sessionProtocolDeltaCauseSchema = z.union([
1345
+ z
1346
+ .object({
1347
+ type: z.enum([
1348
+ "user-message",
1349
+ "assistant-stream",
1350
+ "assistant-message",
1351
+ "tool-run",
1352
+ "tool-result",
1353
+ "notice",
1354
+ "agent-run",
1355
+ "maintenance",
1356
+ "configuration",
1357
+ "goal",
1358
+ ]),
1359
+ })
1360
+ .strip(),
1361
+ z
1362
+ .object({
1363
+ type: z.literal("compaction"),
1364
+ previousEpoch: z.number().int().positive(),
1365
+ epoch: z.number().int().positive(),
1366
+ kind: z.enum(["auto", "manual"]),
1367
+ cutType: z.enum(["turn-boundary", "split-turn"]),
1368
+ retainedMessageCount: z.number().int().nonnegative(),
1369
+ })
1370
+ .strip(),
1371
+ z
1372
+ .object({
1373
+ type: z.literal("rewind"),
1374
+ epoch: z.number().int().positive(),
1375
+ cutoffSequence: z.number().int().nonnegative(),
1376
+ })
1377
+ .strip(),
1378
+ z.object({ type: z.literal("resync") }).strip(),
1165
1379
  ]);
1166
1380
  const sessionProtocolChangeSchema = z.discriminatedUnion("type", [
1167
1381
  z
@@ -1194,11 +1408,16 @@ const sessionProtocolChangeSchema = z.discriminatedUnion("type", [
1194
1408
  settings: sessionProtocolSettingsSnapshotSchema,
1195
1409
  })
1196
1410
  .strip(),
1411
+ z
1412
+ .object({
1413
+ type: z.literal("turn.set"),
1414
+ turn: sessionProtocolTurnRecordSchema,
1415
+ })
1416
+ .strip(),
1197
1417
  z
1198
1418
  .object({
1199
1419
  type: z.literal("message.append"),
1200
1420
  message: sessionProtocolMessageSchema,
1201
- timelineItem: sessionProtocolTimelineItemSchema.optional(),
1202
1421
  })
1203
1422
  .strip(),
1204
1423
  z
@@ -1224,14 +1443,15 @@ const sessionProtocolChangeSchema = z.discriminatedUnion("type", [
1224
1443
  .strip(),
1225
1444
  z
1226
1445
  .object({
1227
- type: z.literal("timeline.replace"),
1228
- item: sessionProtocolTimelineItemSchema,
1446
+ type: z.literal("timeline.remove"),
1447
+ id: nonEmptyStringSchema,
1229
1448
  })
1230
1449
  .strip(),
1231
1450
  z
1232
1451
  .object({
1233
- type: z.literal("timeline.remove"),
1234
- id: nonEmptyStringSchema,
1452
+ type: z.literal("timeline.advance"),
1453
+ epoch: z.number().int().positive(),
1454
+ sequence: z.number().int().positive(),
1235
1455
  })
1236
1456
  .strip(),
1237
1457
  z
@@ -1246,6 +1466,18 @@ const sessionProtocolChangeSchema = z.discriminatedUnion("type", [
1246
1466
  id: nonEmptyStringSchema,
1247
1467
  })
1248
1468
  .strip(),
1469
+ z
1470
+ .object({
1471
+ type: z.literal("operation.set"),
1472
+ operation: sessionProtocolOperationSchema,
1473
+ })
1474
+ .strip(),
1475
+ z
1476
+ .object({
1477
+ type: z.literal("operation.remove"),
1478
+ id: nonEmptyStringSchema,
1479
+ })
1480
+ .strip(),
1249
1481
  z
1250
1482
  .object({
1251
1483
  type: z.literal("agent.set"),
@@ -1292,7 +1524,7 @@ const sessionProtocolDeltaMessageSchema = z
1292
1524
  sessionId: nonEmptyStringSchema,
1293
1525
  fromRevision: z.number().int().positive().nullable(),
1294
1526
  toRevision: z.number().int().positive(),
1295
- reason: sessionProtocolDeltaReasonSchema,
1527
+ cause: sessionProtocolDeltaCauseSchema,
1296
1528
  delta: sessionProtocolDeltaSchema,
1297
1529
  })
1298
1530
  .strip()
@@ -1334,6 +1566,29 @@ const sessionProtocolDeltaMessageSchema = z
1334
1566
  message: "snapshot.reset revision must equal toRevision",
1335
1567
  });
1336
1568
  }
1569
+ if (message.cause.type === "compaction") {
1570
+ if (message.delta.type !== "snapshot.reset" ||
1571
+ message.cause.epoch !== message.cause.previousEpoch + 1 ||
1572
+ message.delta.snapshot.timeline.epoch !== message.cause.epoch) {
1573
+ ctx.addIssue({
1574
+ code: "custom",
1575
+ path: ["cause"],
1576
+ message: "compaction cause must reset the snapshot to the next timeline epoch",
1577
+ });
1578
+ }
1579
+ }
1580
+ if (message.cause.type === "rewind") {
1581
+ const { cutoffSequence, epoch } = message.cause;
1582
+ if (message.delta.type !== "snapshot.reset" ||
1583
+ message.delta.snapshot.timeline.epoch !== epoch ||
1584
+ message.delta.snapshot.timeline.items.some((item) => item.sequence > cutoffSequence)) {
1585
+ ctx.addIssue({
1586
+ code: "custom",
1587
+ path: ["cause"],
1588
+ message: "rewind cause must reset and truncate the active timeline epoch",
1589
+ });
1590
+ }
1591
+ }
1337
1592
  });
1338
1593
  const sessionProtocolEphemeralAgentThreadUpdateEventSchema = z
1339
1594
  .object({
@@ -1349,7 +1604,27 @@ const sessionProtocolEphemeralAgentThreadUpdateEventSchema = z
1349
1604
  .strip(),
1350
1605
  })
1351
1606
  .strip();
1352
- const sessionProtocolEphemeralEventSchema = sessionProtocolEphemeralAgentThreadUpdateEventSchema;
1607
+ const sessionProtocolFeedbackEventSchema = z
1608
+ .object({
1609
+ type: z.literal("feedback.notice"),
1610
+ title: noticeTitleSchema,
1611
+ tone: z.enum(["default", "error"]),
1612
+ presentation: z.literal("footer"),
1613
+ durationMs: z.number().int().positive().max(60_000),
1614
+ })
1615
+ .strip();
1616
+ const sessionProtocolEphemeralTimelineEventSchema = z
1617
+ .object({
1618
+ type: z.literal("timeline.item"),
1619
+ epoch: z.number().int().positive(),
1620
+ item: sessionProtocolTimelineItemSchema.refine((item) => item.type === "notice", "ephemeral timeline items must be notices"),
1621
+ })
1622
+ .strip();
1623
+ const sessionProtocolEphemeralEventSchema = z.union([
1624
+ sessionProtocolEphemeralAgentThreadUpdateEventSchema,
1625
+ sessionProtocolFeedbackEventSchema,
1626
+ sessionProtocolEphemeralTimelineEventSchema,
1627
+ ]);
1353
1628
  const sessionProtocolEphemeralMessageSchema = z
1354
1629
  .object({
1355
1630
  version: z.literal(SESSION_PROTOCOL_VERSION),
@@ -1594,7 +1869,7 @@ export function createSessionProtocolDeltaMessage(options) {
1594
1869
  sessionId: options.sessionId,
1595
1870
  fromRevision: options.fromRevision,
1596
1871
  toRevision: options.toRevision,
1597
- reason: options.reason,
1872
+ cause: options.cause,
1598
1873
  delta: options.delta,
1599
1874
  };
1600
1875
  const parsedMessage = sessionProtocolDeltaMessageSchema.safeParse(message);
@@ -1629,11 +1904,33 @@ export function createSessionProtocolPendingUserMessagesMessage(options) {
1629
1904
  }
1630
1905
  return parsedMessage.data;
1631
1906
  }
1907
+ function validateSnapshotResetCause(snapshot, message) {
1908
+ if (message.delta.type !== "snapshot.reset") {
1909
+ return;
1910
+ }
1911
+ if (message.cause.type === "compaction" &&
1912
+ message.cause.previousEpoch !== snapshot.timeline.epoch) {
1913
+ throw new Error(`compaction delta previous epoch ${message.cause.previousEpoch} does not match current timeline epoch ${snapshot.timeline.epoch}`);
1914
+ }
1915
+ if (message.cause.type !== "rewind") {
1916
+ return;
1917
+ }
1918
+ if (message.cause.epoch !== snapshot.timeline.epoch) {
1919
+ throw new Error(`rewind delta epoch ${message.cause.epoch} does not match current timeline epoch ${snapshot.timeline.epoch}`);
1920
+ }
1921
+ if (message.cause.cutoffSequence > snapshot.timeline.sequence) {
1922
+ throw new Error(`rewind delta cutoff sequence ${message.cause.cutoffSequence} exceeds current timeline sequence ${snapshot.timeline.sequence}`);
1923
+ }
1924
+ if (message.delta.snapshot.timeline.sequence !== snapshot.timeline.sequence) {
1925
+ throw new Error(`rewind delta timeline sequence ${message.delta.snapshot.timeline.sequence} does not preserve current timeline sequence ${snapshot.timeline.sequence}`);
1926
+ }
1927
+ }
1632
1928
  export function applySessionProtocolDelta(snapshot, message) {
1633
1929
  if (message.sessionId !== snapshot.sessionId) {
1634
1930
  throw new Error(`session delta targets '${message.sessionId}' but current snapshot is '${snapshot.sessionId}'`);
1635
1931
  }
1636
1932
  if (message.delta.type === "snapshot.reset") {
1933
+ validateSnapshotResetCause(snapshot, message);
1637
1934
  return structuredClone(message.delta.snapshot);
1638
1935
  }
1639
1936
  if (message.fromRevision !== snapshot.revision) {
@@ -1667,11 +1964,11 @@ export function applySessionProtocolDelta(snapshot, message) {
1667
1964
  case "settings.set":
1668
1965
  next.settings = structuredClone(change.settings);
1669
1966
  break;
1967
+ case "turn.set":
1968
+ setSessionProtocolTurn(next.turns, change.turn);
1969
+ break;
1670
1970
  case "message.append":
1671
1971
  next.messages.push(structuredClone(change.message));
1672
- if (change.timelineItem) {
1673
- next.timeline.push(structuredClone(change.timelineItem));
1674
- }
1675
1972
  break;
1676
1973
  case "message.replace": {
1677
1974
  const index = next.messages.findIndex((entry) => entry.id === change.message.id);
@@ -1688,20 +1985,23 @@ export function applySessionProtocolDelta(snapshot, message) {
1688
1985
  break;
1689
1986
  }
1690
1987
  case "timeline.append":
1691
- next.timeline.push(structuredClone(change.item));
1692
- break;
1693
- case "timeline.replace": {
1694
- const index = next.timeline.findIndex((item) => item.id === change.item.id);
1695
- if (index === -1) {
1696
- next.timeline.push(structuredClone(change.item));
1697
- }
1698
- else {
1699
- next.timeline[index] = structuredClone(change.item);
1988
+ if (change.item.sequence <= next.timeline.sequence) {
1989
+ throw new Error(`timeline append sequence ${change.item.sequence} must exceed current timeline sequence ${next.timeline.sequence}`);
1700
1990
  }
1991
+ next.timeline.items.push(structuredClone(change.item));
1992
+ next.timeline.sequence = change.item.sequence;
1701
1993
  break;
1702
- }
1703
1994
  case "timeline.remove":
1704
- next.timeline = next.timeline.filter((item) => item.id !== change.id);
1995
+ next.timeline.items = next.timeline.items.filter((item) => item.id !== change.id);
1996
+ break;
1997
+ case "timeline.advance":
1998
+ if (next.timeline.epoch !== change.epoch) {
1999
+ throw new Error("timeline advance epoch does not match the active epoch");
2000
+ }
2001
+ if (change.sequence <= next.timeline.sequence) {
2002
+ throw new Error(`timeline advance sequence ${change.sequence} must exceed current timeline sequence ${next.timeline.sequence}`);
2003
+ }
2004
+ next.timeline.sequence = change.sequence;
1705
2005
  break;
1706
2006
  case "tool.set":
1707
2007
  next.tools[change.tool.id] = structuredClone(change.tool);
@@ -1709,6 +2009,12 @@ export function applySessionProtocolDelta(snapshot, message) {
1709
2009
  case "tool.remove":
1710
2010
  delete next.tools[change.id];
1711
2011
  break;
2012
+ case "operation.set":
2013
+ next.operations[change.operation.id] = structuredClone(change.operation);
2014
+ break;
2015
+ case "operation.remove":
2016
+ delete next.operations[change.id];
2017
+ break;
1712
2018
  case "agent.set":
1713
2019
  next.agents[change.agent.id] = structuredClone(change.agent);
1714
2020
  break;
@@ -1741,6 +2047,7 @@ function changeCanInvalidateSnapshot(snapshot, change) {
1741
2047
  case "goal.set":
1742
2048
  case "cost.set":
1743
2049
  case "settings.set":
2050
+ case "turn.set":
1744
2051
  case "agent.set":
1745
2052
  return false;
1746
2053
  case "tool.set": {
@@ -1751,17 +2058,16 @@ function changeCanInvalidateSnapshot(snapshot, change) {
1751
2058
  const previous = snapshot.facets[change.facet.id];
1752
2059
  return !previous || !hasSameFacetSubject(previous, change.facet);
1753
2060
  }
1754
- case "timeline.replace": {
1755
- const previous = snapshot.timeline.find((item) => item.id === change.item.id);
1756
- return !previous || !hasSameTimelineReferences(previous, change.item);
1757
- }
1758
2061
  case "agent-state.set":
1759
2062
  case "message.append":
1760
2063
  case "message.replace":
1761
2064
  case "message.content.append":
1762
2065
  case "timeline.append":
1763
2066
  case "timeline.remove":
2067
+ case "timeline.advance":
1764
2068
  case "tool.remove":
2069
+ case "operation.set":
2070
+ case "operation.remove":
1765
2071
  case "agent.remove":
1766
2072
  case "facet.remove":
1767
2073
  return true;
@@ -1791,25 +2097,41 @@ function hasSameFacetSubject(previous, facet) {
1791
2097
  (previous.subject.type === "session" ||
1792
2098
  (facet.subject.type !== "session" && previous.subject.id === facet.subject.id)));
1793
2099
  }
1794
- function hasSameTimelineReferences(previous, item) {
1795
- if (previous.type !== item.type)
1796
- return false;
1797
- if (previous.type === "message" && item.type === "message") {
1798
- return previous.messageId === item.messageId;
2100
+ function setSessionProtocolTurn(turns, turn) {
2101
+ const previous = Object.hasOwn(turns, turn.userHistoryEntryId)
2102
+ ? turns[turn.userHistoryEntryId]
2103
+ : undefined;
2104
+ if (previous?.state === "settled" && JSON.stringify(previous) !== JSON.stringify(turn)) {
2105
+ throw new Error(`settled turn '${turn.userHistoryEntryId}' cannot be changed`);
1799
2106
  }
1800
- return true;
2107
+ Object.defineProperty(turns, turn.userHistoryEntryId, {
2108
+ configurable: true,
2109
+ enumerable: true,
2110
+ value: structuredClone(turn),
2111
+ writable: true,
2112
+ });
1801
2113
  }
1802
2114
  function applyKeyedRecordDelta(snapshot, message) {
1803
2115
  if (message.delta.type !== "snapshot.patch") {
1804
2116
  return undefined;
1805
2117
  }
2118
+ let nextTurns;
1806
2119
  let nextTools;
2120
+ let nextOperations;
1807
2121
  let nextAgents;
1808
2122
  let nextFacets;
2123
+ const cloneTurns = () => {
2124
+ nextTurns ??= { ...snapshot.turns };
2125
+ return nextTurns;
2126
+ };
1809
2127
  const cloneTools = () => {
1810
2128
  nextTools ??= { ...snapshot.tools };
1811
2129
  return nextTools;
1812
2130
  };
2131
+ const cloneOperations = () => {
2132
+ nextOperations ??= { ...snapshot.operations };
2133
+ return nextOperations;
2134
+ };
1813
2135
  const cloneAgents = () => {
1814
2136
  nextAgents ??= { ...snapshot.agents };
1815
2137
  return nextAgents;
@@ -1820,12 +2142,21 @@ function applyKeyedRecordDelta(snapshot, message) {
1820
2142
  };
1821
2143
  for (const change of message.delta.changes) {
1822
2144
  switch (change.type) {
2145
+ case "turn.set":
2146
+ setSessionProtocolTurn(cloneTurns(), change.turn);
2147
+ break;
1823
2148
  case "tool.set":
1824
2149
  cloneTools()[change.tool.id] = structuredClone(change.tool);
1825
2150
  break;
1826
2151
  case "tool.remove":
1827
2152
  delete cloneTools()[change.id];
1828
2153
  break;
2154
+ case "operation.set":
2155
+ cloneOperations()[change.operation.id] = structuredClone(change.operation);
2156
+ break;
2157
+ case "operation.remove":
2158
+ delete cloneOperations()[change.id];
2159
+ break;
1829
2160
  case "agent.set":
1830
2161
  cloneAgents()[change.agent.id] = structuredClone(change.agent);
1831
2162
  break;
@@ -1845,7 +2176,9 @@ function applyKeyedRecordDelta(snapshot, message) {
1845
2176
  return {
1846
2177
  ...snapshot,
1847
2178
  revision: message.toRevision,
2179
+ ...(nextTurns ? { turns: nextTurns } : {}),
1848
2180
  ...(nextTools ? { tools: nextTools } : {}),
2181
+ ...(nextOperations ? { operations: nextOperations } : {}),
1849
2182
  ...(nextAgents ? { agents: nextAgents } : {}),
1850
2183
  ...(nextFacets ? { facets: nextFacets } : {}),
1851
2184
  };
@@ -2730,8 +3063,8 @@ function parseSessionProtocolDeltaMessage(payload) {
2730
3063
  if (hasIssue(deltaMessage.error, ["toRevision"])) {
2731
3064
  return fail("session.delta.toRevision must be a positive integer");
2732
3065
  }
2733
- if (hasIssue(deltaMessage.error, ["reason"])) {
2734
- return fail("session.delta.reason is invalid");
3066
+ if (hasIssue(deltaMessage.error, ["cause"])) {
3067
+ return fail("session.delta.cause is invalid");
2735
3068
  }
2736
3069
  if (hasIssue(deltaMessage.error, ["delta"])) {
2737
3070
  return fail("session.delta.delta is invalid");
@@ -2746,7 +3079,7 @@ function parseSessionProtocolDeltaMessage(payload) {
2746
3079
  sessionId: deltaMessage.data.sessionId,
2747
3080
  fromRevision: deltaMessage.data.fromRevision,
2748
3081
  toRevision: deltaMessage.data.toRevision,
2749
- reason: deltaMessage.data.reason,
3082
+ cause: deltaMessage.data.cause,
2750
3083
  delta: deltaMessage.data.delta,
2751
3084
  },
2752
3085
  };