super-dsh 0.1.0 → 0.1.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.
Files changed (178) hide show
  1. package/agent-agy/bridge/agy_bridge.py +250 -0
  2. package/agent-agy/bridge/gemini_openai_server.mjs +98 -0
  3. package/agent-agy/cordis.patch.yml +49 -0
  4. package/agent-agy/dist/adapter.d.ts +15 -0
  5. package/agent-agy/dist/adapter.js +42 -0
  6. package/agent-agy/dist/agent-preset-agy.d.ts +47 -0
  7. package/agent-agy/dist/agent-preset-agy.js +169 -0
  8. package/agent-agy/dist/agent-preset-projection.d.ts +31 -0
  9. package/agent-agy/dist/agent-preset-projection.js +33 -0
  10. package/agent-agy/dist/agent.d.ts +137 -0
  11. package/agent-agy/dist/agent.js +493 -0
  12. package/agent-agy/dist/agy-cli-client.d.ts +137 -0
  13. package/agent-agy/dist/agy-cli-client.js +301 -0
  14. package/agent-agy/dist/agy-client.d.ts +160 -0
  15. package/agent-agy/dist/agy-client.js +252 -0
  16. package/agent-agy/dist/agy-sessions.d.ts +24 -0
  17. package/agent-agy/dist/agy-sessions.js +116 -0
  18. package/agent-agy/dist/agy-store.d.ts +39 -0
  19. package/agent-agy/dist/agy-store.js +83 -0
  20. package/agent-agy/dist/inbox.d.ts +26 -0
  21. package/agent-agy/dist/inbox.js +53 -0
  22. package/agent-agy/dist/index.d.ts +80 -0
  23. package/agent-agy/dist/index.js +505 -0
  24. package/agent-agy/dist/models.d.ts +41 -0
  25. package/agent-agy/dist/models.js +74 -0
  26. package/agent-agy/dist/world-plugin.d.ts +9 -0
  27. package/agent-agy/dist/world-plugin.js +97 -0
  28. package/agent-agy/package.json +64 -0
  29. package/agent-claude/cordis.patch.yml +42 -0
  30. package/agent-claude/dist/adapter.js +62 -0
  31. package/agent-claude/dist/agent-preset-claude.js +169 -0
  32. package/agent-claude/dist/agent-preset-projection.js +30 -0
  33. package/agent-claude/dist/agent.js +1271 -0
  34. package/agent-claude/dist/claude-client.js +357 -0
  35. package/agent-claude/dist/claude-events.js +223 -0
  36. package/agent-claude/dist/claude-home.js +29 -0
  37. package/agent-claude/dist/commands.js +182 -0
  38. package/agent-claude/dist/content.js +134 -0
  39. package/agent-claude/dist/inbox.js +53 -0
  40. package/agent-claude/dist/index.js +555 -0
  41. package/agent-claude/dist/input-queue.js +64 -0
  42. package/agent-claude/dist/interaction.js +271 -0
  43. package/agent-claude/dist/models.js +158 -0
  44. package/agent-claude/dist/permission.js +120 -0
  45. package/agent-claude/dist/session-id.js +20 -0
  46. package/agent-claude/dist/session-map.js +165 -0
  47. package/agent-claude/dist/world-plugin.js +14 -0
  48. package/agent-claude/package.json +74 -0
  49. package/agent-codex/README.md +74 -0
  50. package/agent-codex/cordis.patch.yml +77 -0
  51. package/agent-codex/dist/adapter.js +55 -0
  52. package/agent-codex/dist/agent-preset-codex.js +171 -0
  53. package/agent-codex/dist/agent-preset-projection.js +37 -0
  54. package/agent-codex/dist/agent.js +1056 -0
  55. package/agent-codex/dist/app-home.js +65 -0
  56. package/agent-codex/dist/codex-client.js +469 -0
  57. package/agent-codex/dist/codex-events.js +297 -0
  58. package/agent-codex/dist/codex-store.js +190 -0
  59. package/agent-codex/dist/inbox.js +53 -0
  60. package/agent-codex/dist/index.js +605 -0
  61. package/agent-codex/dist/models.js +152 -0
  62. package/agent-codex/dist/permission.js +71 -0
  63. package/agent-codex/dist/session-map.js +170 -0
  64. package/agent-codex/dist/spike.js +46 -0
  65. package/agent-codex/dist/world-plugin.js +96 -0
  66. package/agent-codex/package.json +78 -0
  67. package/agent-hermes/README.md +144 -0
  68. package/agent-hermes/cordis.patch.yml +83 -0
  69. package/agent-hermes/dist/adapter.js +83 -0
  70. package/agent-hermes/dist/agent-preset-hermes.js +171 -0
  71. package/agent-hermes/dist/agent-preset-projection.js +34 -0
  72. package/agent-hermes/dist/agent.js +1068 -0
  73. package/agent-hermes/dist/hermes-client.js +953 -0
  74. package/agent-hermes/dist/hermes-events.js +289 -0
  75. package/agent-hermes/dist/hermes-store.js +158 -0
  76. package/agent-hermes/dist/inbox.js +53 -0
  77. package/agent-hermes/dist/index.js +541 -0
  78. package/agent-hermes/dist/models.js +324 -0
  79. package/agent-hermes/dist/permission.js +60 -0
  80. package/agent-hermes/dist/world-plugin.js +101 -0
  81. package/agent-hermes/package.json +76 -0
  82. package/agent-hub/cordis.patch.yml +12 -0
  83. package/agent-hub/dist/agent-roster.d.ts +34 -0
  84. package/agent-hub/dist/agent-roster.js +27 -0
  85. package/agent-hub/dist/carrier.d.ts +43 -0
  86. package/agent-hub/dist/carrier.js +241 -0
  87. package/agent-hub/dist/client-shim.d.ts +28 -0
  88. package/agent-hub/dist/client-shim.js +251 -0
  89. package/agent-hub/dist/envelope.d.ts +54 -0
  90. package/agent-hub/dist/envelope.js +92 -0
  91. package/agent-hub/dist/gateway.d.ts +28 -0
  92. package/agent-hub/dist/gateway.js +18 -0
  93. package/agent-hub/dist/index-pass.d.ts +22 -0
  94. package/agent-hub/dist/index-pass.js +47 -0
  95. package/agent-hub/dist/index.d.ts +29 -0
  96. package/agent-hub/dist/index.js +29 -0
  97. package/agent-hub/dist/labels.d.ts +45 -0
  98. package/agent-hub/dist/labels.js +83 -0
  99. package/agent-hub/dist/ownership.d.ts +37 -0
  100. package/agent-hub/dist/ownership.js +54 -0
  101. package/agent-hub/dist/roster.d.ts +28 -0
  102. package/agent-hub/dist/roster.js +38 -0
  103. package/agent-hub/dist/rpc.d.ts +38 -0
  104. package/agent-hub/dist/rpc.js +52 -0
  105. package/agent-hub/dist/spawn-world.d.ts +46 -0
  106. package/agent-hub/dist/spawn-world.js +72 -0
  107. package/agent-hub/dist/targets.d.ts +33 -0
  108. package/agent-hub/dist/targets.js +30 -0
  109. package/agent-hub/dist/world-entry.d.ts +31 -0
  110. package/agent-hub/dist/world-entry.js +51 -0
  111. package/agent-hub/dist/world-host.d.ts +69 -0
  112. package/agent-hub/dist/world-host.js +44 -0
  113. package/agent-hub/dist/world-join.d.ts +16 -0
  114. package/agent-hub/dist/world-join.js +122 -0
  115. package/agent-hub/dist/world-mount.d.ts +52 -0
  116. package/agent-hub/dist/world-mount.js +52 -0
  117. package/agent-hub/dist/world-mux.d.ts +72 -0
  118. package/agent-hub/dist/world-mux.js +224 -0
  119. package/agent-hub/dist/world-provision.d.ts +20 -0
  120. package/agent-hub/dist/world-provision.js +122 -0
  121. package/agent-hub/dist/world-web-server.d.ts +162 -0
  122. package/agent-hub/dist/world-web-server.js +248 -0
  123. package/agent-hub/lib/client/index.js +242 -0
  124. package/agent-hub/package.json +65 -0
  125. package/agent-omp/README.md +77 -0
  126. package/agent-omp/cordis.patch.yml +92 -0
  127. package/agent-omp/dist/adapter.js +36 -0
  128. package/agent-omp/dist/agent-preset-omp.js +171 -0
  129. package/agent-omp/dist/agent-preset-projection.js +36 -0
  130. package/agent-omp/dist/agent.js +1145 -0
  131. package/agent-omp/dist/app-home.js +86 -0
  132. package/agent-omp/dist/discovery.js +116 -0
  133. package/agent-omp/dist/inbox.js +53 -0
  134. package/agent-omp/dist/index.js +574 -0
  135. package/agent-omp/dist/knobs.js +50 -0
  136. package/agent-omp/dist/lazy-rpc.js +161 -0
  137. package/agent-omp/dist/mobile/gesture.js +148 -0
  138. package/agent-omp/dist/mobile/zoom-guard.js +294 -0
  139. package/agent-omp/dist/mobile-boot.js +38 -0
  140. package/agent-omp/dist/models.js +299 -0
  141. package/agent-omp/dist/omp-cli.js +147 -0
  142. package/agent-omp/dist/omp-disk-discovery.js +178 -0
  143. package/agent-omp/dist/omp-store.js +518 -0
  144. package/agent-omp/dist/pairing.js +45 -0
  145. package/agent-omp/dist/permission.js +96 -0
  146. package/agent-omp/dist/protocol.js +13 -0
  147. package/agent-omp/dist/rpc-types.js +1 -0
  148. package/agent-omp/dist/sdk-client.js +378 -0
  149. package/agent-omp/dist/sidecar-client.js +195 -0
  150. package/agent-omp/dist/store/adapter.js +47 -0
  151. package/agent-omp/dist/store/db.js +210 -0
  152. package/agent-omp/dist/store/index.js +59 -0
  153. package/agent-omp/dist/store/reconcile.js +51 -0
  154. package/agent-omp/dist/store/schema.js +40 -0
  155. package/agent-omp/dist/world-plugin.js +96 -0
  156. package/agent-omp/package.json +97 -0
  157. package/agent-omp/sidecar/main.ts +481 -0
  158. package/agent-pi/README.md +67 -0
  159. package/agent-pi/cordis.patch.yml +80 -0
  160. package/agent-pi/dist/adapter.js +53 -0
  161. package/agent-pi/dist/agent-preset-pi.js +169 -0
  162. package/agent-pi/dist/agent-preset-projection.js +29 -0
  163. package/agent-pi/dist/agent.js +949 -0
  164. package/agent-pi/dist/inbox.js +53 -0
  165. package/agent-pi/dist/index.js +595 -0
  166. package/agent-pi/dist/knobs.js +24 -0
  167. package/agent-pi/dist/models.js +200 -0
  168. package/agent-pi/dist/permission.js +66 -0
  169. package/agent-pi/dist/pi-client.js +224 -0
  170. package/agent-pi/dist/pi-events.js +155 -0
  171. package/agent-pi/dist/pi-home.js +55 -0
  172. package/agent-pi/dist/safe-json.js +25 -0
  173. package/agent-pi/dist/session-map.js +119 -0
  174. package/agent-pi/dist/world-plugin.js +97 -0
  175. package/agent-pi/package.json +77 -0
  176. package/cordis.patch.yml +22 -19
  177. package/package.json +88 -25
  178. package/client.js +0 -4
