@oh-my-pi/pi-coding-agent 17.1.7 → 17.2.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.
Files changed (246) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/dist/{CHANGELOG-5k4dq4g6.md → CHANGELOG-be1f2t8h.md} +127 -0
  3. package/dist/cli.js +9554 -12379
  4. package/dist/template-8vdv6xb4.js +1671 -0
  5. package/dist/template-f8wx9vfn.css +1355 -0
  6. package/dist/template-qat058wr.html +55 -0
  7. package/dist/tool-views.generated-jdfmzwmn.js +35 -0
  8. package/dist/types/advisor/advise-tool.d.ts +0 -7
  9. package/dist/types/advisor/runtime.d.ts +8 -11
  10. package/dist/types/advisor/transcript-recorder.d.ts +15 -0
  11. package/dist/types/async/job-manager.d.ts +11 -0
  12. package/dist/types/auto-thinking/classifier.d.ts +9 -2
  13. package/dist/types/cleanse/agent.d.ts +19 -0
  14. package/dist/types/cleanse/balance.d.ts +7 -0
  15. package/dist/types/cleanse/checkers.d.ts +13 -0
  16. package/dist/types/cleanse/index.d.ts +16 -0
  17. package/dist/types/cleanse/loop.d.ts +16 -0
  18. package/dist/types/cleanse/parsers.d.ts +13 -0
  19. package/dist/types/cleanse/progress.d.ts +14 -0
  20. package/dist/types/cleanse/types.d.ts +64 -0
  21. package/dist/types/cli/args.d.ts +2 -0
  22. package/dist/types/cli/session-picker.d.ts +14 -7
  23. package/dist/types/collab/guest.d.ts +21 -2
  24. package/dist/types/commands/cleanse.d.ts +23 -0
  25. package/dist/types/commands/launch.d.ts +6 -0
  26. package/dist/types/config/settings-schema.d.ts +63 -8
  27. package/dist/types/cursor-bridge-tools.d.ts +54 -0
  28. package/dist/types/cursor.d.ts +193 -8
  29. package/dist/types/edit/edit-clipboard.d.ts +19 -0
  30. package/dist/types/edit/hashline/diff.d.ts +9 -1
  31. package/dist/types/edit/index.d.ts +8 -1
  32. package/dist/types/edit/streaming.d.ts +8 -1
  33. package/dist/types/export/html/index.d.ts +2 -0
  34. package/dist/types/extensibility/extensions/runner.d.ts +19 -2
  35. package/dist/types/extensibility/extensions/types.d.ts +25 -1
  36. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +27 -1
  37. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +2 -0
  38. package/dist/types/internal-urls/local-protocol.d.ts +3 -2
  39. package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
  40. package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
  41. package/dist/types/launch/terminal-output-worker.d.ts +1 -0
  42. package/dist/types/lsp/client.d.ts +2 -0
  43. package/dist/types/main.d.ts +2 -0
  44. package/dist/types/mcp/manager.d.ts +33 -4
  45. package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
  46. package/dist/types/modes/components/read-tool-group.d.ts +12 -0
  47. package/dist/types/modes/components/session-selector.d.ts +6 -0
  48. package/dist/types/modes/components/status-line/component.d.ts +23 -0
  49. package/dist/types/modes/components/tool-execution.d.ts +16 -7
  50. package/dist/types/modes/components/usage-row.d.ts +2 -0
  51. package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
  52. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  53. package/dist/types/modes/interactive-mode.d.ts +8 -5
  54. package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
  55. package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
  56. package/dist/types/modes/theme/theme.d.ts +12 -1
  57. package/dist/types/modes/types.d.ts +2 -1
  58. package/dist/types/sdk.d.ts +1 -0
  59. package/dist/types/session/agent-session-events.d.ts +2 -0
  60. package/dist/types/session/agent-session-types.d.ts +36 -1
  61. package/dist/types/session/agent-session.d.ts +4 -2
  62. package/dist/types/session/async-job-delivery.d.ts +8 -0
  63. package/dist/types/session/claude-session-store.d.ts +13 -0
  64. package/dist/types/session/codex-session-store.d.ts +14 -0
  65. package/dist/types/session/credential-pin.d.ts +58 -0
  66. package/dist/types/session/foreign-session-import.d.ts +15 -0
  67. package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
  68. package/dist/types/session/foreign-session-store.d.ts +23 -0
  69. package/dist/types/session/model-controls.d.ts +3 -3
  70. package/dist/types/session/session-advisors.d.ts +47 -2
  71. package/dist/types/session/session-entries.d.ts +19 -1
  72. package/dist/types/session/session-handoff.d.ts +4 -1
  73. package/dist/types/session/session-loader.d.ts +2 -0
  74. package/dist/types/session/session-maintenance.d.ts +1 -1
  75. package/dist/types/session/session-manager.d.ts +21 -0
  76. package/dist/types/session/session-tools.d.ts +25 -1
  77. package/dist/types/session/turn-recovery.d.ts +10 -4
  78. package/dist/types/system-prompt.d.ts +2 -1
  79. package/dist/types/task/executor.d.ts +11 -2
  80. package/dist/types/task/isolation-ownership.d.ts +34 -0
  81. package/dist/types/task/render.d.ts +7 -0
  82. package/dist/types/thinking.d.ts +18 -6
  83. package/dist/types/tools/acp-bridge.d.ts +28 -5
  84. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +1 -2
  85. package/dist/types/tools/browser/tab-worker.d.ts +1 -4
  86. package/dist/types/tools/grep.d.ts +17 -1
  87. package/dist/types/tools/hub/launch.d.ts +5 -1
  88. package/dist/types/tools/index.d.ts +4 -1
  89. package/dist/types/tools/path-utils.d.ts +19 -0
  90. package/dist/types/tools/read.d.ts +2 -0
  91. package/dist/types/tools/xdev.d.ts +2 -1
  92. package/dist/types/tui/output-block.d.ts +5 -5
  93. package/dist/types/utils/changelog.d.ts +19 -0
  94. package/dist/types/utils/git.d.ts +10 -0
  95. package/dist/types/utils/jj.d.ts +11 -4
  96. package/dist/types/web/search/types.d.ts +2 -2
  97. package/package.json +16 -12
  98. package/scripts/bundle-dist.ts +3 -1
  99. package/src/advisor/advise-tool.ts +0 -11
  100. package/src/advisor/runtime.ts +138 -24
  101. package/src/advisor/transcript-recorder.ts +56 -0
  102. package/src/async/job-manager.ts +30 -7
  103. package/src/auto-thinking/classifier.ts +48 -10
  104. package/src/cleanse/agent.ts +226 -0
  105. package/src/cleanse/balance.ts +79 -0
  106. package/src/cleanse/checkers.ts +996 -0
  107. package/src/cleanse/index.ts +190 -0
  108. package/src/cleanse/loop.ts +51 -0
  109. package/src/cleanse/parsers.ts +726 -0
  110. package/src/cleanse/progress.ts +50 -0
  111. package/src/cleanse/prompts/assignment.md +47 -0
  112. package/src/cleanse/types.ts +72 -0
  113. package/src/cli/args.ts +6 -0
  114. package/src/cli/flag-tables.ts +2 -0
  115. package/src/cli/gallery-fixtures/fs.ts +25 -0
  116. package/src/cli/session-picker.ts +34 -17
  117. package/src/cli/ttsr-cli.ts +19 -1
  118. package/src/cli/update-cli.ts +3 -0
  119. package/src/cli/usage-cli.ts +29 -4
  120. package/src/cli/worktree-cli.ts +28 -11
  121. package/src/cli-commands.ts +1 -0
  122. package/src/cli.ts +11 -1
  123. package/src/collab/guest.ts +37 -4
  124. package/src/collab/host.ts +1 -0
  125. package/src/commands/cleanse.ts +45 -0
  126. package/src/commands/launch.ts +6 -0
  127. package/src/config/model-registry.ts +103 -25
  128. package/src/config/settings-schema.ts +65 -6
  129. package/src/config/settings.ts +25 -0
  130. package/src/cursor-bridge-tools.ts +81 -0
  131. package/src/cursor.ts +463 -12
  132. package/src/discovery/claude-plugins.ts +144 -34
  133. package/src/edit/edit-clipboard.ts +23 -0
  134. package/src/edit/hashline/diff.ts +49 -10
  135. package/src/edit/hashline/execute.ts +38 -6
  136. package/src/edit/hashline/filesystem.ts +27 -3
  137. package/src/edit/index.ts +12 -2
  138. package/src/edit/renderer.ts +4 -4
  139. package/src/edit/streaming.ts +17 -2
  140. package/src/export/html/index.ts +17 -10
  141. package/src/export/html/template.js +1 -1
  142. package/src/extensibility/extensions/runner.ts +70 -0
  143. package/src/extensibility/extensions/types.ts +30 -0
  144. package/src/extensibility/legacy-pi-ai-shim.ts +46 -1
  145. package/src/extensibility/legacy-pi-coding-agent-shim.ts +12 -13
  146. package/src/internal-urls/local-protocol.ts +3 -2
  147. package/src/internal-urls/mcp-protocol.ts +4 -1
  148. package/src/launch/broker.ts +14 -4
  149. package/src/launch/terminal-output-worker-client.ts +53 -0
  150. package/src/launch/terminal-output-worker-protocol.ts +11 -0
  151. package/src/launch/terminal-output-worker.ts +23 -0
  152. package/src/lsp/client.ts +18 -3
  153. package/src/lsp/index.ts +4 -0
  154. package/src/lsp/render.ts +1 -1
  155. package/src/main.ts +127 -56
  156. package/src/mcp/manager.ts +120 -18
  157. package/src/modes/acp/acp-agent.ts +27 -18
  158. package/src/modes/acp/acp-event-mapper.ts +38 -4
  159. package/src/modes/components/chat-transcript-builder.ts +30 -11
  160. package/src/modes/components/codex-reset-fireworks.ts +369 -0
  161. package/src/modes/components/read-tool-group.ts +159 -17
  162. package/src/modes/components/session-selector.ts +19 -3
  163. package/src/modes/components/settings-selector.ts +9 -1
  164. package/src/modes/components/status-line/component.ts +381 -54
  165. package/src/modes/components/tool-execution.ts +78 -31
  166. package/src/modes/components/usage-row.ts +10 -5
  167. package/src/modes/controllers/event-controller.ts +26 -4
  168. package/src/modes/controllers/mcp-command-controller.ts +68 -3
  169. package/src/modes/controllers/selector-controller.ts +94 -36
  170. package/src/modes/controllers/tan-command-controller.ts +14 -0
  171. package/src/modes/interactive-mode.ts +67 -64
  172. package/src/modes/prompt-action-autocomplete.ts +5 -3
  173. package/src/modes/rpc/rpc-client.ts +18 -0
  174. package/src/modes/rpc/rpc-mode.ts +16 -1
  175. package/src/modes/rpc/rpc-types.ts +11 -0
  176. package/src/modes/theme/theme.ts +151 -6
  177. package/src/modes/types.ts +2 -1
  178. package/src/modes/utils/ui-helpers.ts +38 -21
  179. package/src/prompts/goals/guided-goal-interview.md +41 -6
  180. package/src/prompts/system/auto-thinking-difficulty.md +4 -2
  181. package/src/prompts/system/plan-mode-active.md +2 -2
  182. package/src/prompts/system/system-prompt.md +3 -0
  183. package/src/prompts/system/vibe-mode-active.md +4 -1
  184. package/src/prompts/system/xdev-mount-notice.md +1 -1
  185. package/src/prompts/tools/bash.md +15 -17
  186. package/src/prompts/tools/browser.md +1 -0
  187. package/src/prompts/tools/checkpoint.md +1 -1
  188. package/src/prompts/tools/glob.md +7 -6
  189. package/src/prompts/tools/goal.md +1 -1
  190. package/src/prompts/tools/grep.md +7 -6
  191. package/src/sdk.ts +178 -15
  192. package/src/session/agent-session-events.ts +1 -0
  193. package/src/session/agent-session-types.ts +40 -1
  194. package/src/session/agent-session.ts +330 -179
  195. package/src/session/async-job-delivery.ts +8 -0
  196. package/src/session/claude-session-store.ts +426 -0
  197. package/src/session/codex-session-store.ts +673 -0
  198. package/src/session/credential-pin.ts +93 -0
  199. package/src/session/foreign-session-import.ts +52 -0
  200. package/src/session/foreign-session-jsonl.ts +29 -0
  201. package/src/session/foreign-session-store.ts +26 -0
  202. package/src/session/model-controls.ts +15 -7
  203. package/src/session/session-advisors.ts +142 -23
  204. package/src/session/session-entries.ts +21 -1
  205. package/src/session/session-handoff.ts +20 -3
  206. package/src/session/session-history-format.ts +31 -6
  207. package/src/session/session-loader.ts +25 -9
  208. package/src/session/session-maintenance.ts +59 -11
  209. package/src/session/session-manager.ts +61 -0
  210. package/src/session/session-tools.ts +107 -5
  211. package/src/session/settings-stream-fn.ts +1 -0
  212. package/src/session/turn-recovery.ts +97 -51
  213. package/src/slash-commands/builtin-registry.ts +136 -5
  214. package/src/system-prompt.ts +3 -2
  215. package/src/task/executor.ts +17 -4
  216. package/src/task/isolation-ownership.ts +106 -0
  217. package/src/task/render.ts +20 -4
  218. package/src/task/worktree.ts +8 -0
  219. package/src/thinking.ts +29 -10
  220. package/src/tools/acp-bridge.ts +52 -8
  221. package/src/tools/ask.ts +3 -3
  222. package/src/tools/ast-edit.ts +9 -2
  223. package/src/tools/browser/cmux/cmux-tab.ts +9 -14
  224. package/src/tools/browser/tab-worker.ts +12 -35
  225. package/src/tools/browser.ts +7 -2
  226. package/src/tools/checkpoint.ts +0 -13
  227. package/src/tools/gh-renderer.ts +3 -3
  228. package/src/tools/grep.ts +64 -8
  229. package/src/tools/hub/launch.ts +10 -15
  230. package/src/tools/index.ts +38 -5
  231. package/src/tools/path-utils.ts +88 -0
  232. package/src/tools/read.ts +3 -0
  233. package/src/tools/write.ts +44 -3
  234. package/src/tools/xdev.ts +39 -14
  235. package/src/tui/code-cell.ts +4 -4
  236. package/src/tui/output-block.ts +25 -8
  237. package/src/utils/changelog.ts +144 -4
  238. package/src/utils/git.ts +61 -14
  239. package/src/utils/jj.ts +19 -13
  240. package/src/utils/shell-snapshot-fn-env.sh +5 -2
  241. package/src/web/search/providers/codex.ts +34 -0
  242. package/src/web/search/render.ts +2 -2
  243. package/src/web/search/types.ts +1 -1
  244. package/dist/types/goals/guided-setup.d.ts +0 -30
  245. package/src/goals/guided-setup.ts +0 -171
  246. package/src/prompts/goals/guided-goal-system.md +0 -33
