@agentproto/runtime 1.1.0 → 2.1.0

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.
@@ -93,6 +93,12 @@ var init_tool_presenter = __esm({
93
93
  };
94
94
  }
95
95
  });
96
+
97
+ // src/tool-call-record.ts
98
+ var init_tool_call_record = __esm({
99
+ "src/tool-call-record.ts"() {
100
+ }
101
+ });
96
102
  function sessionTranscriptDir(sessionId, baseDir) {
97
103
  return join(join(homedir(), ".agentproto", "sessions"), sessionId);
98
104
  }
@@ -101,6 +107,7 @@ function sessionEventsPath(sessionId, baseDir) {
101
107
  }
102
108
  var init_transcript_writer = __esm({
103
109
  "src/transcript-writer.ts"() {
110
+ init_tool_call_record();
104
111
  }
105
112
  });
106
113
 
@@ -108,10 +115,19 @@ var init_transcript_writer = __esm({
108
115
  var transcript_export_exports = {};
109
116
  __export(transcript_export_exports, {
110
117
  crossValidateHermesExport: () => crossValidateHermesExport,
118
+ discoverCodexSessions: () => discoverCodexSessions,
111
119
  discoverHermesSessions: () => discoverHermesSessions,
120
+ discoverMastracodeInprocessSessions: () => discoverMastracodeInprocessSessions,
121
+ discoverOpenCodeSessions: () => discoverOpenCodeSessions,
122
+ discoverPiSessions: () => discoverPiSessions,
112
123
  exportAgentSession: () => exportAgentSession,
113
124
  exportClaudeCodeSession: () => exportClaudeCodeSession,
125
+ exportCodexSession: () => exportCodexSession,
126
+ exportDaemonEventsSession: () => exportDaemonEventsSession,
114
127
  exportHermesSession: () => exportHermesSession,
128
+ exportMastracodeInprocessSession: () => exportMastracodeInprocessSession,
129
+ exportOpenCodeSession: () => exportOpenCodeSession,
130
+ exportPiSession: () => exportPiSession,
115
131
  renderJson: () => renderJson,
116
132
  renderMarkdown: () => renderMarkdown
117
133
  });
@@ -127,7 +143,9 @@ function renderMarkdown(session, opts = {}) {
127
143
  out.push(`# ${meta.title ?? "(untitled)"}`);
128
144
  out.push("");
129
145
  const sourceNote = meta.source ? ` \xB7 source \`${meta.source}\`` : "";
130
- out.push(`> Session${sourceNote}`);
146
+ const originNote = meta.origin ? ` \xB7 origin \`${meta.origin}\`` : "";
147
+ const callerNote = meta.callerSessionId ? ` \xB7 caller \`${meta.callerSessionId}\`` : "";
148
+ out.push(`> Session${sourceNote}${originNote}${callerNote}`);
131
149
  out.push("");
132
150
  out.push("| | |");
133
151
  out.push("|---|---|");
@@ -338,6 +356,27 @@ function withRetryOnBusy(fn) {
338
356
  throw err;
339
357
  }
340
358
  }
359
+ async function openReadonlySqlite(dbPath, label) {
360
+ let DatabaseSync;
361
+ try {
362
+ const sqlite = await import('sqlite');
363
+ DatabaseSync = sqlite.DatabaseSync;
364
+ } catch {
365
+ throw new Error(`${label}: node:sqlite unavailable. Requires Node.js \u226522.5.0.`);
366
+ }
367
+ try {
368
+ return new DatabaseSync(dbPath, { readOnly: true });
369
+ } catch (err) {
370
+ const msg = String(err);
371
+ if (err.code === "ENOENT" || msg.includes("unable to open database file")) {
372
+ throw new Error(`${label}: database not found at ${dbPath}. Has ${label} been run at least once?`);
373
+ }
374
+ if (msg.includes("SQLITE_BUSY") || msg.includes("database is locked")) {
375
+ throw new Error(`${label}: database is locked (SQLITE_BUSY). It may be writing. Try again in a moment.`);
376
+ }
377
+ throw err;
378
+ }
379
+ }
341
380
  async function exportHermesSession(adapterSessionId) {
342
381
  const dbPath = join(homedir(), ".hermes", "state.db");
343
382
  const db = await openHermesDb(dbPath);
@@ -620,6 +659,10 @@ Either the session never drove an agent-cli turn, or it predates this feature.`
620
659
  case "turn-end":
621
660
  flushAssistant();
622
661
  break;
662
+ case "notice":
663
+ flushAssistant();
664
+ messages.push({ role: "system", text: rec.text ?? "" });
665
+ break;
623
666
  }
624
667
  }
625
668
  flushAssistant();
@@ -628,6 +671,8 @@ Either the session never drove an agent-cli turn, or it predates this feature.`
628
671
  if (desc?.model) meta.model = desc.model;
629
672
  if (desc?.startedAt) meta.startedAt = desc.startedAt;
630
673
  if (desc?.endedAt) meta.endedAt = desc.endedAt;
674
+ if (desc?.origin) meta.origin = desc.origin;
675
+ if (desc?.callerSessionId) meta.callerSessionId = desc.callerSessionId;
631
676
  meta.messageCount = messages.length;
632
677
  meta.toolCallCount = toolCallCount;
633
678
  if (desc?.costUsd !== void 0) meta.costUsd = desc.costUsd;
@@ -640,6 +685,629 @@ Either the session never drove an agent-cli turn, or it predates this feature.`
640
685
  }
641
686
  return { meta, messages };
642
687
  }
688
+ async function readFirstJsonLine(path) {
689
+ const stream = createReadStream(path, { encoding: "utf8" });
690
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
691
+ try {
692
+ for await (const line of rl) {
693
+ const t = line.trim();
694
+ if (t) return t;
695
+ }
696
+ return void 0;
697
+ } finally {
698
+ rl.close();
699
+ stream.destroy();
700
+ }
701
+ }
702
+ function codexHome() {
703
+ return process.env.CODEX_HOME ?? join(homedir(), ".codex");
704
+ }
705
+ function codexSessionsDir() {
706
+ return join(codexHome(), "sessions");
707
+ }
708
+ async function walkCodexRollouts(dir) {
709
+ let entries;
710
+ try {
711
+ entries = await promises.readdir(dir, { withFileTypes: true });
712
+ } catch {
713
+ return [];
714
+ }
715
+ const out = [];
716
+ for (const e of entries) {
717
+ const p = join(dir, e.name);
718
+ if (e.isDirectory()) {
719
+ out.push(...await walkCodexRollouts(p));
720
+ } else if (e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")) {
721
+ out.push(p);
722
+ }
723
+ }
724
+ return out;
725
+ }
726
+ function codexBlocksText(content) {
727
+ if (typeof content === "string") return content;
728
+ if (!Array.isArray(content)) return "";
729
+ let acc = "";
730
+ for (const c of content) {
731
+ if (c && typeof c === "object") {
732
+ const b = c;
733
+ if (typeof b.text === "string" && (b.type === "input_text" || b.type === "output_text" || b.type === "text")) {
734
+ acc += b.text;
735
+ }
736
+ }
737
+ }
738
+ return acc;
739
+ }
740
+ async function findCodexRolloutFile(conversationId) {
741
+ const files = await walkCodexRollouts(codexSessionsDir());
742
+ const byName = files.find((f) => f.endsWith(`-${conversationId}.jsonl`));
743
+ if (byName) return byName;
744
+ for (const f of files) {
745
+ const first = await readFirstJsonLine(f);
746
+ if (!first) continue;
747
+ let meta;
748
+ try {
749
+ meta = JSON.parse(first);
750
+ } catch {
751
+ continue;
752
+ }
753
+ const p = meta.payload;
754
+ if (p && (p.id === conversationId || p.session_id === conversationId)) return f;
755
+ }
756
+ return void 0;
757
+ }
758
+ async function exportCodexSession(conversationId) {
759
+ const file = await findCodexRolloutFile(conversationId);
760
+ if (!file) {
761
+ throw new Error(
762
+ `codex: no rollout file for conversation "${conversationId}" under ${codexSessionsDir()}.
763
+ The session may predate persistence, or CODEX_HOME points elsewhere.`
764
+ );
765
+ }
766
+ const stream = createReadStream(file, { encoding: "utf8" });
767
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
768
+ const messages = [];
769
+ const toolNameByCall = /* @__PURE__ */ new Map();
770
+ let toolCallCount = 0;
771
+ let startedAt;
772
+ let model;
773
+ for await (const line of rl) {
774
+ const trimmed = line.trim();
775
+ if (!trimmed) continue;
776
+ let entry;
777
+ try {
778
+ entry = JSON.parse(trimmed);
779
+ } catch {
780
+ continue;
781
+ }
782
+ const p = entry.payload ?? {};
783
+ if (entry.type === "session_meta") {
784
+ startedAt = p.timestamp ?? entry.timestamp;
785
+ if (typeof p.model === "string") model = p.model;
786
+ continue;
787
+ }
788
+ if (entry.type === "turn_context") {
789
+ if (!model && typeof p.model === "string") model = p.model;
790
+ continue;
791
+ }
792
+ if (entry.type !== "response_item") continue;
793
+ switch (p.type) {
794
+ case "message": {
795
+ const text = codexBlocksText(p.content).trim();
796
+ if (!text) break;
797
+ const role = p.role === "assistant" ? "assistant" : p.role === "user" ? "user" : "system";
798
+ messages.push({ role, text });
799
+ break;
800
+ }
801
+ case "reasoning": {
802
+ const r = (p.summary ?? []).map((s) => s.text ?? "").join("\n").trim();
803
+ if (r) messages.push({ role: "assistant", reasoning: r });
804
+ break;
805
+ }
806
+ case "function_call":
807
+ case "custom_tool_call": {
808
+ const name = p.name ?? "tool";
809
+ if (typeof p.call_id === "string") toolNameByCall.set(p.call_id, name);
810
+ const raw = p.type === "custom_tool_call" ? p.input : p.arguments;
811
+ const args = typeof raw === "string" ? raw : JSON.stringify(raw ?? {});
812
+ messages.push({ role: "assistant", toolCalls: [{ name, args }] });
813
+ toolCallCount += 1;
814
+ break;
815
+ }
816
+ case "function_call_output":
817
+ case "custom_tool_call_output": {
818
+ const name = typeof p.call_id === "string" ? toolNameByCall.get(p.call_id) : void 0;
819
+ const out = codexBlocksText(p.output) || (typeof p.output === "string" ? p.output : "");
820
+ messages.push({ role: "tool", text: out, ...name ? { toolName: name } : {} });
821
+ break;
822
+ }
823
+ }
824
+ }
825
+ const meta = { source: "codex" };
826
+ if (startedAt) meta.startedAt = startedAt;
827
+ if (model) meta.model = model;
828
+ meta.messageCount = messages.length;
829
+ meta.toolCallCount = toolCallCount;
830
+ return { meta, messages };
831
+ }
832
+ async function discoverCodexSessions(cwd, since, until, expectedId) {
833
+ const files = await walkCodexRollouts(codexSessionsDir());
834
+ const sinceMs = since ? Date.parse(since) : NaN;
835
+ const untilMs = until ? Date.parse(until) : NaN;
836
+ const scored = [];
837
+ for (const f of files) {
838
+ let mtimeMs;
839
+ try {
840
+ mtimeMs = (await promises.stat(f)).mtimeMs;
841
+ } catch {
842
+ continue;
843
+ }
844
+ if (Number.isFinite(sinceMs) && mtimeMs < sinceMs - 1e3) continue;
845
+ const first = await readFirstJsonLine(f);
846
+ if (!first) continue;
847
+ let meta;
848
+ try {
849
+ meta = JSON.parse(first);
850
+ } catch {
851
+ continue;
852
+ }
853
+ if (meta.type !== "session_meta") continue;
854
+ const p = meta.payload ?? {};
855
+ const id = p.id ?? p.session_id;
856
+ if (!id) continue;
857
+ if (expectedId) {
858
+ if (id !== expectedId) continue;
859
+ } else if (p.cwd !== cwd) {
860
+ continue;
861
+ }
862
+ const startedAt = p.timestamp;
863
+ if (Number.isFinite(untilMs) && startedAt) {
864
+ const startedMs = Date.parse(startedAt);
865
+ if (Number.isFinite(startedMs) && startedMs > untilMs) continue;
866
+ }
867
+ scored.push({
868
+ candidate: {
869
+ conversationId: id,
870
+ ...startedAt ? { startedAt } : {},
871
+ lastActivityAt: new Date(mtimeMs).toISOString()
872
+ },
873
+ mtimeMs
874
+ });
875
+ }
876
+ scored.sort((a, b) => b.mtimeMs - a.mtimeMs);
877
+ return scored.map((s) => s.candidate);
878
+ }
879
+ function openCodeDbPath() {
880
+ const dataHome = process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
881
+ return join(dataHome, "opencode", "opencode.db");
882
+ }
883
+ function openCodeModelLabel(raw) {
884
+ if (!raw) return void 0;
885
+ try {
886
+ const m = JSON.parse(raw);
887
+ const id = m.modelID ?? m.id;
888
+ if (!id) return void 0;
889
+ return m.providerID ? `${m.providerID}/${id}` : id;
890
+ } catch {
891
+ return void 0;
892
+ }
893
+ }
894
+ async function exportOpenCodeSession(conversationId) {
895
+ const dbPath = openCodeDbPath();
896
+ const db = await openReadonlySqlite(dbPath, "opencode");
897
+ try {
898
+ const session = withRetryOnBusy(
899
+ () => db.prepare("SELECT * FROM session WHERE id = ?").get(conversationId)
900
+ );
901
+ if (!session) {
902
+ throw new Error(
903
+ `opencode: session "${conversationId}" not found in ${dbPath}. Sessions are keyed by the ACP session id (ses_\u2026) recorded as adapterSessionId.`
904
+ );
905
+ }
906
+ const msgRows = withRetryOnBusy(
907
+ () => db.prepare("SELECT id, data, time_created FROM message WHERE session_id = ? ORDER BY time_created ASC, id ASC").all(conversationId)
908
+ );
909
+ const partRows = withRetryOnBusy(
910
+ () => db.prepare("SELECT message_id, data, time_created FROM part WHERE session_id = ? ORDER BY time_created ASC, id ASC").all(conversationId)
911
+ );
912
+ const partsByMsg = /* @__PURE__ */ new Map();
913
+ for (const r of partRows) {
914
+ const list = partsByMsg.get(r.message_id) ?? [];
915
+ list.push(r);
916
+ partsByMsg.set(r.message_id, list);
917
+ }
918
+ const messages = [];
919
+ let toolCallCount = 0;
920
+ for (const m of msgRows) {
921
+ let mdata;
922
+ try {
923
+ mdata = JSON.parse(m.data);
924
+ } catch {
925
+ continue;
926
+ }
927
+ const role = mdata.role === "assistant" ? "assistant" : "user";
928
+ let text = "";
929
+ let reasoning = "";
930
+ const toolCalls = [];
931
+ const toolResults = [];
932
+ for (const pr of partsByMsg.get(m.id) ?? []) {
933
+ let part;
934
+ try {
935
+ part = JSON.parse(pr.data);
936
+ } catch {
937
+ continue;
938
+ }
939
+ switch (part.type) {
940
+ case "text":
941
+ if (part.text) text += part.text;
942
+ break;
943
+ case "reasoning":
944
+ if (part.text) reasoning += part.text;
945
+ break;
946
+ case "tool": {
947
+ const name = part.tool ?? "tool";
948
+ toolCalls.push({ name, args: JSON.stringify(part.state?.input ?? {}) });
949
+ toolCallCount += 1;
950
+ const output = part.state?.output;
951
+ if (output !== void 0 && output !== null && output !== "") {
952
+ toolResults.push({ name, text: typeof output === "string" ? output : JSON.stringify(output) });
953
+ }
954
+ break;
955
+ }
956
+ }
957
+ }
958
+ if (text.trim() || reasoning.trim() || toolCalls.length) {
959
+ const em = { role };
960
+ if (text.trim()) em.text = text.trim();
961
+ if (reasoning.trim()) em.reasoning = reasoning.trim();
962
+ if (toolCalls.length) em.toolCalls = toolCalls;
963
+ em.ts = m.time_created;
964
+ messages.push(em);
965
+ }
966
+ for (const tr of toolResults) {
967
+ messages.push({ role: "tool", text: tr.text, ...tr.name ? { toolName: tr.name } : {} });
968
+ }
969
+ }
970
+ const meta = { source: "opencode" };
971
+ if (session.title) meta.title = session.title;
972
+ const model = openCodeModelLabel(session.model);
973
+ if (model) meta.model = model;
974
+ if (session.time_created) meta.startedAt = new Date(session.time_created).toISOString();
975
+ if (session.time_updated) meta.endedAt = new Date(session.time_updated).toISOString();
976
+ meta.messageCount = messages.length;
977
+ meta.toolCallCount = toolCallCount;
978
+ if (session.cost != null) meta.costUsd = Number(session.cost);
979
+ const tk = {
980
+ ...session.tokens_input != null ? { input: session.tokens_input } : {},
981
+ ...session.tokens_output != null ? { output: session.tokens_output } : {},
982
+ ...session.tokens_cache_read != null ? { cacheRead: session.tokens_cache_read } : {},
983
+ ...session.tokens_cache_write != null ? { cacheWrite: session.tokens_cache_write } : {},
984
+ ...session.tokens_reasoning != null ? { reasoning: session.tokens_reasoning } : {}
985
+ };
986
+ if (Object.keys(tk).length) meta.tokens = tk;
987
+ return { meta, messages };
988
+ } finally {
989
+ db.close();
990
+ }
991
+ }
992
+ async function discoverOpenCodeSessions(cwd, since, expectedId) {
993
+ const dbPath = openCodeDbPath();
994
+ let db;
995
+ try {
996
+ db = await openReadonlySqlite(dbPath, "opencode");
997
+ } catch (err) {
998
+ const msg = err instanceof Error ? err.message : String(err);
999
+ if (msg.includes("database not found")) return [];
1000
+ throw err;
1001
+ }
1002
+ try {
1003
+ if (expectedId) {
1004
+ const row = withRetryOnBusy(
1005
+ () => db.prepare("SELECT * FROM session WHERE id = ?").get(expectedId)
1006
+ );
1007
+ return row ? [openCodeRowToCandidate(row)] : [];
1008
+ }
1009
+ const rows = withRetryOnBusy(
1010
+ () => db.prepare("SELECT * FROM session WHERE directory = ?").all(cwd)
1011
+ );
1012
+ const sinceMs = since ? Date.parse(since) : NaN;
1013
+ return rows.filter((r) => {
1014
+ if (!Number.isFinite(sinceMs)) return true;
1015
+ const last = r.time_updated ?? r.time_created;
1016
+ return last === void 0 || last >= sinceMs - 1e3;
1017
+ }).map(openCodeRowToCandidate);
1018
+ } finally {
1019
+ db.close();
1020
+ }
1021
+ }
1022
+ function openCodeRowToCandidate(row) {
1023
+ return {
1024
+ conversationId: row.id,
1025
+ ...row.time_created ? { startedAt: new Date(row.time_created).toISOString() } : {},
1026
+ ...row.time_updated ? { lastActivityAt: new Date(row.time_updated).toISOString() } : {},
1027
+ ...row.title ? { preview: row.title } : {}
1028
+ };
1029
+ }
1030
+ function mastracodeInprocessDbPath() {
1031
+ const home = process.env.AGENTPROTO_HOME ?? join(homedir(), ".agentproto");
1032
+ return join(home, "mastracode-inprocess", "storage.db");
1033
+ }
1034
+ function mastraThreadId(conversationId) {
1035
+ const idx = conversationId.indexOf(":");
1036
+ return idx > 0 ? conversationId.slice(idx + 1) : conversationId;
1037
+ }
1038
+ function mastraContentToMessage(role, content) {
1039
+ const toolResults = [];
1040
+ let parsed;
1041
+ try {
1042
+ parsed = JSON.parse(content);
1043
+ } catch {
1044
+ return { message: void 0, toolResults, toolCalls: 0 };
1045
+ }
1046
+ let text = "";
1047
+ let reasoning = "";
1048
+ const toolCalls = [];
1049
+ for (const raw of parsed.parts ?? []) {
1050
+ if (!raw || typeof raw !== "object") continue;
1051
+ const part = raw;
1052
+ if (part.type === "text" && part.text) text += part.text;
1053
+ else if (part.type === "reasoning" && part.reasoning) reasoning += part.reasoning;
1054
+ else if (part.type === "tool-invocation" && part.toolInvocation) {
1055
+ const inv = part.toolInvocation;
1056
+ const name = inv.toolName ?? "tool";
1057
+ toolCalls.push({ name, args: JSON.stringify(inv.args ?? {}) });
1058
+ if (inv.result !== void 0) {
1059
+ const r = inv.result;
1060
+ const rtext = Array.isArray(r?.content) ? r.content.filter((c) => c?.type === "text").map((c) => c.text ?? "").join("") : typeof inv.result === "string" ? inv.result : JSON.stringify(inv.result);
1061
+ toolResults.push({ name, text: rtext });
1062
+ }
1063
+ }
1064
+ }
1065
+ let message;
1066
+ if (text.trim() || reasoning.trim() || toolCalls.length) {
1067
+ message = { role };
1068
+ if (text.trim()) message.text = text.trim();
1069
+ if (reasoning.trim()) message.reasoning = reasoning.trim();
1070
+ if (toolCalls.length) message.toolCalls = toolCalls;
1071
+ }
1072
+ return { message, toolResults, toolCalls: toolCalls.length };
1073
+ }
1074
+ async function exportMastracodeInprocessSession(conversationId) {
1075
+ const threadId = mastraThreadId(conversationId);
1076
+ const dbPath = mastracodeInprocessDbPath();
1077
+ const db = await openReadonlySqlite(dbPath, "mastracode-inprocess");
1078
+ try {
1079
+ const thread = withRetryOnBusy(
1080
+ () => db.prepare("SELECT * FROM mastra_threads WHERE id = ?").get(threadId)
1081
+ );
1082
+ if (!thread) {
1083
+ throw new Error(
1084
+ `mastracode-inprocess: thread "${threadId}" not found in ${dbPath}. The sessionId is "<resourceId>:<threadId>"; only the threadId is looked up.`
1085
+ );
1086
+ }
1087
+ const rows = withRetryOnBusy(
1088
+ () => db.prepare('SELECT role, content, createdAt FROM mastra_messages WHERE thread_id = ? ORDER BY "createdAt" ASC').all(threadId)
1089
+ );
1090
+ const messages = [];
1091
+ let toolCallCount = 0;
1092
+ for (const r of rows) {
1093
+ const role = r.role === "assistant" ? "assistant" : "user";
1094
+ const { message, toolResults, toolCalls } = mastraContentToMessage(role, r.content);
1095
+ if (message) messages.push(message);
1096
+ toolCallCount += toolCalls;
1097
+ for (const tr of toolResults) {
1098
+ messages.push({ role: "tool", text: tr.text, ...tr.name ? { toolName: tr.name } : {} });
1099
+ }
1100
+ }
1101
+ const meta = { source: "mastracode-inprocess" };
1102
+ if (thread.title) meta.title = thread.title;
1103
+ if (thread.createdAt) meta.startedAt = thread.createdAt;
1104
+ if (thread.updatedAt) meta.endedAt = thread.updatedAt;
1105
+ meta.messageCount = messages.length;
1106
+ meta.toolCallCount = toolCallCount;
1107
+ return { meta, messages };
1108
+ } finally {
1109
+ db.close();
1110
+ }
1111
+ }
1112
+ async function discoverMastracodeInprocessSessions(_cwd, since, expectedId) {
1113
+ const dbPath = mastracodeInprocessDbPath();
1114
+ let db;
1115
+ try {
1116
+ db = await openReadonlySqlite(dbPath, "mastracode-inprocess");
1117
+ } catch (err) {
1118
+ const msg = err instanceof Error ? err.message : String(err);
1119
+ if (msg.includes("database not found")) return [];
1120
+ throw err;
1121
+ }
1122
+ try {
1123
+ if (!expectedId) return [];
1124
+ const threadId = mastraThreadId(expectedId);
1125
+ const thread = withRetryOnBusy(
1126
+ () => db.prepare("SELECT * FROM mastra_threads WHERE id = ?").get(threadId)
1127
+ );
1128
+ if (!thread) return [];
1129
+ const sinceMs = since ? Date.parse(since) : NaN;
1130
+ if (Number.isFinite(sinceMs) && thread.updatedAt) {
1131
+ const upd = Date.parse(thread.updatedAt);
1132
+ if (Number.isFinite(upd) && upd < sinceMs - 1e3) return [];
1133
+ }
1134
+ return [
1135
+ {
1136
+ // Echo back the full composite id so callers/read() round-trip it.
1137
+ conversationId: expectedId,
1138
+ ...thread.createdAt ? { startedAt: thread.createdAt } : {},
1139
+ ...thread.updatedAt ? { lastActivityAt: thread.updatedAt } : {},
1140
+ ...thread.title ? { preview: thread.title } : {}
1141
+ }
1142
+ ];
1143
+ } finally {
1144
+ db.close();
1145
+ }
1146
+ }
1147
+ function piSessionsDir() {
1148
+ return join(homedir(), ".pi", "agent", "sessions");
1149
+ }
1150
+ function piBlocksText(content) {
1151
+ if (typeof content === "string") return content;
1152
+ if (!Array.isArray(content)) return "";
1153
+ let acc = "";
1154
+ for (const c of content) {
1155
+ if (c && typeof c === "object") {
1156
+ const b = c;
1157
+ if (b.type === "text" && typeof b.text === "string") acc += b.text;
1158
+ }
1159
+ }
1160
+ return acc;
1161
+ }
1162
+ async function walkPiSessionFiles(dir) {
1163
+ let entries;
1164
+ try {
1165
+ entries = await promises.readdir(dir, { withFileTypes: true });
1166
+ } catch {
1167
+ return [];
1168
+ }
1169
+ const out = [];
1170
+ for (const e of entries) {
1171
+ const p = join(dir, e.name);
1172
+ if (e.isDirectory()) out.push(...await walkPiSessionFiles(p));
1173
+ else if (e.isFile() && e.name.endsWith(".jsonl")) out.push(p);
1174
+ }
1175
+ return out;
1176
+ }
1177
+ async function findPiSessionFile(conversationId) {
1178
+ const files = await walkPiSessionFiles(piSessionsDir());
1179
+ const byName = files.find((f) => f.endsWith(`_${conversationId}.jsonl`));
1180
+ if (byName) return byName;
1181
+ for (const f of files) {
1182
+ const first = await readFirstJsonLine(f);
1183
+ if (!first) continue;
1184
+ try {
1185
+ const meta = JSON.parse(first);
1186
+ if (meta.type === "session" && meta.id === conversationId) return f;
1187
+ } catch {
1188
+ }
1189
+ }
1190
+ return void 0;
1191
+ }
1192
+ async function exportPiSession(conversationId) {
1193
+ const file = await findPiSessionFile(conversationId);
1194
+ if (!file) {
1195
+ throw new Error(
1196
+ `pi: no session file for conversation "${conversationId}" under ${piSessionsDir()}.`
1197
+ );
1198
+ }
1199
+ const stream = createReadStream(file, { encoding: "utf8" });
1200
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
1201
+ const messages = [];
1202
+ let toolCallCount = 0;
1203
+ let startedAt;
1204
+ let model;
1205
+ for await (const line of rl) {
1206
+ const trimmed = line.trim();
1207
+ if (!trimmed) continue;
1208
+ let entry;
1209
+ try {
1210
+ entry = JSON.parse(trimmed);
1211
+ } catch {
1212
+ continue;
1213
+ }
1214
+ if (entry.type === "session") {
1215
+ startedAt = entry.timestamp;
1216
+ continue;
1217
+ }
1218
+ if (entry.type === "model_change") {
1219
+ model = entry.provider ? `${entry.provider}/${entry.modelId ?? ""}` : entry.modelId;
1220
+ continue;
1221
+ }
1222
+ if (entry.type !== "message" || !entry.message) continue;
1223
+ const m = entry.message;
1224
+ const content = m.content;
1225
+ if (m.role === "toolResult") {
1226
+ const text2 = piBlocksText(content);
1227
+ messages.push({
1228
+ role: "tool",
1229
+ text: m.isError ? `[error] ${text2}` : text2,
1230
+ ...m.toolName ? { toolName: m.toolName } : {}
1231
+ });
1232
+ continue;
1233
+ }
1234
+ const role = m.role === "assistant" ? "assistant" : "user";
1235
+ let text = "";
1236
+ let reasoning = "";
1237
+ const toolCalls = [];
1238
+ if (typeof content === "string") {
1239
+ text = content;
1240
+ } else if (Array.isArray(content)) {
1241
+ for (const raw of content) {
1242
+ if (!raw || typeof raw !== "object") continue;
1243
+ const part = raw;
1244
+ if (part.type === "text" && part.text) text += part.text;
1245
+ else if (part.type === "thinking" && part.thinking) reasoning += part.thinking;
1246
+ else if (part.type === "toolCall") {
1247
+ toolCalls.push({ name: part.name ?? "tool", args: JSON.stringify(part.arguments ?? {}) });
1248
+ toolCallCount += 1;
1249
+ }
1250
+ }
1251
+ }
1252
+ if (text.trim() || reasoning.trim() || toolCalls.length) {
1253
+ const em = { role };
1254
+ if (text.trim()) em.text = text.trim();
1255
+ if (reasoning.trim()) em.reasoning = reasoning.trim();
1256
+ if (toolCalls.length) em.toolCalls = toolCalls;
1257
+ messages.push(em);
1258
+ }
1259
+ }
1260
+ const meta = { source: "pi" };
1261
+ if (startedAt) meta.startedAt = startedAt;
1262
+ if (model) meta.model = model;
1263
+ meta.messageCount = messages.length;
1264
+ meta.toolCallCount = toolCallCount;
1265
+ return { meta, messages };
1266
+ }
1267
+ async function discoverPiSessions(cwd, since, until, expectedId) {
1268
+ const files = await walkPiSessionFiles(piSessionsDir());
1269
+ const sinceMs = since ? Date.parse(since) : NaN;
1270
+ const untilMs = until ? Date.parse(until) : NaN;
1271
+ const scored = [];
1272
+ for (const f of files) {
1273
+ let mtimeMs;
1274
+ try {
1275
+ mtimeMs = (await promises.stat(f)).mtimeMs;
1276
+ } catch {
1277
+ continue;
1278
+ }
1279
+ if (Number.isFinite(sinceMs) && mtimeMs < sinceMs - 1e3) continue;
1280
+ const first = await readFirstJsonLine(f);
1281
+ if (!first) continue;
1282
+ let meta;
1283
+ try {
1284
+ meta = JSON.parse(first);
1285
+ } catch {
1286
+ continue;
1287
+ }
1288
+ if (meta.type !== "session" || !meta.id) continue;
1289
+ if (expectedId) {
1290
+ if (meta.id !== expectedId) continue;
1291
+ } else if (meta.cwd !== cwd) {
1292
+ continue;
1293
+ }
1294
+ const startedAt = meta.timestamp;
1295
+ if (Number.isFinite(untilMs) && startedAt) {
1296
+ const s = Date.parse(startedAt);
1297
+ if (Number.isFinite(s) && s > untilMs) continue;
1298
+ }
1299
+ scored.push({
1300
+ candidate: {
1301
+ conversationId: meta.id,
1302
+ ...startedAt ? { startedAt } : {},
1303
+ lastActivityAt: new Date(mtimeMs).toISOString()
1304
+ },
1305
+ mtimeMs
1306
+ });
1307
+ }
1308
+ scored.sort((a, b) => b.mtimeMs - a.mtimeMs);
1309
+ return scored.map((s) => s.candidate);
1310
+ }
643
1311
  async function exportAgentSession(input) {
644
1312
  const { sessionId, registry, format = "markdown", maxToolChars = 1200 } = input;
645
1313
  const source = input.source ?? "auto";
@@ -735,6 +1403,18 @@ var init_transcript_export = __esm({
735
1403
  },
736
1404
  hermes: {
737
1405
  exportSession: (id) => exportHermesSession(id)
1406
+ },
1407
+ codex: {
1408
+ exportSession: (id) => exportCodexSession(id)
1409
+ },
1410
+ opencode: {
1411
+ exportSession: (id) => exportOpenCodeSession(id)
1412
+ },
1413
+ "mastracode-inprocess": {
1414
+ exportSession: (id) => exportMastracodeInprocessSession(id)
1415
+ },
1416
+ pi: {
1417
+ exportSession: (id) => exportPiSession(id)
738
1418
  }
739
1419
  };
740
1420
  }
@@ -862,6 +1542,38 @@ async function readHermes(conversationId) {
862
1542
  const { exportHermesSession: exportHermesSession2 } = await Promise.resolve().then(() => (init_transcript_export(), transcript_export_exports));
863
1543
  return exportHermesSession2(conversationId);
864
1544
  }
1545
+ async function discoverCodex(input) {
1546
+ const { discoverCodexSessions: discoverCodexSessions2 } = await Promise.resolve().then(() => (init_transcript_export(), transcript_export_exports));
1547
+ return discoverCodexSessions2(input.cwd, input.since, input.until, input.expectedId);
1548
+ }
1549
+ async function readCodex(conversationId) {
1550
+ const { exportCodexSession: exportCodexSession2 } = await Promise.resolve().then(() => (init_transcript_export(), transcript_export_exports));
1551
+ return exportCodexSession2(conversationId);
1552
+ }
1553
+ async function discoverOpenCode(input) {
1554
+ const { discoverOpenCodeSessions: discoverOpenCodeSessions2 } = await Promise.resolve().then(() => (init_transcript_export(), transcript_export_exports));
1555
+ return discoverOpenCodeSessions2(input.cwd, input.since, input.expectedId);
1556
+ }
1557
+ async function readOpenCode(conversationId) {
1558
+ const { exportOpenCodeSession: exportOpenCodeSession2 } = await Promise.resolve().then(() => (init_transcript_export(), transcript_export_exports));
1559
+ return exportOpenCodeSession2(conversationId);
1560
+ }
1561
+ async function discoverMastracodeInprocess(input) {
1562
+ const { discoverMastracodeInprocessSessions: discoverMastracodeInprocessSessions2 } = await Promise.resolve().then(() => (init_transcript_export(), transcript_export_exports));
1563
+ return discoverMastracodeInprocessSessions2(input.cwd, input.since, input.expectedId);
1564
+ }
1565
+ async function readMastracodeInprocess(conversationId) {
1566
+ const { exportMastracodeInprocessSession: exportMastracodeInprocessSession2 } = await Promise.resolve().then(() => (init_transcript_export(), transcript_export_exports));
1567
+ return exportMastracodeInprocessSession2(conversationId);
1568
+ }
1569
+ async function discoverPi(input) {
1570
+ const { discoverPiSessions: discoverPiSessions2 } = await Promise.resolve().then(() => (init_transcript_export(), transcript_export_exports));
1571
+ return discoverPiSessions2(input.cwd, input.since, input.until, input.expectedId);
1572
+ }
1573
+ async function readPi(conversationId) {
1574
+ const { exportPiSession: exportPiSession2 } = await Promise.resolve().then(() => (init_transcript_export(), transcript_export_exports));
1575
+ return exportPiSession2(conversationId);
1576
+ }
865
1577
  var CONVERSATION_STORES;
866
1578
  var init_conversation_store = __esm({
867
1579
  "src/conversation-store.ts"() {
@@ -885,6 +1597,28 @@ var init_conversation_store = __esm({
885
1597
  attachArgv: (conversationId) => ["hermes", "--resume", conversationId, "--tui"],
886
1598
  discover: discoverHermes,
887
1599
  read: readHermes
1600
+ },
1601
+ // codex-acp writes no resume flag we sniff, and there's no native PTY
1602
+ // reattach argv to declare — export/read only, so no outputHint/attachArgv.
1603
+ codex: {
1604
+ storeAs: "codexResumeId",
1605
+ discover: discoverCodex,
1606
+ read: readCodex
1607
+ },
1608
+ opencode: {
1609
+ storeAs: "openCodeResumeId",
1610
+ discover: discoverOpenCode,
1611
+ read: readOpenCode
1612
+ },
1613
+ "mastracode-inprocess": {
1614
+ storeAs: "mastracodeInprocessResumeId",
1615
+ discover: discoverMastracodeInprocess,
1616
+ read: readMastracodeInprocess
1617
+ },
1618
+ pi: {
1619
+ storeAs: "piResumeId",
1620
+ discover: discoverPi,
1621
+ read: readPi
888
1622
  }
889
1623
  };
890
1624
  }
@@ -892,37 +1626,34 @@ var init_conversation_store = __esm({
892
1626
 
893
1627
  // src/resume-strategies.ts
894
1628
  init_conversation_store();
895
- var claudeCodeStore = CONVERSATION_STORES["claude-code"];
896
- var RESUME_STRATEGIES = {
897
- "claude-code": {
898
- outputHint: claudeCodeStore.outputHint,
899
- storeAs: claudeCodeStore.storeAs,
900
- fsProbe: async (cwd, prevStartedAt, expectedId) => {
901
- const candidates = await claudeCodeStore.discover({
902
- cwd,
903
- since: prevStartedAt,
904
- expectedId
905
- });
906
- return candidates[0]?.conversationId ?? null;
907
- },
908
- spawnArgs: claudeCodeStore.attachArgv
909
- }
910
- // Stubs for other shipped adapters — fill in as we learn each
911
- // provider's resume mechanism. Today they fall back to ACP-level
912
- // resume (whatever the agent-cli runtime supports) or fresh spawn.
913
- //
914
- // hermes: { storeAs: "hermesResumeId", ... }
915
- // codex: { storeAs: "codexResumeId", ... }
916
- // openclaw: { storeAs: "openClawResumeId", ... }
917
- // opencode: { storeAs: "openCodeResumeId", ... }
918
- };
1629
+ var RESUME_STRATEGIES = Object.fromEntries(
1630
+ Object.entries(CONVERSATION_STORES).filter(([, store]) => typeof store.attachArgv === "function").map(([slug, store]) => {
1631
+ const s = store;
1632
+ return [
1633
+ slug,
1634
+ {
1635
+ outputHint: store.outputHint,
1636
+ storeAs: store.storeAs,
1637
+ fsProbe: async (cwd, prevStartedAt, expectedId) => {
1638
+ const candidates = await s.discover({
1639
+ cwd,
1640
+ since: prevStartedAt,
1641
+ expectedId
1642
+ });
1643
+ return candidates[0]?.conversationId ?? null;
1644
+ },
1645
+ spawnArgs: s.attachArgv
1646
+ }
1647
+ ];
1648
+ })
1649
+ );
919
1650
  function hasResumeStrategy(adapterSlug) {
920
1651
  if (!adapterSlug) return false;
921
1652
  const s = RESUME_STRATEGIES[adapterSlug];
922
1653
  return !!(s && (s.outputHint || s.fsProbe || s.spawnArgs));
923
1654
  }
924
1655
  function decideRestartStrategy(prev) {
925
- if (prev.adapterSlug) {
1656
+ if (prev.adapterSlug && prev.nativeTerminalResume === true) {
926
1657
  const strategy = RESUME_STRATEGIES[prev.adapterSlug];
927
1658
  const id = strategy?.storeAs ? prev.resumeMetadata?.[strategy.storeAs] : void 0;
928
1659
  if (strategy?.spawnArgs && id) {
@@ -933,9 +1664,11 @@ function decideRestartStrategy(prev) {
933
1664
  return { kind: "pty-plain" };
934
1665
  }
935
1666
  if (prev.adapterSlug) {
1667
+ const canResumeAtAcpLevel = prev.resumable !== false;
936
1668
  return {
937
1669
  kind: "agent",
938
- ...prev.adapterSessionId ? { resumeSessionId: prev.adapterSessionId } : {}
1670
+ ...prev.adapterSessionId && canResumeAtAcpLevel ? { resumeSessionId: prev.adapterSessionId } : {},
1671
+ ...prev.adapterSessionId && !canResumeAtAcpLevel ? { resumeFallback: true } : {}
939
1672
  };
940
1673
  }
941
1674
  return {
@@ -943,6 +1676,7 @@ function decideRestartStrategy(prev) {
943
1676
  reason: "generic command session \u2014 restart only supports pty + agent-cli"
944
1677
  };
945
1678
  }
1679
+ var RESUME_ID_REJECTED_RE = /not found|does not support|not supported|unsupported/i;
946
1680
  async function augmentWithFsResume(prev) {
947
1681
  const slug = prev.adapterSlug;
948
1682
  if (!slug) return prev;
@@ -973,6 +1707,9 @@ function describeResumePath(prev) {
973
1707
  }
974
1708
  }
975
1709
  if (prev.adapterSlug && prev.adapterSessionId) {
1710
+ if (prev.resumable === false) {
1711
+ return `fresh \u2014 resume not supported by ${prev.adapterSlug}`;
1712
+ }
976
1713
  return "resumed via ACP";
977
1714
  }
978
1715
  return "";
@@ -1005,6 +1742,6 @@ function tokenizeCommand(s) {
1005
1742
  return out;
1006
1743
  }
1007
1744
 
1008
- export { RESUME_STRATEGIES, augmentWithFsResume, decideRestartStrategy, describeResumePath, hasResumeStrategy, tokenizeCommand };
1745
+ export { RESUME_ID_REJECTED_RE, RESUME_STRATEGIES, augmentWithFsResume, decideRestartStrategy, describeResumePath, hasResumeStrategy, tokenizeCommand };
1009
1746
  //# sourceMappingURL=resume-strategies.mjs.map
1010
1747
  //# sourceMappingURL=resume-strategies.mjs.map