@@ -0,0 +1,297 @@
1
+ /** Watermark of streamed agent_message text (item id → last emitted full text). */
2
+ const textWatermark = new Map();
3
+ /** Items whose tool_execution_start already fired (id → true). */
4
+ const startedTools = new Set();
5
+ /** Reset the projection watermarks (test / new-session hygiene). */
6
+ export function resetProjectionState() {
7
+ textWatermark.clear();
8
+ startedTools.clear();
9
+ }
10
+ function asRecord(value) {
11
+ if (value === null || typeof value !== "object" || Array.isArray(value))
12
+ return null;
13
+ return value;
14
+ }
15
+ function asText(value) {
16
+ return typeof value === "string" ? value : "";
17
+ }
18
+ function textDelta(delta) {
19
+ const assistantMessageEvent = { type: "text_delta", contentIndex: 0, delta };
20
+ return { type: "message_update", assistantMessageEvent };
21
+ }
22
+ /**
23
+ * Identity of a codex tool item on the omp tool_execution_* wire: the display
24
+ * `args` object (omp parity) plus `argsJson` — the RAW JSON string the model
25
+ * produced, which is what the DSH `tool/call` payload requires. The SDK hands
26
+ * `mcp_tool_call.arguments` over as an unparsed string, so that one rides
27
+ * through verbatim; the other item shapes expose discrete fields (command,
28
+ * query, changes, items), so their string is the faithful JSON rendering of
29
+ * exactly those fields.
30
+ */
31
+ function toolIdentity(item) {
32
+ const json = (value) => JSON.stringify(value);
33
+ switch (item.type) {
34
+ case "command_execution": {
35
+ const args = { command: asText(item.command) };
36
+ return { toolName: "command_execution", args, argsJson: json(args) };
37
+ }
38
+ case "mcp_tool_call": {
39
+ const args = asRecord(item.arguments) ?? {};
40
+ return {
41
+ toolName: asText(item.tool) || "mcp_tool_call",
42
+ args,
43
+ argsJson: typeof item.arguments === "string" ? item.arguments : json(args),
44
+ };
45
+ }
46
+ case "file_change": {
47
+ const args = { changes: Array.isArray(item.changes) ? item.changes : [] };
48
+ return { toolName: "file_change", args, argsJson: json(args) };
49
+ }
50
+ case "web_search": {
51
+ const args = { query: asText(item.query) };
52
+ return { toolName: "web_search", args, argsJson: json(args) };
53
+ }
54
+ case "todo_list": {
55
+ const args = { items: Array.isArray(item.items) ? item.items : [] };
56
+ return { toolName: "todo_list", args, argsJson: json(args) };
57
+ }
58
+ default:
59
+ return { toolName: asText(item.type) || "tool", args: {}, argsJson: "{}" };
60
+ }
61
+ }
62
+ /** Render a codex tool item's outcome as omp tool_execution_end result blocks. */
63
+ function toolResult(item) {
64
+ switch (item.type) {
65
+ case "command_execution": {
66
+ const output = asText(item.aggregated_output);
67
+ return { content: [{ type: "text", text: output }], isError: item.status === "failed" };
68
+ }
69
+ case "mcp_tool_call": {
70
+ const result = asRecord(item.result);
71
+ const content = [];
72
+ const rawContent = result !== null && Array.isArray(result.content) ? result.content : [];
73
+ for (const block of rawContent) {
74
+ const record = asRecord(block);
75
+ // Only MCP text blocks bridge (omp parity: images/unknown are dropped).
76
+ if (record !== null && record.type === "text" && typeof record.text === "string") {
77
+ content.push({ type: "text", text: record.text });
78
+ }
79
+ }
80
+ if (content.length === 0) {
81
+ const error = asRecord(item.error);
82
+ if (error !== null && typeof error.message === "string")
83
+ content.push({ type: "text", text: error.message });
84
+ }
85
+ return { content, isError: item.status === "failed" };
86
+ }
87
+ case "file_change": {
88
+ const changes = Array.isArray(item.changes) ? item.changes : [];
89
+ const lines = [];
90
+ for (const change of changes) {
91
+ const record = asRecord(change);
92
+ if (record !== null)
93
+ lines.push(`${asText(record.kind) || "update"} ${asText(record.path)}`);
94
+ }
95
+ return { content: [{ type: "text", text: lines.join("\n") }], isError: item.status === "failed" };
96
+ }
97
+ case "web_search":
98
+ return { content: [{ type: "text", text: asText(item.query) }], isError: false };
99
+ case "todo_list": {
100
+ const items = Array.isArray(item.items) ? item.items : [];
101
+ const lines = [];
102
+ for (const entry of items) {
103
+ const record = asRecord(entry);
104
+ if (record !== null)
105
+ lines.push(`${record.completed === true ? "[x]" : "[ ]"} ${asText(record.text)}`);
106
+ }
107
+ return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
108
+ }
109
+ default:
110
+ return { content: [], isError: false };
111
+ }
112
+ }
113
+ /**
114
+ * Codex `todo_list` items → the DSH `todo/write` whole-list snapshot payload
115
+ * (`TodoItem { content, status }`; upstream packages/todo/tool-todo). The
116
+ * whole list is replaced on every write (latest-wins); a codex item only
117
+ * carries the binary completed flag, so non-completed entries are `pending`.
118
+ */
119
+ function todoSnapshot(items) {
120
+ const todos = [];
121
+ for (const entry of items) {
122
+ const record = asRecord(entry);
123
+ if (record === null)
124
+ continue;
125
+ const text = asText(record.text);
126
+ if (text === "")
127
+ continue;
128
+ todos.push({ content: text, status: record.completed === true ? "completed" : "pending" });
129
+ }
130
+ return todos;
131
+ }
132
+ function toolStart(item) {
133
+ const { toolName, args, argsJson } = toolIdentity(item);
134
+ const start = {
135
+ type: "tool_execution_start",
136
+ toolCallId: asText(item.id),
137
+ toolName,
138
+ args,
139
+ argumentsJson: argsJson,
140
+ };
141
+ if (item.type === "mcp_tool_call")
142
+ start.server = asText(item.server);
143
+ return start;
144
+ }
145
+ /**
146
+ * Terminal tool event for a completed/updated tool item. Emits the start first
147
+ * when this item's start was never projected (minimal chains), keeping the
148
+ * wire's start/end pairing intact without duplicate starts.
149
+ */
150
+ function toolEnd(item) {
151
+ const id = asText(item.id);
152
+ const { toolName } = toolIdentity(item);
153
+ const result = toolResult(item);
154
+ const end = { type: "tool_execution_end", toolCallId: id, toolName, result, isError: result.isError };
155
+ if (item.type === "command_execution") {
156
+ end.command = asText(item.command);
157
+ if (typeof item.exit_code === "number")
158
+ end.exit_code = item.exit_code;
159
+ }
160
+ if (!startedTools.has(id)) {
161
+ // start never fired for this id (fixture-style minimal chain): emit the pair.
162
+ startedTools.add(id);
163
+ return [toolStart(item), end];
164
+ }
165
+ return end;
166
+ }
167
+ function projectItem(item, phase) {
168
+ const record = asRecord(item);
169
+ if (record === null)
170
+ return null;
171
+ const id = asText(record.id);
172
+ switch (record.type) {
173
+ case "agent_message": {
174
+ const text = asText(record.text);
175
+ if (phase === "started") {
176
+ textWatermark.set(id, text);
177
+ const start = { type: "message_start", message: { role: "assistant", content: [] } };
178
+ return text === "" ? start : [start, textDelta(text)];
179
+ }
180
+ if (phase === "updated") {
181
+ const previous = textWatermark.get(id) ?? "";
182
+ if (text === previous)
183
+ return null;
184
+ const delta = text.startsWith(previous) ? text.slice(previous.length) : text;
185
+ textWatermark.set(id, text);
186
+ return textDelta(delta);
187
+ }
188
+ textWatermark.delete(id);
189
+ return {
190
+ type: "message_end",
191
+ message: { role: "assistant", content: [{ type: "text", text }] },
192
+ };
193
+ }
194
+ case "reasoning": {
195
+ const text = asText(record.text);
196
+ if (phase === "started") {
197
+ return {
198
+ type: "message_start",
199
+ message: { role: "assistant", content: [{ type: "thinking", thinking: text }] },
200
+ };
201
+ }
202
+ if (phase === "updated") {
203
+ // Deltas fold into the final thinking block (omp folds thinking_* too).
204
+ return null;
205
+ }
206
+ return {
207
+ type: "message_end",
208
+ message: { role: "assistant", content: [{ type: "thinking", thinking: text }] },
209
+ };
210
+ }
211
+ case "command_execution":
212
+ case "mcp_tool_call":
213
+ case "file_change":
214
+ case "web_search":
215
+ case "todo_list": {
216
+ if (phase === "started") {
217
+ startedTools.add(id);
218
+ return toolStart(record);
219
+ }
220
+ const terminal = record.status === "completed" || record.status === "failed";
221
+ if (phase === "updated") {
222
+ return terminal ? toolEnd(record) : null; // still in_progress → nothing new
223
+ }
224
+ const events = toolEnd(record); // completed is terminal by definition (toolEnd dedups the start)
225
+ if (record.type !== "todo_list")
226
+ return events;
227
+ // `item.todo_list` ALSO projects the DSH `todo/write` whole-list snapshot
228
+ // (log-only, latest-wins) after the tool pair — the tool pair stays.
229
+ const todos = todoSnapshot(Array.isArray(record.items) ? record.items : []);
230
+ return [...(Array.isArray(events) ? events : [events]), { type: "todo_write", todos }];
231
+ }
232
+ case "error": {
233
+ if (phase === "completed") {
234
+ // Non-fatal item-level error: ride omp's stopReason "error" assistant
235
+ // message shape so agent.ts's ompFailure() classifies it downstream.
236
+ return {
237
+ type: "message_end",
238
+ message: {
239
+ role: "assistant",
240
+ content: [],
241
+ stopReason: "error",
242
+ errorMessage: asText(record.message),
243
+ },
244
+ };
245
+ }
246
+ return null;
247
+ }
248
+ default:
249
+ return null;
250
+ }
251
+ }
252
+ /**
253
+ * Project one codex SDK ThreadEvent (duck-typed, never imported into dist)
254
+ * onto the omp wire vocabulary. Returns one event, an ordered array, or null
255
+ * for ignorable/malformed input. Never throws.
256
+ */
257
+ export function projectThreadEvent(evt) {
258
+ try {
259
+ const record = asRecord(evt);
260
+ if (record === null || typeof record.type !== "string")
261
+ return null;
262
+ switch (record.type) {
263
+ case "thread.started": {
264
+ const start = { type: "agent_start" };
265
+ if (typeof record.thread_id === "string" && record.thread_id !== "") {
266
+ start.data = { threadId: record.thread_id };
267
+ }
268
+ return start;
269
+ }
270
+ case "turn.started":
271
+ return { type: "turn_start" };
272
+ case "item.started":
273
+ return projectItem(record.item, "started");
274
+ case "item.updated":
275
+ return projectItem(record.item, "updated");
276
+ case "item.completed":
277
+ return projectItem(record.item, "completed");
278
+ case "turn.completed": {
279
+ const usage = asRecord(record.usage);
280
+ return usage === null ? { type: "turn_end" } : { type: "turn_end", data: { usage } };
281
+ }
282
+ case "turn.failed": {
283
+ const error = asRecord(record.error);
284
+ return error === null ? { type: "turn_end" } : { type: "turn_end", data: { error } };
285
+ }
286
+ case "error":
287
+ // Fatal stream error: the client reports it via onFailure; the wire
288
+ // stream stays silent (S5 ignorable-event invariant).
289
+ return null;
290
+ default:
291
+ return null;
292
+ }
293
+ }
294
+ catch {
295
+ return null;
296
+ }
297
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Codex home resolution + the rollout-HEAD metadata reader.
3
+ *
4
+ * The listing/replay scanner that used to live here is GONE (2026-09-16): the
5
+ * DSH session log is now the session authority (upstream `sessionPersistence`
6
+ * owns list/read/replay), so the adapter no longer derives session ids or
7
+ * transcripts from Codex's rollout store. What remains is:
8
+ *
9
+ * - home resolution (`<DSH home>/agents/codex`, spec S7) with the prod-home
10
+ * - home resolution (native `~/.codex`, never redirected) with the prod-home
11
+ * - {@link readRolloutHead}: a bounded, memoized reader of ONE rollout's
12
+ * FIRST line (`session_meta`), used only to recover the Codex system
13
+ * prompt (`payload.base_instructions`) and the recorded cwd. It never
14
+ * parses the transcript and never throws.
15
+ *
16
+ * Single-direction valve (spec S7): this module NEVER writes under the codex
17
+ * home; the rollout stays Codex's transcript of record.
18
+ */
19
+ import { closeSync, openSync, readdirSync, readSync, statSync } from "node:fs";
20
+ import { homedir } from "node:os";
21
+ import { join, resolve } from "node:path";
22
+ /** Prod homes that must never be resolved into (repo red line). */
23
+ const PROD_HOMES = [join(homedir(), ".dsh"), join(homedir(), ".superd")];
24
+ export function assertNotProdHome(path, label) {
25
+ if (PROD_HOMES.includes(path) || PROD_HOMES.some((p) => path.startsWith(`${p}/`))) {
26
+ throw new Error(`codex-store: refusing prod home as ${label}: ${path}`);
27
+ }
28
+ }
29
+ /**
30
+ * Resolve the Codex CLI's app-data home — the NATIVE `~/.codex` (user ruling
31
+ * 2026-09-17, pi precedent): the installed app keeps its own home, so the
32
+ * adapter never redirects it when spawning the SDK/CLI and never seeds config
33
+ * into it. Precedence: `CODEX_HOME` (tests / operator override), then the
34
+ * native default.
35
+ *
36
+ * The world's DSH home (`<dshHome>/agents/codex`) is NOT an input here anymore
37
+ * — it anchors adapter-owned DSH state only (session-map.ts). A home without
38
+ * `config.toml` would make the CLI fall back to OpenAI's own endpoint
39
+ * (region-blocked here), but that is the operator's native home: provisioning
40
+ * it is the operator's business (test helper: scripts/setup-codex-home.mjs).
41
+ */
42
+ export function resolveCodexHome() {
43
+ if (process.env.CODEX_HOME !== undefined) {
44
+ const env = resolve(process.env.CODEX_HOME);
45
+ assertNotProdHome(env, "codex home");
46
+ return env;
47
+ }
48
+ const native = join(homedir(), ".codex");
49
+ assertNotProdHome(native, "codex home");
50
+ return native;
51
+ }
52
+ /**
53
+ * Take an explicit codex home as-is (prod-guarded) — for callers that already
54
+ * hold the final path (models.ts, the spike); distinct from
55
+ * {@link resolveCodexHome}, which resolves the native default.
56
+ */
57
+ export function useCodexHome(path) {
58
+ const resolved = resolve(path);
59
+ assertNotProdHome(resolved, "codex home");
60
+ return resolved;
61
+ }
62
+ /** Bounded read budget: `session_meta` is the FIRST line, so this is generous. */
63
+ const HEAD_BYTES = 64 * 1024;
64
+ /** Memoized heads by thread id — the head line is immutable once written. */
65
+ const headCache = new Map();
66
+ /**
67
+ * Locate one thread's rollout file under `<codexHome>/sessions/<Y>/<M>/<D>/`.
68
+ * The rollout file name embeds the thread id, so presence is the match test;
69
+ * a miss is retried (a thread's rollout may not exist yet at first call).
70
+ */
71
+ function findRolloutFile(codexHome, threadId) {
72
+ const root = join(codexHome, "sessions");
73
+ try {
74
+ for (const year of readdirSync(root, { withFileTypes: true })) {
75
+ if (!year.isDirectory())
76
+ continue;
77
+ for (const month of readdirSync(join(root, year.name), { withFileTypes: true })) {
78
+ if (!month.isDirectory())
79
+ continue;
80
+ for (const day of readdirSync(join(root, year.name, month.name), { withFileTypes: true })) {
81
+ if (!day.isDirectory())
82
+ continue;
83
+ const dir = join(root, year.name, month.name, day.name);
84
+ for (const file of readdirSync(dir)) {
85
+ if (file.endsWith(".jsonl") && file.includes(threadId))
86
+ return join(dir, file);
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+ catch {
93
+ return undefined;
94
+ }
95
+ return undefined;
96
+ }
97
+ /** Read (at most) the first line of a file; fail-soft. */
98
+ function readFirstLine(path) {
99
+ let fd;
100
+ try {
101
+ fd = openSync(path, "r");
102
+ }
103
+ catch {
104
+ return undefined;
105
+ }
106
+ try {
107
+ const chunk = Buffer.allocUnsafe(HEAD_BYTES);
108
+ const read = readSync(fd, chunk, 0, chunk.length, 0);
109
+ if (read <= 0)
110
+ return undefined;
111
+ const text = chunk.subarray(0, read).toString("utf8");
112
+ const end = text.indexOf("\n");
113
+ return end === -1 ? text : text.slice(0, end);
114
+ }
115
+ catch {
116
+ return undefined;
117
+ }
118
+ finally {
119
+ closeSync(fd);
120
+ }
121
+ }
122
+ /**
123
+ * Read the `session_meta` head of the rollout belonging to `threadId`:
124
+ * the Codex system prompt and the recorded cwd. Memoized per thread id (the
125
+ * head line never changes); fail-soft — an unknown thread, an unwritten
126
+ * rollout, or a malformed head all yield `undefined`.
127
+ */
128
+ /**
129
+ * Codex writes `payload.base_instructions` either as a plain string or as an
130
+ * object `{ text }` (observed 2026-09-16 on the SDK line: the object form is
131
+ * what the vendored binary emits today). Accept both; anything else is absent.
132
+ */
133
+ function baseInstructionsText(value) {
134
+ if (typeof value === "string")
135
+ return value.trim() === "" ? undefined : value;
136
+ if (value !== null && typeof value === "object") {
137
+ const text = value["text"];
138
+ if (typeof text === "string" && text.trim() !== "")
139
+ return text;
140
+ }
141
+ return undefined;
142
+ }
143
+ export function readRolloutHead(codexHome, threadId) {
144
+ const cached = headCache.get(threadId);
145
+ if (cached !== undefined)
146
+ return cached;
147
+ const file = findRolloutFile(codexHome, threadId);
148
+ if (file === undefined)
149
+ return undefined;
150
+ // Cheap existence/size gate before paying for a read.
151
+ try {
152
+ if (!statSync(file).isFile())
153
+ return undefined;
154
+ }
155
+ catch {
156
+ return undefined;
157
+ }
158
+ const line = readFirstLine(file);
159
+ if (line === undefined)
160
+ return undefined;
161
+ let record;
162
+ try {
163
+ record = JSON.parse(line);
164
+ }
165
+ catch {
166
+ return undefined;
167
+ }
168
+ if (record === null || typeof record !== "object")
169
+ return undefined;
170
+ const rec = record;
171
+ if (rec["type"] !== "session_meta")
172
+ return undefined;
173
+ const payload = rec["payload"];
174
+ if (payload === null || typeof payload !== "object")
175
+ return undefined;
176
+ const meta = payload;
177
+ const head = {
178
+ ...(typeof meta["id"] === "string" && meta["id"] !== "" ? { threadId: meta["id"] } : {}),
179
+ ...(typeof meta["cwd"] === "string" && meta["cwd"] !== "" ? { cwd: meta["cwd"] } : {}),
180
+ ...(baseInstructionsText(meta["base_instructions"]) === undefined
181
+ ? {}
182
+ : { baseInstructions: baseInstructionsText(meta["base_instructions"]) }),
183
+ };
184
+ headCache.set(threadId, head);
185
+ return head;
186
+ }
187
+ /** Test hygiene: drop memoized heads. */
188
+ export function clearRolloutHeadCache() {
189
+ headCache.clear();
190
+ }
@@ -0,0 +1,53 @@
1
+ export class Inbox {
2
+ #nextTurn = [];
3
+ #nextStep = [];
4
+ get nextTurn() {
5
+ return this.#nextTurn;
6
+ }
7
+ get nextStep() {
8
+ return this.#nextStep;
9
+ }
10
+ clear() {
11
+ this.#nextTurn = [];
12
+ this.#nextStep = [];
13
+ }
14
+ append(target, message) {
15
+ this.#list(target).push(message);
16
+ }
17
+ prepend(target, message) {
18
+ this.#list(target).unshift(message);
19
+ }
20
+ replace(messageId, newMessage) {
21
+ const list = this.#find(messageId);
22
+ if (list === undefined)
23
+ return false;
24
+ const index = list.findIndex((message) => message.id === messageId);
25
+ if (index < 0)
26
+ return false;
27
+ list.splice(index, 1, newMessage);
28
+ return true;
29
+ }
30
+ remove(messageId) {
31
+ const list = this.#find(messageId);
32
+ if (list === undefined)
33
+ return false;
34
+ const index = list.findIndex((message) => message.id === messageId);
35
+ if (index < 0)
36
+ return false;
37
+ list.splice(index, 1);
38
+ return true;
39
+ }
40
+ splice(target, start, deleteCount, inserted) {
41
+ return this.#list(target).splice(start, deleteCount, ...inserted);
42
+ }
43
+ #list(target) {
44
+ return target === "next-turn" ? this.#nextTurn : this.#nextStep;
45
+ }
46
+ #find(messageId) {
47
+ if (this.#nextTurn.some((message) => message.id === messageId))
48
+ return this.#nextTurn;
49
+ if (this.#nextStep.some((message) => message.id === messageId))
50
+ return this.#nextStep;
51
+ return undefined;
52
+ }
53
+ }