@@ -0,0 +1,673 @@
1
+ import { Database } from "bun:sqlite";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import type {
6
+ AssistantMessage,
7
+ ImageContent,
8
+ TextContent,
9
+ ThinkingContent,
10
+ ToolCall,
11
+ ToolResultMessage,
12
+ UserMessage,
13
+ } from "@oh-my-pi/pi-ai";
14
+ import { isRecord } from "@oh-my-pi/pi-utils";
15
+ import { readForeignJsonRecords } from "./foreign-session-jsonl";
16
+ import type { ForeignSessionInfo, ForeignSessionStore } from "./foreign-session-store";
17
+ import type { CompactionEntry, ModelChangeEntry, SessionEntry, SessionMessageEntry } from "./session-entries";
18
+ import { SessionManager } from "./session-manager";
19
+
20
+ interface CodexThreadRow {
21
+ id: string;
22
+ rollout_path: string;
23
+ created_at: number | null;
24
+ updated_at: number | null;
25
+ cwd: string;
26
+ title: string | null;
27
+ first_user_message: string | null;
28
+ }
29
+
30
+ interface CodexIndexRow {
31
+ id: string;
32
+ thread_name: string;
33
+ updated_at: string;
34
+ }
35
+
36
+ interface CodexCompaction {
37
+ summary: string;
38
+ replacementHistory?: Array<Record<string, unknown>>;
39
+ compactionItem?: Record<string, unknown>;
40
+ }
41
+
42
+ interface ConvertedRecord {
43
+ message?: UserMessage | AssistantMessage | ToolResultMessage;
44
+ followingMessage?: ToolResultMessage;
45
+ model?: string;
46
+ rollbackTurns?: number;
47
+ title?: string;
48
+ timestamp?: number;
49
+ compaction?: CodexCompaction;
50
+ }
51
+
52
+ const EMPTY_USAGE: AssistantMessage["usage"] = {
53
+ input: 0,
54
+ output: 0,
55
+ cacheRead: 0,
56
+ cacheWrite: 0,
57
+ totalTokens: 0,
58
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
59
+ };
60
+
61
+ function stringField(record: Record<string, unknown>, key: string): string | undefined {
62
+ const value = record[key];
63
+ return typeof value === "string" ? value : undefined;
64
+ }
65
+
66
+ function numberField(record: Record<string, unknown>, key: string): number | undefined {
67
+ const value = record[key];
68
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
69
+ }
70
+
71
+ function timestampMillis(value: unknown, fallback: number): number {
72
+ if (typeof value === "number" && Number.isFinite(value)) return value < 10_000_000_000 ? value * 1000 : value;
73
+ if (typeof value === "string") {
74
+ const parsed = Date.parse(value);
75
+ if (Number.isFinite(parsed)) return parsed;
76
+ }
77
+ return fallback;
78
+ }
79
+
80
+ function dateFromEpoch(value: number | null, fallback: Date): Date {
81
+ if (value === null || !Number.isFinite(value)) return fallback;
82
+ return new Date(value < 10_000_000_000 ? value * 1000 : value);
83
+ }
84
+
85
+ function imageFromUrl(value: unknown, detail: unknown): ImageContent | undefined {
86
+ if (typeof value !== "string") return undefined;
87
+ const match = /^data:([^;,]+);base64,(.+)$/s.exec(value);
88
+ if (!match) return undefined;
89
+ const resolution =
90
+ detail === "auto" || detail === "low" || detail === "high" || detail === "original" ? detail : undefined;
91
+ return { type: "image", mimeType: match[1], data: match[2], detail: resolution };
92
+ }
93
+
94
+ function responseContent(value: unknown): Array<TextContent | ImageContent> {
95
+ if (!Array.isArray(value)) return [];
96
+ const content: Array<TextContent | ImageContent> = [];
97
+ for (const item of value) {
98
+ if (!isRecord(item)) continue;
99
+ const type = stringField(item, "type");
100
+ const text = stringField(item, "text");
101
+ if ((type === "input_text" || type === "output_text" || type === "text") && text !== undefined) {
102
+ content.push({ type: "text", text });
103
+ continue;
104
+ }
105
+ if (type === "input_image") {
106
+ const image = imageFromUrl(item.image_url, item.detail);
107
+ if (image) content.push(image);
108
+ }
109
+ }
110
+ return content;
111
+ }
112
+
113
+ function textFromContent(value: unknown): string {
114
+ return responseContent(value)
115
+ .filter((part): part is TextContent => part.type === "text")
116
+ .map(part => part.text)
117
+ .join("");
118
+ }
119
+
120
+ function toolArguments(value: unknown): Record<string, unknown> {
121
+ if (isRecord(value)) return value;
122
+ if (typeof value !== "string") return {};
123
+ try {
124
+ const parsed: unknown = JSON.parse(value);
125
+ if (isRecord(parsed)) return parsed;
126
+ } catch {
127
+ // Custom tools intentionally carry non-JSON input.
128
+ }
129
+ return { input: value };
130
+ }
131
+
132
+ function toolOutputContent(value: unknown): Array<TextContent | ImageContent> {
133
+ const structured = responseContent(value);
134
+ if (structured.length > 0) return structured;
135
+ if (typeof value === "string") return [{ type: "text", text: value }];
136
+ if (value === undefined) return [];
137
+ try {
138
+ return [{ type: "text", text: JSON.stringify(value) }];
139
+ } catch {
140
+ return [{ type: "text", text: String(value) }];
141
+ }
142
+ }
143
+
144
+ function reasoningContent(payload: Record<string, unknown>): ThinkingContent[] {
145
+ const parts: ThinkingContent[] = [];
146
+ for (const key of ["summary", "content"]) {
147
+ const value = payload[key];
148
+ if (!Array.isArray(value)) continue;
149
+ for (const item of value) {
150
+ if (!isRecord(item)) continue;
151
+ const text = stringField(item, "text");
152
+ if (text) parts.push({ type: "thinking", thinking: text });
153
+ }
154
+ }
155
+ return parts;
156
+ }
157
+
158
+ function assistantMessage(
159
+ content: AssistantMessage["content"],
160
+ timestamp: number,
161
+ model: string,
162
+ stopReason: AssistantMessage["stopReason"],
163
+ ): AssistantMessage {
164
+ return {
165
+ role: "assistant",
166
+ content,
167
+ api: "openai-codex-responses",
168
+ provider: "openai-codex",
169
+ model,
170
+ usage: EMPTY_USAGE,
171
+ stopReason,
172
+ timestamp,
173
+ };
174
+ }
175
+
176
+ async function firstJsonRecord(filePath: string): Promise<Record<string, unknown> | undefined> {
177
+ for await (const { value } of readForeignJsonRecords(filePath)) return value;
178
+ return undefined;
179
+ }
180
+
181
+ async function readJsonLines(filePath: string): Promise<Record<string, unknown>[]> {
182
+ const records: Record<string, unknown>[] = [];
183
+ for await (const { value } of readForeignJsonRecords(filePath)) records.push(value);
184
+ return records;
185
+ }
186
+
187
+ async function rolloutFiles(directory: string): Promise<string[]> {
188
+ let entries: fs.Dirent[];
189
+ try {
190
+ entries = await fs.promises.readdir(directory, { withFileTypes: true });
191
+ } catch {
192
+ return [];
193
+ }
194
+ const files: string[] = [];
195
+ for (const entry of entries) {
196
+ const child = path.join(directory, entry.name);
197
+ if (entry.isDirectory()) files.push(...(await rolloutFiles(child)));
198
+ else if (entry.isFile() && entry.name.endsWith(".jsonl")) files.push(child);
199
+ }
200
+ return files;
201
+ }
202
+
203
+ function rolloutId(filePath: string): string {
204
+ const match = /([0-9a-f]{8}-[0-9a-f-]{27})\.jsonl$/i.exec(filePath);
205
+ return match?.[1] ?? path.basename(filePath, ".jsonl");
206
+ }
207
+
208
+ async function stateDatabasePath(root: string): Promise<string | undefined> {
209
+ const names = await fs.promises.readdir(root).catch(() => []);
210
+ return names
211
+ .map(name => ({ name, version: /^state_(\d+)\.sqlite$/.exec(name) }))
212
+ .filter(item => item.version !== null)
213
+ .sort((left, right) => Number(right.version?.[1]) - Number(left.version?.[1]))
214
+ .map(item => path.join(root, item.name))
215
+ .at(0);
216
+ }
217
+
218
+ async function loadIndex(root: string): Promise<Map<string, CodexIndexRow>> {
219
+ const index = new Map<string, CodexIndexRow>();
220
+ const filePath = path.join(root, "session_index.jsonl");
221
+ let records: Record<string, unknown>[];
222
+ try {
223
+ records = await readJsonLines(filePath);
224
+ } catch {
225
+ return index;
226
+ }
227
+ for (const record of records) {
228
+ const id = stringField(record, "id");
229
+ const threadName = stringField(record, "thread_name");
230
+ const updatedAt = stringField(record, "updated_at");
231
+ if (id && threadName && updatedAt) index.set(id, { id, thread_name: threadName, updated_at: updatedAt });
232
+ }
233
+ return index;
234
+ }
235
+
236
+ function convertedResponseItem(
237
+ payload: Record<string, unknown>,
238
+ timestamp: number,
239
+ model: string,
240
+ toolNames: Map<string, string>,
241
+ ): ConvertedRecord | undefined {
242
+ const type = stringField(payload, "type");
243
+ if (type === "message") {
244
+ const role = stringField(payload, "role");
245
+ const content = responseContent(payload.content);
246
+ if (content.length === 0) return undefined;
247
+ if (role === "user") return { message: { role: "user", content, timestamp } };
248
+ if (role === "assistant") return { message: assistantMessage(content, timestamp, model, "stop") };
249
+ return undefined;
250
+ }
251
+ if (type === "reasoning") {
252
+ const content = reasoningContent(payload);
253
+ return content.length > 0 ? { message: assistantMessage(content, timestamp, model, "stop") } : undefined;
254
+ }
255
+ if (type === "function_call" || type === "custom_tool_call") {
256
+ const callId = stringField(payload, "call_id") ?? stringField(payload, "id");
257
+ const name = stringField(payload, "name");
258
+ if (!callId || !name) return undefined;
259
+ toolNames.set(callId, name);
260
+ const call: ToolCall = {
261
+ type: "toolCall",
262
+ id: callId,
263
+ name,
264
+ arguments: toolArguments(type === "custom_tool_call" ? payload.input : payload.arguments),
265
+ customWireName: type === "custom_tool_call" ? name : undefined,
266
+ };
267
+ return { message: assistantMessage([call], timestamp, model, "toolUse") };
268
+ }
269
+ if (type === "function_call_output" || type === "custom_tool_call_output") {
270
+ const callId = stringField(payload, "call_id");
271
+ if (!callId) return undefined;
272
+ return {
273
+ message: {
274
+ role: "toolResult",
275
+ toolCallId: callId,
276
+ toolName: toolNames.get(callId) ?? "unknown",
277
+ content: toolOutputContent(payload.output),
278
+ isError: false,
279
+ timestamp,
280
+ },
281
+ };
282
+ }
283
+ if (type === "web_search_call" || type === "tool_search_call") {
284
+ const callId = stringField(payload, "call_id") ?? stringField(payload, "id");
285
+ if (!callId) return undefined;
286
+ const name = type === "web_search_call" ? "web_search" : "tool_search";
287
+ toolNames.set(callId, name);
288
+ const input = type === "web_search_call" ? payload.action : payload.arguments;
289
+ const call: ToolCall = { type: "toolCall", id: callId, name, arguments: toolArguments(input) };
290
+ return { message: assistantMessage([call], timestamp, model, "toolUse") };
291
+ }
292
+ if (type === "tool_search_output") {
293
+ const callId = stringField(payload, "call_id");
294
+ if (!callId) return undefined;
295
+ return {
296
+ message: {
297
+ role: "toolResult",
298
+ toolCallId: callId,
299
+ toolName: toolNames.get(callId) ?? "tool_search",
300
+ content: toolOutputContent(payload.tools),
301
+ isError: payload.status === "failed",
302
+ timestamp,
303
+ },
304
+ };
305
+ }
306
+ return undefined;
307
+ }
308
+
309
+ function convertedEvent(
310
+ payload: Record<string, unknown>,
311
+ timestamp: number,
312
+ model: string,
313
+ canonicalUserText: Set<string>,
314
+ canonicalAssistantText: Set<string>,
315
+ canonicalToolCalls: Set<string>,
316
+ toolNames: Map<string, string>,
317
+ ): ConvertedRecord | undefined {
318
+ const type = stringField(payload, "type");
319
+ if (type === "user_message") {
320
+ const text = stringField(payload, "message");
321
+ if (!text || canonicalUserText.has(text)) return undefined;
322
+ return { message: { role: "user", content: text, timestamp } };
323
+ }
324
+ if (type === "agent_message") {
325
+ const text = stringField(payload, "message");
326
+ if (!text || canonicalAssistantText.has(text)) return undefined;
327
+ return { message: assistantMessage([{ type: "text", text }], timestamp, model, "stop") };
328
+ }
329
+ if (type === "agent_reasoning") {
330
+ const text = stringField(payload, "text");
331
+ if (!text || canonicalAssistantText.has(text)) return undefined;
332
+ return { message: assistantMessage([{ type: "thinking", thinking: text }], timestamp, model, "stop") };
333
+ }
334
+ if (type === "dynamic_tool_call_request") {
335
+ const callId = stringField(payload, "callId") ?? stringField(payload, "call_id");
336
+ const name = stringField(payload, "tool");
337
+ if (!callId || !name || canonicalToolCalls.has(callId)) return undefined;
338
+ toolNames.set(callId, name);
339
+ const call: ToolCall = { type: "toolCall", id: callId, name, arguments: toolArguments(payload.arguments) };
340
+ return { message: assistantMessage([call], timestamp, model, "toolUse") };
341
+ }
342
+ if (type === "dynamic_tool_call_response") {
343
+ const callId = stringField(payload, "call_id") ?? stringField(payload, "callId");
344
+ if (!callId || canonicalToolCalls.has(callId)) return undefined;
345
+ const error = stringField(payload, "error");
346
+ return {
347
+ message: {
348
+ role: "toolResult",
349
+ toolCallId: callId,
350
+ toolName: toolNames.get(callId) ?? stringField(payload, "tool") ?? "unknown",
351
+ content: error ? [{ type: "text", text: error }] : toolOutputContent(payload.content_items),
352
+ isError: error !== undefined || payload.success === false,
353
+ timestamp,
354
+ },
355
+ };
356
+ }
357
+ if (type === "web_search_end") {
358
+ const callId = stringField(payload, "call_id");
359
+ if (!callId) return undefined;
360
+ const name = toolNames.get(callId) ?? "web_search";
361
+ const result: ToolResultMessage = {
362
+ role: "toolResult",
363
+ toolCallId: callId,
364
+ toolName: name,
365
+ content: toolOutputContent(payload.results ?? payload.query),
366
+ isError: false,
367
+ timestamp,
368
+ };
369
+ if (canonicalToolCalls.has(callId) || toolNames.has(callId)) return { message: result };
370
+ toolNames.set(callId, name);
371
+ const call: ToolCall = {
372
+ type: "toolCall",
373
+ id: callId,
374
+ name,
375
+ arguments: toolArguments(payload.action ?? payload.query),
376
+ };
377
+ return {
378
+ message: assistantMessage([call], timestamp, model, "toolUse"),
379
+ followingMessage: result,
380
+ };
381
+ }
382
+ if (type === "mcp_tool_call_end") {
383
+ const callId = stringField(payload, "call_id");
384
+ if (!callId || canonicalToolCalls.has(callId) || !isRecord(payload.invocation)) return undefined;
385
+ const server = stringField(payload.invocation, "server");
386
+ const tool = stringField(payload.invocation, "tool");
387
+ if (!server || !tool) return undefined;
388
+ const name = `${server}/${tool}`;
389
+ toolNames.set(callId, name);
390
+ const call: ToolCall = {
391
+ type: "toolCall",
392
+ id: callId,
393
+ name,
394
+ arguments: toolArguments(payload.invocation.arguments),
395
+ };
396
+ const result = isRecord(payload.result) && isRecord(payload.result.Ok) ? payload.result.Ok : payload.result;
397
+ const error = isRecord(payload.result) && typeof payload.result.Err === "string" ? payload.result.Err : undefined;
398
+ return {
399
+ message: assistantMessage([call], timestamp, model, "toolUse"),
400
+ followingMessage: {
401
+ role: "toolResult",
402
+ toolCallId: callId,
403
+ toolName: name,
404
+ content: error
405
+ ? [{ type: "text", text: error }]
406
+ : toolOutputContent(isRecord(result) ? result.content : result),
407
+ isError: error !== undefined || (isRecord(result) && result.isError === true),
408
+ timestamp,
409
+ },
410
+ };
411
+ }
412
+ if (type === "thread_name_updated") return { title: stringField(payload, "thread_name") };
413
+ if (type === "thread_rolled_back") return { rollbackTurns: numberField(payload, "num_turns") ?? 0 };
414
+ return undefined;
415
+ }
416
+
417
+ function canonicalTexts(records: Record<string, unknown>[]): {
418
+ users: Set<string>;
419
+ assistants: Set<string>;
420
+ toolCalls: Set<string>;
421
+ } {
422
+ const users = new Set<string>();
423
+ const assistants = new Set<string>();
424
+ const toolCalls = new Set<string>();
425
+ for (const record of records) {
426
+ if (record.type !== "response_item" || !isRecord(record.payload)) continue;
427
+ const callId = stringField(record.payload, "call_id") ?? stringField(record.payload, "id");
428
+ if (callId && typeof record.payload.type === "string" && record.payload.type.includes("call"))
429
+ toolCalls.add(callId);
430
+ if (record.payload.type === "message") {
431
+ const text = textFromContent(record.payload.content);
432
+ if (!text) continue;
433
+ if (record.payload.role === "user") users.add(text);
434
+ else if (record.payload.role === "assistant") assistants.add(text);
435
+ } else if (record.payload.type === "reasoning") {
436
+ for (const part of reasoningContent(record.payload)) assistants.add(part.thinking);
437
+ }
438
+ }
439
+ return { users, assistants, toolCalls };
440
+ }
441
+
442
+ function rollback(records: ConvertedRecord[], turns: number): void {
443
+ for (let remaining = turns; remaining > 0; remaining--) {
444
+ let userIndex = -1;
445
+ for (let index = records.length - 1; index >= 0; index--) {
446
+ if (records[index].message?.role === "user") {
447
+ userIndex = index;
448
+ break;
449
+ }
450
+ }
451
+ if (userIndex < 0) return;
452
+ records.splice(userIndex);
453
+ }
454
+ }
455
+
456
+ /** Imports locally stored OpenAI Codex sessions into OMP's in-memory session format. */
457
+ export class CodexSessionStore implements ForeignSessionStore {
458
+ /** Foreign-session source discriminator. */
459
+ readonly source = "codex";
460
+ readonly #root: string;
461
+
462
+ /** Uses the supplied Codex data root, or ~/.codex by default. */
463
+ constructor(rootDirectory: string = path.join(os.homedir(), ".codex")) {
464
+ this.#root = path.resolve(rootDirectory);
465
+ }
466
+
467
+ /** Lists Codex sessions from its state index without reading transcript bodies. */
468
+ async list(): Promise<ForeignSessionInfo[]> {
469
+ const databasePath = await stateDatabasePath(this.#root);
470
+ if (databasePath) {
471
+ try {
472
+ const database = new Database(databasePath, { readonly: true });
473
+ try {
474
+ const rows = database
475
+ .query<CodexThreadRow, []>(
476
+ "SELECT id, rollout_path, created_at, updated_at, cwd, title, first_user_message FROM threads",
477
+ )
478
+ .all();
479
+ const sessions: ForeignSessionInfo[] = [];
480
+ for (const row of rows) {
481
+ if (!row.id || !row.rollout_path || !row.cwd) continue;
482
+ const rolloutPath = path.isAbsolute(row.rollout_path)
483
+ ? row.rollout_path
484
+ : path.join(this.#root, row.rollout_path);
485
+ const modified = dateFromEpoch(row.updated_at, new Date(0));
486
+ const created = dateFromEpoch(row.created_at, modified);
487
+ sessions.push({
488
+ source: "codex",
489
+ id: row.id,
490
+ path: rolloutPath,
491
+ cwd: row.cwd,
492
+ title: row.title ?? undefined,
493
+ created,
494
+ modified,
495
+ firstMessage: row.first_user_message ?? undefined,
496
+ });
497
+ }
498
+ sessions.sort(
499
+ (left, right) =>
500
+ right.modified.getTime() - left.modified.getTime() || left.id.localeCompare(right.id),
501
+ );
502
+ if (sessions.length > 0) return sessions;
503
+ } finally {
504
+ database.close();
505
+ }
506
+ } catch {
507
+ // Older Codex state databases fall back to the rollout metadata path.
508
+ }
509
+ }
510
+
511
+ const index = await loadIndex(this.#root);
512
+ const roots = ["sessions", ".sessions", "archived_sessions"].map(name => path.join(this.#root, name));
513
+ const files = (await Promise.all(roots.map(rolloutFiles))).flat();
514
+ const sessions: ForeignSessionInfo[] = [];
515
+ for (const filePath of files) {
516
+ const first = await firstJsonRecord(filePath);
517
+ if (first?.type !== "session_meta" || !isRecord(first.payload)) continue;
518
+ const id = stringField(first.payload, "id") ?? rolloutId(filePath);
519
+ const cwd = stringField(first.payload, "cwd");
520
+ if (!cwd) continue;
521
+ const stat = await fs.promises.stat(filePath);
522
+ const indexed = index.get(id);
523
+ const sourceCreated = stringField(first.payload, "timestamp");
524
+ const created = new Date(timestampMillis(sourceCreated, stat.birthtimeMs));
525
+ const modified = indexed ? new Date(timestampMillis(indexed.updated_at, stat.mtimeMs)) : stat.mtime;
526
+ sessions.push({
527
+ source: "codex",
528
+ id,
529
+ path: filePath,
530
+ cwd,
531
+ title: indexed?.thread_name,
532
+ created,
533
+ modified,
534
+ });
535
+ }
536
+ sessions.sort(
537
+ (left, right) => right.modified.getTime() - left.modified.getTime() || left.id.localeCompare(right.id),
538
+ );
539
+ return sessions;
540
+ }
541
+
542
+ /** Converts one Codex rollout into a non-persistent OMP session. */
543
+ async load(info: ForeignSessionInfo): Promise<SessionManager> {
544
+ if (info.source !== "codex") throw new Error(`Cannot load ${info.source} session with CodexSessionStore`);
545
+ let records: Record<string, unknown>[];
546
+ try {
547
+ records = await readJsonLines(info.path);
548
+ } catch (error) {
549
+ throw new Error(`Unable to read Codex session ${info.id} at ${info.path}`, { cause: error });
550
+ }
551
+ if (records.length === 0) throw new Error(`Codex session ${info.id} at ${info.path} is empty or malformed`);
552
+
553
+ const metadata = records.find(record => record.type === "session_meta" && isRecord(record.payload));
554
+ const cwd =
555
+ metadata && isRecord(metadata.payload) ? (stringField(metadata.payload, "cwd") ?? info.cwd) : info.cwd;
556
+ const manager = SessionManager.inMemory(cwd);
557
+ const canonical = canonicalTexts(records);
558
+ const converted: ConvertedRecord[] = [];
559
+ const toolNames = new Map<string, string>();
560
+ let model = "codex";
561
+ let fallbackTimestamp = info.created.getTime();
562
+ let title = info.title;
563
+
564
+ for (const record of records) {
565
+ const timestamp = timestampMillis(record.timestamp, fallbackTimestamp);
566
+ fallbackTimestamp = Math.max(fallbackTimestamp + 1, timestamp);
567
+ if (!isRecord(record.payload)) continue;
568
+ let item: ConvertedRecord | undefined;
569
+ if (record.type === "turn_context") {
570
+ const nextModel = stringField(record.payload, "model");
571
+ if (nextModel && nextModel !== model) {
572
+ model = nextModel;
573
+ item = { model, timestamp };
574
+ }
575
+ } else if (record.type === "response_item") {
576
+ item = convertedResponseItem(record.payload, timestamp, model, toolNames);
577
+ } else if (record.type === "event_msg") {
578
+ item = convertedEvent(
579
+ record.payload,
580
+ timestamp,
581
+ model,
582
+ canonical.users,
583
+ canonical.assistants,
584
+ canonical.toolCalls,
585
+ toolNames,
586
+ );
587
+ } else if (record.type === "compacted") {
588
+ const sourceSummary = stringField(record.payload, "message")?.trim();
589
+ const rawReplacementHistory = record.payload.replacement_history;
590
+ const replacementHistory =
591
+ Array.isArray(rawReplacementHistory) && rawReplacementHistory.every(isRecord)
592
+ ? rawReplacementHistory
593
+ : undefined;
594
+ if (sourceSummary || replacementHistory) {
595
+ const compactionItem = replacementHistory?.findLast(
596
+ candidate =>
597
+ (candidate.type === "compaction" && typeof candidate.encrypted_content === "string") ||
598
+ candidate.type === "compaction_summary",
599
+ );
600
+ item = {
601
+ compaction: {
602
+ summary: sourceSummary || "Context compacted by Codex.",
603
+ replacementHistory,
604
+ compactionItem,
605
+ },
606
+ timestamp,
607
+ };
608
+ }
609
+ }
610
+ if (!item) continue;
611
+ if (item.rollbackTurns) rollback(converted, item.rollbackTurns);
612
+ else converted.push(item);
613
+ if (item.followingMessage) converted.push({ message: item.followingMessage });
614
+ if (item.title) title = item.title;
615
+ }
616
+
617
+ let parentId: string | null = null;
618
+ let ordinal = 0;
619
+ for (const item of converted) {
620
+ const id = `codex-${(++ordinal).toString(36)}`;
621
+ const timestamp = new Date(item.message?.timestamp ?? item.timestamp ?? fallbackTimestamp).toISOString();
622
+ let entry: SessionEntry | undefined;
623
+ if (item.message) {
624
+ const messageEntry: SessionMessageEntry = {
625
+ type: "message",
626
+ id,
627
+ parentId,
628
+ timestamp,
629
+ message: item.message,
630
+ };
631
+ entry = messageEntry;
632
+ } else if (item.model) {
633
+ const modelEntry: ModelChangeEntry = {
634
+ type: "model_change",
635
+ id,
636
+ parentId,
637
+ timestamp,
638
+ model: `openai-codex/${item.model}`,
639
+ };
640
+ entry = modelEntry;
641
+ } else if (item.compaction) {
642
+ let preserveData: Record<string, unknown> | undefined;
643
+ if (item.compaction.replacementHistory) {
644
+ const remoteCompaction: Record<string, unknown> = {
645
+ provider: "openai-codex",
646
+ replacementHistory: item.compaction.replacementHistory,
647
+ };
648
+ if (item.compaction.compactionItem) {
649
+ remoteCompaction.compactionItem = item.compaction.compactionItem;
650
+ }
651
+ preserveData = { openaiRemoteCompaction: remoteCompaction };
652
+ }
653
+ const compactionEntry: CompactionEntry = {
654
+ type: "compaction",
655
+ id,
656
+ parentId,
657
+ timestamp,
658
+ summary: item.compaction.summary,
659
+ shortSummary: "Imported Codex compaction",
660
+ firstKeptEntryId: id,
661
+ tokensBefore: 0,
662
+ preserveData,
663
+ };
664
+ entry = compactionEntry;
665
+ }
666
+ if (!entry) continue;
667
+ manager.ingestReplicatedEntry(entry);
668
+ parentId = id;
669
+ }
670
+ if (title) await manager.setSessionName(title, "auto", "codex-import");
671
+ return manager;
672
+ }
673
+ }