agentel 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/archive.js CHANGED
@@ -11,6 +11,16 @@ const { loadRedactionConfig, loadEnvValues, mergeSummaries, redactText, styleRed
11
11
  const { ensureBaseDirs, ensureDir, paths, readJson, writeJson } = require("./paths");
12
12
 
13
13
  const SHARED_RAW_SOURCE_CACHE = new Map();
14
+ const ESTIMATED_TOKEN_CHARS = 4;
15
+ const MESSAGE_TOKEN_ESTIMATE_METHOD = "chars-per-token-v1";
16
+ const CHAT_ESTIMATED_USAGE_METHOD = "visible-message-chars-v1";
17
+ const CURSOR_ESTIMATED_MAX_INPUT_PER_TURN = 200000;
18
+ const CURSOR_ESTIMATED_TOKEN_RATES = {
19
+ opus: { inputPerAssistantTurn: 11000, outputPerAssistantTurn: 240 },
20
+ sonnet: { inputPerAssistantTurn: 10000, outputPerAssistantTurn: 120 },
21
+ unknown: { inputPerAssistantTurn: 6900, outputPerAssistantTurn: 270 },
22
+ default: { inputPerAssistantTurn: 8400, outputPerAssistantTurn: 230 }
23
+ };
14
24
 
15
25
  function archiveRoot(env = process.env) {
16
26
  const cfg = loadConfig(env);
@@ -59,7 +69,7 @@ function writeSession(input, env = process.env) {
59
69
  const sessionId = input.sessionId || stableSessionId(provider, input.sourcePath || "", startedAt, messages);
60
70
  const repoCanonical = input.scopeCanonical ? "" : input.repoCanonical || repoInfo.key;
61
71
  const redactionConfig = loadRedactionConfig(env);
62
- const envValues = loadEnvValues(redactionCwd, redactionConfig.envVars, env);
72
+ const envValues = redactionConfig.enabled ? loadEnvValues(redactionCwd, redactionConfig.envVars, env) : [];
63
73
  const redactionSummary = {};
64
74
  const redactedMessages = messages.map((message) => {
65
75
  const result = redactText(message.content, {
@@ -83,6 +93,17 @@ function writeSession(input, env = process.env) {
83
93
  })
84
94
  };
85
95
  });
96
+ const archivedMessages = redactedMessages.map(withMessageTokenEstimate);
97
+ const sessionSummary = input.sessionSummary && typeof input.sessionSummary === "object"
98
+ ? redactStructuredStrings(input.sessionSummary, {
99
+ config: redactionConfig,
100
+ cwd: redactionCwd,
101
+ env,
102
+ envValues,
103
+ repoCanonical,
104
+ summary: redactionSummary
105
+ })
106
+ : null;
86
107
 
87
108
  const session = {
88
109
  version: 1,
@@ -115,10 +136,16 @@ function writeSession(input, env = process.env) {
115
136
  "chatVirtualRepo",
116
137
  "chatDisplayPath",
117
138
  "conversationKind",
118
- "pinned"
139
+ "pinned",
140
+ "timeStatus",
141
+ "composerId"
119
142
  ]) {
120
143
  if (input[key] !== undefined) session[key] = input[key];
121
144
  }
145
+ if (sessionSummary && Object.keys(sessionSummary).length) session.sessionSummary = sessionSummary;
146
+ if (session.composerId === undefined && typeof input.id === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(input.id)) {
147
+ session.composerId = input.id;
148
+ }
122
149
 
123
150
  const dir = objectPathForSession(session, env);
124
151
  ensureDir(dir);
@@ -129,7 +156,7 @@ function writeSession(input, env = process.env) {
129
156
  const viewPath = path.join(dir, `session=${sessionId}.view.json`);
130
157
  const rawPath = path.join(dir, `session=${sessionId}.raw`);
131
158
  const events = redactCanonicalEvents(
132
- Array.isArray(input.events) ? normalizePrecomputedEvents(input.events, session) : normalizeSessionEvents(session, redactedMessages),
159
+ Array.isArray(input.events) ? normalizePrecomputedEvents(input.events, session) : normalizeSessionEvents(session, archivedMessages),
133
160
  {
134
161
  config: redactionConfig,
135
162
  cwd: redactionCwd,
@@ -147,21 +174,25 @@ function writeSession(input, env = process.env) {
147
174
  session.rawFiles = rawFiles;
148
175
  session.rawFileCount = rawFiles.length;
149
176
  }
150
- fs.writeFileSync(conversationPath, renderConversationMarkdown(session, redactedMessages, events), { mode: 0o600 });
151
- fs.writeFileSync(transcriptPath, redactedMessages.map((message) => `${JSON.stringify(message)}\n`).join(""), {
177
+ fs.writeFileSync(conversationPath, renderConversationMarkdown(session, archivedMessages, events), { mode: 0o600 });
178
+ fs.writeFileSync(transcriptPath, archivedMessages.map((message) => `${JSON.stringify(message)}\n`).join(""), {
152
179
  mode: 0o600
153
180
  });
154
181
  fs.writeFileSync(eventPath, events.map((event) => `${JSON.stringify(event)}\n`).join(""), { mode: 0o600 });
155
- writeSessionView(viewPath, computeSessionView(session, redactedMessages, events));
182
+ writeSessionView(viewPath, computeSessionView(session, archivedMessages, events));
156
183
  session.conversationPath = conversationPath;
157
184
  session.transcriptPath = transcriptPath;
158
185
  session.eventPath = eventPath;
159
186
  session.viewPath = viewPath;
160
187
  session.viewSchemaVersion = VIEW_SCHEMA_VERSION;
161
- const usageStats = computeSessionUsage(redactedMessages);
188
+ const usageStats = mergeSessionUsage(computeSessionUsage(archivedMessages), sessionSummaryUsage(session.sessionSummary));
162
189
  if (usageStats) session.usage = usageStats;
163
- const models = collectSessionModels(redactedMessages);
190
+ const models = collectSessionModels(archivedMessages, session.sessionSummary);
191
+ const estimatedUsage = usageStats ? null : estimateSessionUsage(archivedMessages, { provider, sourceType, models });
192
+ if (estimatedUsage) session.estimatedUsage = estimatedUsage;
164
193
  if (models.length) session.models = models;
194
+ const commandTypeCounts = collectCursorCommandTypeCounts(archivedMessages);
195
+ if (commandTypeCounts) session.cursorCommandTypeCounts = commandTypeCounts;
165
196
  writeJson(metadataPath, session);
166
197
 
167
198
  if (cfg.privacy.revealCache && !input.scopeCanonical) {
@@ -578,6 +609,9 @@ function computeSessionUsage(messages) {
578
609
  let totalOutputTokens = 0;
579
610
  const seenUsageRequests = new Set();
580
611
  let any = false;
612
+ let estimatedUsageCount = 0;
613
+ let actualUsageCount = 0;
614
+ const estimationMethods = new Set();
581
615
  for (const message of messages || []) {
582
616
  const usage = message && message.metadata && message.metadata.usage;
583
617
  if (!usage || typeof usage !== "object") continue;
@@ -588,6 +622,12 @@ function computeSessionUsage(messages) {
588
622
  }
589
623
  const summary = usageTokenSummary(usage);
590
624
  if (!summary.hasAny) continue;
625
+ if (summary.estimated) {
626
+ estimatedUsageCount++;
627
+ if (summary.estimationMethod) estimationMethods.add(summary.estimationMethod);
628
+ } else {
629
+ actualUsageCount++;
630
+ }
591
631
  if (summary.inputTokens > 0) {
592
632
  inputTokens += summary.inputTokens;
593
633
  any = true;
@@ -607,15 +647,17 @@ function computeSessionUsage(messages) {
607
647
  }
608
648
  if (Number.isFinite(totalInputTokens) && totalInputTokens > 0) any = true;
609
649
  if (Number.isFinite(totalOutputTokens) && totalOutputTokens > 0) any = true;
650
+ if (reasoningOutputTokens > 0 || toolUsePromptTokens > 0) any = true;
610
651
  if (!any && maxUndifferentiatedTotalTokens > 0) {
611
652
  inputTokens = maxUndifferentiatedTotalTokens;
612
653
  any = true;
613
654
  }
614
655
  const cumulativeTokens = totalInputTokens + totalOutputTokens;
615
- const finalTotalTokens = inputTokens + outputTokens || cumulativeTokens || maxUndifferentiatedTotalTokens;
656
+ const splitTotalTokens = inputTokens + outputTokens + reasoningOutputTokens + toolUsePromptTokens;
657
+ const finalTotalTokens = splitTotalTokens || cumulativeTokens || maxUndifferentiatedTotalTokens;
616
658
  if (!any && finalTotalTokens === 0) return null;
617
- return {
618
- inputTokens: inputTokens || totalInputTokens || (!outputTokens && finalTotalTokens ? finalTotalTokens : 0),
659
+ const result = {
660
+ inputTokens: inputTokens || totalInputTokens || (!outputTokens && !reasoningOutputTokens && !toolUsePromptTokens && maxUndifferentiatedTotalTokens ? maxUndifferentiatedTotalTokens : 0),
619
661
  outputTokens: outputTokens || totalOutputTokens,
620
662
  cacheInputTokens,
621
663
  reasoningOutputTokens,
@@ -624,10 +666,148 @@ function computeSessionUsage(messages) {
624
666
  totalInputTokens: totalInputTokens || inputTokens,
625
667
  totalOutputTokens: totalOutputTokens || outputTokens
626
668
  };
669
+ if (estimatedUsageCount > 0 && actualUsageCount === 0) {
670
+ result.estimated = true;
671
+ if (estimationMethods.size === 1) result.estimationMethod = Array.from(estimationMethods)[0];
672
+ }
673
+ return result;
674
+ }
675
+
676
+ function estimateSessionUsage(messages, options = {}) {
677
+ const provider = String(options.provider || "").toLowerCase();
678
+ if (provider === "cursor") return estimateCursorSessionUsage(messages, options);
679
+ if (isWebChatProvider(provider, options.sourceType)) return estimateVisibleMessageUsage(messages);
680
+ return null;
681
+ }
682
+
683
+ function estimateCursorSessionUsage(messages, options = {}) {
684
+ let contextTokens = 0;
685
+ let visibleContextInputTokens = 0;
686
+ let visibleOutputTokens = 0;
687
+ let assistantTurns = 0;
688
+ for (const message of messages || []) {
689
+ const messageTokens = estimateMessageTokens(message);
690
+ if (message?.role === "assistant") {
691
+ if (messageTokens > 0 || contextTokens > 0) {
692
+ visibleContextInputTokens += Math.min(contextTokens, CURSOR_ESTIMATED_MAX_INPUT_PER_TURN);
693
+ visibleOutputTokens += messageTokens;
694
+ assistantTurns++;
695
+ }
696
+ }
697
+ contextTokens += messageTokens;
698
+ }
699
+ if (!assistantTurns) return null;
700
+ const family = cursorEstimatedModelFamily(options);
701
+ const rates = cursorEstimatedTokenRates(family, options);
702
+ const empiricalInputTokens = rates.inputPerAssistantTurn * assistantTurns;
703
+ const empiricalOutputTokens = rates.outputPerAssistantTurn * assistantTurns;
704
+ const inputTokens = Math.max(empiricalInputTokens, visibleContextInputTokens);
705
+ const outputTokens = Math.max(empiricalOutputTokens, visibleOutputTokens);
706
+ if (inputTokens + outputTokens <= 0) return null;
707
+ return {
708
+ inputTokens,
709
+ outputTokens,
710
+ totalTokens: inputTokens + outputTokens,
711
+ estimated: true,
712
+ estimationMethod: "cursor-empirical-turns-v1",
713
+ estimatedModelFamily: family,
714
+ estimatedAssistantTurns: assistantTurns
715
+ };
716
+ }
717
+
718
+ function estimateVisibleMessageUsage(messages) {
719
+ let inputTokens = 0;
720
+ let outputTokens = 0;
721
+ let messageCount = 0;
722
+ for (const message of messages || []) {
723
+ const tokens = messageTokenEstimate(message);
724
+ if (!tokens) continue;
725
+ if (message?.role === "assistant") outputTokens += tokens;
726
+ else inputTokens += tokens;
727
+ messageCount++;
728
+ }
729
+ if (inputTokens + outputTokens <= 0) return null;
730
+ return {
731
+ inputTokens,
732
+ outputTokens,
733
+ totalTokens: inputTokens + outputTokens,
734
+ estimated: true,
735
+ estimationMethod: CHAT_ESTIMATED_USAGE_METHOD,
736
+ estimatedMessageCount: messageCount
737
+ };
738
+ }
739
+
740
+ function isWebChatProvider(provider, sourceType = "") {
741
+ if (provider === "chatgpt" || provider === "claude_web") return true;
742
+ return /^(web-chat-export|chatgpt-export|claude-web-export|claude-web-memory)$/.test(String(sourceType || ""));
743
+ }
744
+
745
+ function cursorEstimatedModelFamily(options = {}) {
746
+ const models = Array.isArray(options.models) ? options.models.join(" ").toLowerCase() : "";
747
+ if (models.includes("opus")) return "opus";
748
+ if (models.includes("sonnet")) return "sonnet";
749
+ if (models.includes("gpt") || models.includes("gemini") || models.includes("claude")) return "default";
750
+ if (String(options.sourceType || "") === "cursor-agent-transcripts") return "default";
751
+ return "unknown";
752
+ }
753
+
754
+ function cursorEstimatedTokenRates(family) {
755
+ return CURSOR_ESTIMATED_TOKEN_RATES[family] || CURSOR_ESTIMATED_TOKEN_RATES.default;
756
+ }
757
+
758
+ function estimateMessageTokens(message) {
759
+ const parts = [];
760
+ if (message?.content) parts.push(String(message.content));
761
+ const toolCalls = Array.isArray(message?.metadata?.toolCalls) ? message.metadata.toolCalls : [];
762
+ for (const call of toolCalls) {
763
+ const summary = {
764
+ name: call?.name || call?.displayName,
765
+ target: call?.target,
766
+ argument: call?.argument || call?.rawInputSummary || call?.inputPreview,
767
+ arguments: call?.arguments
768
+ };
769
+ parts.push(JSON.stringify(summary));
770
+ }
771
+ const text = parts.filter(Boolean).join("\n");
772
+ if (!text.trim()) return 0;
773
+ return Math.max(1, Math.ceil(text.length / ESTIMATED_TOKEN_CHARS));
774
+ }
775
+
776
+ function withMessageTokenEstimate(message) {
777
+ const tokens = estimateMessageTokens(message);
778
+ if (!tokens) return message;
779
+ const existing = message?.metadata?.tokenEstimate;
780
+ if (existing && typeof existing === "object" && positiveTokenNumber(existing.tokens)) return message;
781
+ return {
782
+ ...message,
783
+ metadata: {
784
+ ...(message.metadata || {}),
785
+ tokenEstimate: {
786
+ tokens,
787
+ estimated: true,
788
+ method: MESSAGE_TOKEN_ESTIMATE_METHOD,
789
+ charsPerToken: ESTIMATED_TOKEN_CHARS
790
+ }
791
+ }
792
+ };
793
+ }
794
+
795
+ function messageTokenEstimate(message) {
796
+ const estimate = message?.metadata?.tokenEstimate || message?.metadata?.token_estimate;
797
+ if (estimate && typeof estimate === "object") {
798
+ return positiveTokenNumber(firstUsageNumber(estimate.tokens, estimate.tokenCount, estimate.token_count, estimate.totalTokens, estimate.total_tokens));
799
+ }
800
+ return estimateMessageTokens(message);
627
801
  }
628
802
 
629
803
  function usageTokenSummary(usage) {
630
804
  if (!usage || typeof usage !== "object") return emptyUsageTokenSummary();
805
+ const estimated = usage.estimated === true || usage.estimated === "true";
806
+ const estimationMethod = typeof usage.estimationMethod === "string" && usage.estimationMethod.trim()
807
+ ? usage.estimationMethod.trim()
808
+ : typeof usage.estimation_method === "string" && usage.estimation_method.trim()
809
+ ? usage.estimation_method.trim()
810
+ : "";
631
811
  const inputTokens = positiveTokenNumber(firstUsageNumber(
632
812
  usage.inputTokens,
633
813
  usage.input_tokens,
@@ -716,7 +896,9 @@ function usageTokenSummary(usage) {
716
896
  hasSplitTokens: splitTokens > 0,
717
897
  hasExtraTokens: extraTokens > 0,
718
898
  hasTotalTokens: explicitTotalTokens > 0,
719
- hasCumulativeTokens: cumulativeTokens > 0
899
+ hasCumulativeTokens: cumulativeTokens > 0,
900
+ estimated,
901
+ estimationMethod
720
902
  };
721
903
  }
722
904
 
@@ -735,7 +917,9 @@ function emptyUsageTokenSummary() {
735
917
  hasSplitTokens: false,
736
918
  hasExtraTokens: false,
737
919
  hasTotalTokens: false,
738
- hasCumulativeTokens: false
920
+ hasCumulativeTokens: false,
921
+ estimated: false,
922
+ estimationMethod: ""
739
923
  };
740
924
  }
741
925
 
@@ -757,15 +941,76 @@ function sumPositiveTokenNumbers(...values) {
757
941
  return values.reduce((sum, value) => sum + positiveTokenNumber(value), 0);
758
942
  }
759
943
 
760
- function collectSessionModels(messages) {
944
+ function sessionSummaryUsage(sessionSummary) {
945
+ const usage = sessionSummary?.usage;
946
+ if (!usage || typeof usage !== "object") return null;
947
+ const inputTokens = positiveTokenNumber(firstUsageNumber(usage.inputTokens, usage.input_tokens, usage.promptTokens, usage.prompt_tokens));
948
+ const outputTokens = positiveTokenNumber(firstUsageNumber(usage.outputTokens, usage.output_tokens, usage.completionTokens, usage.completion_tokens));
949
+ const cacheInputTokens = sumPositiveTokenNumbers(
950
+ firstUsageNumber(usage.cacheInputTokens, usage.cache_input_tokens),
951
+ firstUsageNumber(usage.cacheReadTokens, usage.cache_read_tokens)
952
+ );
953
+ const totalTokens = positiveTokenNumber(firstUsageNumber(usage.totalTokens, usage.total_tokens, usage.totalTokenCount, usage.total_token_count)) || inputTokens + outputTokens;
954
+ if (!inputTokens && !outputTokens && !cacheInputTokens && !totalTokens) return null;
955
+ return {
956
+ inputTokens,
957
+ outputTokens,
958
+ cacheInputTokens,
959
+ reasoningOutputTokens: 0,
960
+ toolUsePromptTokens: 0,
961
+ totalTokens,
962
+ totalInputTokens: inputTokens,
963
+ totalOutputTokens: outputTokens
964
+ };
965
+ }
966
+
967
+ function mergeSessionUsage(primary, secondary) {
968
+ if (!primary && !secondary) return null;
969
+ if (!primary) return secondary;
970
+ if (!secondary) return primary;
971
+ const result = {
972
+ inputTokens: primary.inputTokens || secondary.inputTokens || 0,
973
+ outputTokens: primary.outputTokens || secondary.outputTokens || 0,
974
+ cacheInputTokens: primary.cacheInputTokens || secondary.cacheInputTokens || 0,
975
+ reasoningOutputTokens: primary.reasoningOutputTokens || secondary.reasoningOutputTokens || 0,
976
+ toolUsePromptTokens: primary.toolUsePromptTokens || secondary.toolUsePromptTokens || 0,
977
+ totalTokens: Math.max(primary.totalTokens || 0, secondary.totalTokens || 0),
978
+ totalInputTokens: primary.totalInputTokens || secondary.totalInputTokens || primary.inputTokens || secondary.inputTokens || 0,
979
+ totalOutputTokens: primary.totalOutputTokens || secondary.totalOutputTokens || primary.outputTokens || secondary.outputTokens || 0
980
+ };
981
+ if (primary.estimated && secondary.estimated) {
982
+ result.estimated = true;
983
+ if (primary.estimationMethod && primary.estimationMethod === secondary.estimationMethod) {
984
+ result.estimationMethod = primary.estimationMethod;
985
+ }
986
+ }
987
+ return result;
988
+ }
989
+
990
+ function collectSessionModels(messages, sessionSummary = null) {
761
991
  const models = new Set();
762
992
  for (const message of messages || []) {
763
993
  const model = message && message.metadata && message.metadata.model;
764
994
  if (typeof model === "string" && model.trim()) models.add(model.trim());
765
995
  }
996
+ for (const item of sessionSummary?.modelUsage || []) {
997
+ if (typeof item?.model === "string" && item.model.trim()) models.add(item.model.trim());
998
+ }
766
999
  return Array.from(models);
767
1000
  }
768
1001
 
1002
+ function collectCursorCommandTypeCounts(messages) {
1003
+ const counts = {};
1004
+ for (const message of messages || []) {
1005
+ if (message?.role !== "user") continue;
1006
+ const value = message?.metadata?.commandType;
1007
+ if (typeof value !== "string" || !value.trim()) continue;
1008
+ const key = value.trim().toLowerCase();
1009
+ counts[key] = (counts[key] || 0) + 1;
1010
+ }
1011
+ return Object.keys(counts).length ? counts : null;
1012
+ }
1013
+
769
1014
  function readTranscript(file) {
770
1015
  const messages = [];
771
1016
  try {
@@ -814,7 +1059,7 @@ function ensureConversationMarkdown(session, env = process.env) {
814
1059
  return conversationPath;
815
1060
  }
816
1061
 
817
- const VIEW_SCHEMA_VERSION = 1;
1062
+ const VIEW_SCHEMA_VERSION = 2;
818
1063
 
819
1064
  function sessionViewPathFromMetadata(metadataPath) {
820
1065
  if (!metadataPath) return "";
@@ -881,6 +1126,13 @@ function cursorRawSyntheticTimeParts(session) {
881
1126
  }
882
1127
 
883
1128
  function sessionHistoryTime(session) {
1129
+ const explicitStatus = session?.timeStatus || session?.time_status || "";
1130
+ if (explicitStatus === "recovered-time-unknown") {
1131
+ return { startedAt: "", endedAt: "", status: "recovered-time-unknown" };
1132
+ }
1133
+ if (explicitStatus === "recovered-partial-time") {
1134
+ return { startedAt: session?.startedAt || "", endedAt: "", status: "recovered-partial-time" };
1135
+ }
884
1136
  const rawTime = cursorRawSyntheticTimeParts(session);
885
1137
  if (rawTime.startSynthetic && rawTime.endSynthetic) {
886
1138
  return { startedAt: "", endedAt: "", status: "recovered-time-unknown" };
@@ -900,6 +1152,7 @@ function computeSessionView(session, transcriptMessages, canonicalEvents) {
900
1152
  started_at: time.startedAt || "",
901
1153
  ended_at: time.endedAt || "",
902
1154
  time_status: time.status || "",
1155
+ session_summary: session?.sessionSummary || undefined,
903
1156
  transcript_messages: display,
904
1157
  canonical_events: Array.isArray(canonicalEvents) ? canonicalEvents : []
905
1158
  };
@@ -939,9 +1192,11 @@ function ensureSessionView(session, env = process.env) {
939
1192
 
940
1193
  function renderConversationMarkdown(session, messages, events = []) {
941
1194
  const title = session.title || `${session.provider || "agent"} session ${session.sessionId}`;
1195
+ const time = sessionHistoryTime(session);
1196
+ const displayMessages = displayTranscriptMessages(messages, time);
942
1197
  const hasRedactions =
943
- messages.some((message) => /\[REDACTED:[^\]\n]+\]/.test(JSON.stringify(message))) ||
944
- events.some((event) => /\[REDACTED:[^\]\n]+\]/.test(JSON.stringify(event)));
1198
+ displayMessages.some((message) => /\[REDACTED(?::|\s+)[^\]\n]+\]/.test(JSON.stringify(message))) ||
1199
+ events.some((event) => /\[REDACTED(?::|\s+)[^\]\n]+\]/.test(JSON.stringify(event)));
945
1200
  const frontmatter = [
946
1201
  "---",
947
1202
  `session_id: ${yamlString(session.sessionId)}`,
@@ -949,14 +1204,15 @@ function renderConversationMarkdown(session, messages, events = []) {
949
1204
  `repo: ${yamlString(session.repoCanonical || "")}`,
950
1205
  `scope: ${yamlString(session.scopeCanonical || "")}`,
951
1206
  `cwd: ${yamlString(session.cwd || "")}`,
952
- `started_at: ${yamlString(session.startedAt || "")}`,
953
- `ended_at: ${yamlString(session.endedAt || "")}`,
1207
+ `started_at: ${yamlString(time.startedAt || "")}`,
1208
+ `ended_at: ${yamlString(time.endedAt || "")}`,
954
1209
  `source_type: ${yamlString(session.sourceType || "")}`,
955
1210
  `storage_scope: ${yamlString(session.storageScope || "")}`,
956
1211
  `chat_account_id: ${yamlString(session.chatAccountId || "")}`,
957
1212
  `chat_display_name: ${yamlString(session.chatDisplayName || "")}`,
958
1213
  `chat_project_path: ${yamlString(session.chatProjectPath || "")}`,
959
1214
  `conversation_kind: ${yamlString(session.conversationKind || "")}`,
1215
+ `time_status: ${yamlString(time.status || session.timeStatus || "")}`,
960
1216
  `archive_schema_version: ${yamlString(session.archiveSchemaVersion || 1)}`,
961
1217
  `event_count: ${yamlString(events.length || session.eventCount || 0)}`,
962
1218
  `parser_versions: ${yamlString(JSON.stringify(session.parserVersions || {}))}`,
@@ -974,7 +1230,7 @@ function renderConversationMarkdown(session, messages, events = []) {
974
1230
  ""
975
1231
  );
976
1232
  }
977
- for (const message of messages) {
1233
+ for (const message of displayMessages) {
978
1234
  body.push(`## ${roleLabel(message.role)} - ${message.timestamp || "unknown time"}`);
979
1235
  body.push("");
980
1236
  body.push(styleRedactionMarkersForMarkdown(String(message.content || "").trim()));
@@ -1108,6 +1364,8 @@ module.exports = {
1108
1364
  deleteSessionArchive,
1109
1365
  displayTranscriptMessages,
1110
1366
  encodeSegment,
1367
+ estimateMessageTokens,
1368
+ estimateSessionUsage,
1111
1369
  ensureConversationMarkdown,
1112
1370
  ensureSessionView,
1113
1371
  findSessionById,