@hienlh/ppm 0.20.0 → 0.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/CLAUDE.md +1 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +5 -1
- package/dist/web/assets/accounts-settings-section-DzDmIsPe.js +1 -0
- package/dist/web/assets/{ai-resource-editor-BgYIPGVP.js → ai-resource-editor-D7C9wmwA.js} +1 -1
- package/dist/web/assets/{ai-settings-section-iBRCQvBR.js → ai-settings-section-BW5a07-Z.js} +1 -1
- package/dist/web/assets/{ai-settings-section-Cywmtnps.js → ai-settings-section-DZuwllLy.js} +1 -1
- package/dist/web/assets/{appearance-settings-section-mhu9T6Fk.js → appearance-settings-section-RfH2N1rj.js} +1 -1
- package/dist/web/assets/chat-tab-Bz0pk0_E.js +15 -0
- package/dist/web/assets/{code-editor-Cv_pzHo7.js → code-editor-C4o1SyuO.js} +2 -2
- package/dist/web/assets/{conflict-editor-BnWaMSEa.js → conflict-editor-Dk65mBIe.js} +1 -1
- package/dist/web/assets/{csv-preview-DG4YAabr.js → csv-preview-DENi3CFz.js} +1 -1
- package/dist/web/assets/{database-viewer-B-wA2qT-.js → database-viewer-BiBC7ulB.js} +1 -1
- package/dist/web/assets/{diff-viewer-DRtzdfXk.js → diff-viewer-CSvZJHak.js} +1 -1
- package/dist/web/assets/{edit-diff-preview-6tzdk7Pn.js → edit-diff-preview-xdDQvPhs.js} +1 -1
- package/dist/web/assets/{explorer-body-uh8oJp6J.js → explorer-body-QXS0h4AF.js} +1 -1
- package/dist/web/assets/{explorer-window-content-DLn_4egF.js → explorer-window-content-CQgqoRsj.js} +1 -1
- package/dist/web/assets/{extension-manager-section-C_ZGYvWf.js → extension-manager-section-CyC7wX35.js} +1 -1
- package/dist/web/assets/{extension-webview-DbFWxJpj.js → extension-webview-Da8SIlRa.js} +1 -1
- package/dist/web/assets/{files-settings-section-BrUbJglA.js → files-settings-section-D8cebM51.js} +1 -1
- package/dist/web/assets/{general-settings-section-B9ZPZVso.js → general-settings-section-1HVTvlIw.js} +1 -1
- package/dist/web/assets/{git-log-panel-C0jnhmgC.js → git-log-panel-C7a4rp8f.js} +1 -1
- package/dist/web/assets/{glide-data-grid-CZVKIum-.js → glide-data-grid-BT9p_4AN.js} +1 -1
- package/dist/web/assets/{group-chat-tab-DnPtmbPc.js → group-chat-tab-BUHGnUyW.js} +1 -1
- package/dist/web/assets/index-CuYtJ5xF.css +2 -0
- package/dist/web/assets/{index-CHQAPZIX.js → index-kGmcC9cb.js} +5 -5
- package/dist/web/assets/{jira-watcher-section-Bjldyhru.js → jira-watcher-section-BxzwW4uQ.js} +1 -1
- package/dist/web/assets/{keybindings-store-DBVXZKp8.js → keybindings-store-DjJum4OS.js} +1 -1
- package/dist/web/assets/{keyboard-shortcuts-section-D7jaCPrv.js → keyboard-shortcuts-section-DDKG3BU1.js} +1 -1
- package/dist/web/assets/{markdown-renderer-B4B5oxQS.js → markdown-renderer-dHeak2mE.js} +2 -2
- package/dist/web/assets/{markdown-renderer-DwHh-jd_.js → markdown-renderer-iBw1fCI_.js} +1 -1
- package/dist/web/assets/{mobile-explorer-sheet-JJxZYWkV.js → mobile-explorer-sheet-CzL8SLtz.js} +1 -1
- package/dist/web/assets/{notification-store-CCWNUHsS.js → notification-store-DVi6Ds71.js} +1 -1
- package/dist/web/assets/{notifications-settings-section-DbbX7-Qp.js → notifications-settings-section-shQ-ugKh.js} +1 -1
- package/dist/web/assets/{pdf-preview-DxNjWa1v.js → pdf-preview-DB26KSzm.js} +1 -1
- package/dist/web/assets/{postgres-viewer-BScvk5ev.js → postgres-viewer-CJe2meyF.js} +1 -1
- package/dist/web/assets/{ppmbot-settings-section-jFA9Ncgo.js → ppmbot-settings-section-Ba4c9AI9.js} +1 -1
- package/dist/web/assets/{process-table-BexeYWQm.js → process-table-PXwLsMuG.js} +1 -1
- package/dist/web/assets/{proxy-settings-section-C_seKq56.js → proxy-settings-section-BHBhK6HC.js} +1 -1
- package/dist/web/assets/{query-audit-section-BWXJmPDV.js → query-audit-section-BD5ZVqDz.js} +1 -1
- package/dist/web/assets/{remote-desktop-mobile-sheet-B--PeL5i.js → remote-desktop-mobile-sheet-Di4QG8VX.js} +2 -2
- package/dist/web/assets/{remote-desktop-mobile-view-jgn8nmQa.js → remote-desktop-mobile-view-DzoZvAqc.js} +1 -1
- package/dist/web/assets/{remote-desktop-readiness-gate-CjDth-W_.js → remote-desktop-readiness-gate-CHO-KJ9Z.js} +1 -1
- package/dist/web/assets/{remote-desktop-window-content-CrHAUA03.js → remote-desktop-window-content-X324gYbW.js} +1 -1
- package/dist/web/assets/{schedules-settings-section-DMFn3mYL.js → schedules-settings-section-a4-h6THc.js} +1 -1
- package/dist/web/assets/{settings-body-Dz2vPTEP.js → settings-body-Fq7BVDw2.js} +2 -2
- package/dist/web/assets/{settings-tab-B5QbsvS4.js → settings-tab-CVLQy-ie.js} +1 -1
- package/dist/web/assets/{settings-window-content-iM2bPoX5.js → settings-window-content-Bi7M0iaZ.js} +1 -1
- package/dist/web/assets/{sql-query-editor-C0mGgLfX.js → sql-query-editor-D1r265fl.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-Bs3RKf1X.js → sqlite-viewer-DHkIotRl.js} +1 -1
- package/dist/web/assets/{system-monitor-body-DvUmEdNe.js → system-monitor-body-y6VUkYTL.js} +2 -2
- package/dist/web/assets/{system-monitor-tab-CHZ5F_ZE.js → system-monitor-tab-DRAOejhk.js} +1 -1
- package/dist/web/assets/{system-monitor-window-content-DjzqoeCN.js → system-monitor-window-content-B5i57xHD.js} +1 -1
- package/dist/web/assets/{tab-host-window-content-DKeAr5mD.js → tab-host-window-content-SkyPFFlc.js} +1 -1
- package/dist/web/assets/{team-member-sheet-Cl9ZB3dl.js → team-member-sheet-BB63u-Lf.js} +2 -2
- package/dist/web/assets/{team-member-window-content-DfP7WdFD.js → team-member-window-content-D6tVTXuV.js} +1 -1
- package/dist/web/assets/{terminal-tab-J1Fqa_rm.js → terminal-tab-BYaHAkI5.js} +1 -1
- package/dist/web/assets/{tool-cards-DQghSuKX.js → tool-cards-DRJu20sL.js} +3 -3
- package/dist/web/assets/use-accounts-data-Ba1_uvEE.js +1 -0
- package/dist/web/assets/{use-monaco-theme-aa4-wjKQ.js → use-monaco-theme-Uakgj3hJ.js} +1 -1
- package/dist/web/assets/{use-remote-desktop-display-choice-B-TpQ-Ua.js → use-remote-desktop-display-choice-Cd3Xalca.js} +1 -1
- package/dist/web/assets/{use-websocket-BBslmv3D.js → use-websocket-BC_EGf5q.js} +1 -1
- package/dist/web/assets/{video-preview-Bd0lcK3d.js → video-preview-BUb43zWb.js} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/docs/deployment-guide.md +10 -0
- package/package.json +1 -1
- package/src/providers/codex-app-server/codex-event-mapper.ts +11 -0
- package/src/providers/codex-app-server/codex-history.ts +118 -45
- package/src/providers/codex-app-server/codex-provider.ts +277 -15
- package/src/providers/codex-app-server/codex-rollout-header.ts +99 -0
- package/src/providers/codex-app-server/codex-rollout-items.ts +6 -0
- package/src/providers/codex-app-server/codex-rollout-transfer.ts +58 -0
- package/src/providers/codex-app-server/codex-subagent-thread.ts +103 -0
- package/src/providers/codex-app-server/codex-usage-limit.ts +69 -0
- package/src/providers/codex-app-server/codex-usage-parser.ts +31 -14
- package/src/server/routes/chat.ts +23 -2
- package/src/server/routes/codex-accounts.ts +32 -3
- package/src/server/ws/chat.ts +15 -11
- package/src/services/codex-account-cooldown.ts +56 -0
- package/src/services/codex-account-login.ts +109 -34
- package/src/services/codex-account.service.ts +56 -10
- package/src/services/codex-login-callback.ts +57 -0
- package/src/web/components/chat/chat-tab.tsx +8 -2
- package/src/web/components/chat/codex-usage-panel.tsx +129 -165
- package/src/web/components/chat/edit-diff-preview.tsx +14 -7
- package/src/web/components/chat/usage-badge.tsx +38 -129
- package/src/web/components/chat/usage-panel-shell.tsx +153 -0
- package/src/web/components/settings/accounts/account-card.tsx +20 -3
- package/src/web/components/settings/accounts/codex-accounts-section.tsx +9 -2
- package/src/web/components/settings/accounts/codex-add-account-dialog.tsx +47 -27
- package/src/web/components/settings/accounts/codex-usage-rows.tsx +17 -6
- package/src/web/components/settings/accounts/use-codex-accounts.ts +90 -44
- package/src/web/components/shared/markdown-code-block.tsx +10 -5
- package/src/web/components/ui/tooltip.tsx +14 -1
- package/src/web/hooks/use-usage.ts +13 -1
- package/dist/web/assets/accounts-settings-section-BQyru31C.js +0 -1
- package/dist/web/assets/chat-tab-Bs9OD6ss.js +0 -15
- package/dist/web/assets/index-CoaVwAF6.css +0 -2
- package/dist/web/assets/use-accounts-data-KWcMgRlc.js +0 -1
package/docs/deployment-guide.md
CHANGED
|
@@ -69,6 +69,16 @@ ppm init -y # non-interactive: defaults + auto-generated password
|
|
|
69
69
|
Writes `~/.ppm/ppm.db` (SQLite) and generates an auth token. `ppm start` runs the wizard
|
|
70
70
|
automatically if no config exists.
|
|
71
71
|
|
|
72
|
+
### Add a Codex account
|
|
73
|
+
|
|
74
|
+
In Settings → Accounts → Codex → Add account, choose one of three methods:
|
|
75
|
+
|
|
76
|
+
- **Browser login:** Open the ChatGPT sign-in link. On the same computer, completion can be automatic. On a phone or another computer, copy the full `http://localhost:1455/auth/callback?...` URL from the address bar after sign-in, even if the page cannot load, and paste it into **Callback URL** in PPM. Submit it and keep the dialog open until the account appears. This method does not require enabling device-code login.
|
|
77
|
+
- **Device code:** Open the verification link and enter the displayed code. Enable device-code login in ChatGPT first when required.
|
|
78
|
+
- **API key:** Paste an OpenAI API key; this uses API billing rather than the ChatGPT subscription.
|
|
79
|
+
|
|
80
|
+
Browser login expires after ten minutes. Closing the dialog cancels a pending login. Only one browser login can run at a time; if its callback port is busy, finish the other login or use device code. Callback URLs contain temporary credentials: paste them only into the login dialog.
|
|
81
|
+
|
|
72
82
|
### Dev vs Production Config
|
|
73
83
|
|
|
74
84
|
| Profile | Database | Conventional port |
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import type { ChatEvent } from "../provider.interface.ts";
|
|
|
2
2
|
import type { TurnUsage } from "../../shared/turn-usage.ts";
|
|
3
3
|
import { redactTruncate } from "./codex-redact.ts";
|
|
4
4
|
import { diffToOldNew, changeToToolUse } from "./codex-patch.ts";
|
|
5
|
+
import { parseSubagentActivity, subagentToolResult, subagentToolUse } from "./codex-subagent-thread.ts";
|
|
5
6
|
|
|
6
7
|
/** ThreadItem variants that are NOT tool calls (text/metadata). Everything else
|
|
7
8
|
* is treated as a tool so nothing is ever silently hidden — known types get a
|
|
@@ -172,6 +173,11 @@ export function mapCodexEvent(notif: Notif, sessionId: string): ChatEvent[] {
|
|
|
172
173
|
case "item/started": {
|
|
173
174
|
const item = asObj(p.item) as Item;
|
|
174
175
|
if (item.type === "contextCompaction") return [{ type: "system", subtype: "compacting" }];
|
|
176
|
+
// A spawned agent is a card, not a tool: its start and its completion are
|
|
177
|
+
// two records naming one thread, and the generic mapping rendered each as
|
|
178
|
+
// a card of its own with the raw item as its body.
|
|
179
|
+
const subagent = parseSubagentActivity(item);
|
|
180
|
+
if (subagent) return [subagentToolUse(subagent)];
|
|
175
181
|
if (item.type && !NON_TOOL_ITEM_TYPES.has(item.type)) return [itemToToolUse(item)];
|
|
176
182
|
return [];
|
|
177
183
|
}
|
|
@@ -183,6 +189,11 @@ export function mapCodexEvent(notif: Notif, sessionId: string): ChatEvent[] {
|
|
|
183
189
|
case "item/completed": {
|
|
184
190
|
const item = asObj(p.item) as Item;
|
|
185
191
|
if (item.type === "contextCompaction") return [{ type: "system", subtype: "compact_done" }];
|
|
192
|
+
const subagent = parseSubagentActivity(item);
|
|
193
|
+
// Answers the card opened at `started` — keyed on the thread, because the
|
|
194
|
+
// two records carry different item ids. The agent's own report is not on
|
|
195
|
+
// this record; the card picks it up when the transcript is read back.
|
|
196
|
+
if (subagent) return [subagent.done ? subagentToolResult(subagent) : subagentToolUse(subagent)];
|
|
186
197
|
if (item.type && !NON_TOOL_ITEM_TYPES.has(item.type)) {
|
|
187
198
|
// Image generation is announced before the picture exists: at `started`
|
|
188
199
|
// there is no saved file and no revised prompt, so the call it produced
|
|
@@ -5,6 +5,11 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { redactTruncate } from "./codex-redact.ts";
|
|
6
6
|
import { parseApplyPatch, changeToToolUse } from "./codex-patch.ts";
|
|
7
7
|
import { mapRolloutItem } from "./codex-rollout-items.ts";
|
|
8
|
+
import { completeLines, parseLine, readRolloutHeader, type RolloutHeader } from "./codex-rollout-header.ts";
|
|
9
|
+
import {
|
|
10
|
+
finalAssistantText, subagentToolResult, subagentToolUse, transcriptToEvents,
|
|
11
|
+
type SubagentTranscript,
|
|
12
|
+
} from "./codex-subagent-thread.ts";
|
|
8
13
|
|
|
9
14
|
/**
|
|
10
15
|
* Independent parser for Codex rollout JSONL transcripts
|
|
@@ -18,30 +23,12 @@ import { mapRolloutItem } from "./codex-rollout-items.ts";
|
|
|
18
23
|
* scaffolding).
|
|
19
24
|
*/
|
|
20
25
|
|
|
21
|
-
interface RolloutLine {
|
|
22
|
-
timestamp?: string;
|
|
23
|
-
type?: string;
|
|
24
|
-
payload?: Record<string, unknown>;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
26
|
/** Normalize a path for cross-platform comparison (case-insensitive on win32). */
|
|
28
27
|
function normPath(p: string): string {
|
|
29
28
|
const r = resolve(p);
|
|
30
29
|
return process.platform === "win32" ? r.toLowerCase() : r;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
/** Split into complete, newline-terminated lines only (drop a trailing partial). */
|
|
34
|
-
function completeLines(text: string): string[] {
|
|
35
|
-
const lines = text.split("\n");
|
|
36
|
-
// If the text does not end in a newline, the last element is a partial line.
|
|
37
|
-
if (!text.endsWith("\n")) lines.pop();
|
|
38
|
-
return lines.filter((l) => l.trim() !== "");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function parseLine(line: string): RolloutLine | null {
|
|
42
|
-
try { return JSON.parse(line) as RolloutLine; } catch { return null; }
|
|
43
|
-
}
|
|
44
|
-
|
|
45
32
|
/** Extract plain text from a response_item message content array. */
|
|
46
33
|
function contentToText(content: unknown): string {
|
|
47
34
|
if (typeof content === "string") return content;
|
|
@@ -106,10 +93,20 @@ function fnOutputToToolResult(p: Record<string, unknown>): ChatEvent {
|
|
|
106
93
|
* tool calls come from `response_item` function_call / function_call_output and
|
|
107
94
|
* are nested into the assistant turn's `events` so the chat UI renders tool cards.
|
|
108
95
|
*/
|
|
109
|
-
export function parseRolloutJsonl(
|
|
96
|
+
export function parseRolloutJsonl(
|
|
97
|
+
text: string,
|
|
98
|
+
opts?: {
|
|
99
|
+
preCompact?: boolean;
|
|
100
|
+
/** Reads a spawned thread's transcript so its card can nest it. */
|
|
101
|
+
loadSubagent?: (threadId: string) => SubagentTranscript | null;
|
|
102
|
+
},
|
|
103
|
+
): ChatMessage[] {
|
|
110
104
|
const messages: ChatMessage[] = [];
|
|
111
105
|
let i = 0;
|
|
112
106
|
let pendingEvents: ChatEvent[] = [];
|
|
107
|
+
// Kept so the completion can answer the card the spawn opened, with the
|
|
108
|
+
// report the child ended on.
|
|
109
|
+
const subagentTranscripts = new Map<string, SubagentTranscript | null>();
|
|
113
110
|
|
|
114
111
|
// Newer codex records every finished step as an `item_completed` event AND
|
|
115
112
|
// keeps the raw model exchange in `response_item` records. Both describe the
|
|
@@ -153,6 +150,15 @@ export function parseRolloutJsonl(text: string, opts?: { preCompact?: boolean })
|
|
|
153
150
|
flushAssistant(mapped.text, ts);
|
|
154
151
|
} else if (mapped.kind === "events") {
|
|
155
152
|
pendingEvents.push(...mapped.events);
|
|
153
|
+
} else if (mapped.kind === "subagent") {
|
|
154
|
+
const { activity } = mapped;
|
|
155
|
+
if (!subagentTranscripts.has(activity.threadId)) {
|
|
156
|
+
subagentTranscripts.set(activity.threadId, opts?.loadSubagent?.(activity.threadId) ?? null);
|
|
157
|
+
}
|
|
158
|
+
const transcript = subagentTranscripts.get(activity.threadId) ?? null;
|
|
159
|
+
pendingEvents.push(activity.done
|
|
160
|
+
? subagentToolResult(activity, transcript)
|
|
161
|
+
: subagentToolUse(activity, transcript));
|
|
156
162
|
}
|
|
157
163
|
} else if (p.type === "thread_rolled_back") {
|
|
158
164
|
// codex doesn't truncate the rollout file on rollback/fork — it appends this
|
|
@@ -199,21 +205,10 @@ export function parseRolloutJsonl(text: string, opts?: { preCompact?: boolean })
|
|
|
199
205
|
return messages;
|
|
200
206
|
}
|
|
201
207
|
|
|
202
|
-
/** Read
|
|
203
|
-
function readSessionMeta(file: string
|
|
208
|
+
/** Read a rollout file's header (session_meta, plus its title when asked). */
|
|
209
|
+
function readSessionMeta(file: string, opts?: { withTitle?: boolean }): RolloutHeader | null {
|
|
204
210
|
try {
|
|
205
|
-
|
|
206
|
-
for (const line of completeLines(text)) {
|
|
207
|
-
const rec = parseLine(line);
|
|
208
|
-
if (rec?.type === "session_meta") {
|
|
209
|
-
const p = rec.payload ?? {};
|
|
210
|
-
return {
|
|
211
|
-
id: typeof p.id === "string" ? p.id : undefined,
|
|
212
|
-
cwd: typeof p.cwd === "string" ? p.cwd : undefined,
|
|
213
|
-
timestamp: typeof p.timestamp === "string" ? p.timestamp : rec.timestamp,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
}
|
|
211
|
+
return readRolloutHeader(readFileSync(file, "utf-8"), opts);
|
|
217
212
|
} catch { /* unreadable file → excluded (fail-closed) */ }
|
|
218
213
|
return null;
|
|
219
214
|
}
|
|
@@ -241,6 +236,12 @@ function threadIdFromName(file: string): string | null {
|
|
|
241
236
|
* is included ONLY when its session_meta cwd resolves and matches `requestedCwd`
|
|
242
237
|
* (normalized; case-insensitive on win32). Unattributable rollouts are excluded
|
|
243
238
|
* — `~/.codex/sessions` holds every project's transcripts.
|
|
239
|
+
*
|
|
240
|
+
* A spawned subagent's rollout is excluded too. It carries the same cwd as the
|
|
241
|
+
* conversation that spawned it, so cwd alone let one subagent per spawn into the
|
|
242
|
+
* session list — each opening with no prompt and no ending, because a subagent's
|
|
243
|
+
* task never appears in its own transcript. Its work shows in the parent, on the
|
|
244
|
+
* Agent card for that spawn.
|
|
244
245
|
*/
|
|
245
246
|
export function listCodexRollouts(
|
|
246
247
|
sessionsDir: string,
|
|
@@ -253,9 +254,10 @@ export function listCodexRollouts(
|
|
|
253
254
|
const sessions: SessionInfo[] = [];
|
|
254
255
|
|
|
255
256
|
for (const file of files) {
|
|
256
|
-
const meta = readSessionMeta(file);
|
|
257
|
+
const meta = readSessionMeta(file, { withTitle: true });
|
|
257
258
|
if (!meta?.cwd) continue; // fail-closed: no cwd → exclude
|
|
258
259
|
if (normPath(meta.cwd) !== target) continue;
|
|
260
|
+
if (meta.parentThreadId) continue; // one step of another session, not a session
|
|
259
261
|
|
|
260
262
|
const id = meta.id ?? threadIdFromName(file);
|
|
261
263
|
if (!id) continue;
|
|
@@ -266,7 +268,7 @@ export function listCodexRollouts(
|
|
|
266
268
|
sessions.push({
|
|
267
269
|
id,
|
|
268
270
|
providerId,
|
|
269
|
-
title: "Codex session",
|
|
271
|
+
title: meta.title ?? "Codex session",
|
|
270
272
|
createdAt: meta.timestamp ?? new Date().toISOString(),
|
|
271
273
|
updatedAt,
|
|
272
274
|
});
|
|
@@ -287,20 +289,89 @@ export function listCodexRollouts(
|
|
|
287
289
|
*/
|
|
288
290
|
export function findRolloutByThreadId(sessionsDir: string, threadId: string, requestedCwd?: string): string | null {
|
|
289
291
|
const target = requestedCwd != null ? normPath(requestedCwd) : null;
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
292
|
+
const files = findRolloutFiles(sessionsDir);
|
|
293
|
+
// The filename carries the thread id, so try those first and read nothing for
|
|
294
|
+
// the rest unless the name match is rejected. Reading a transcript to identify
|
|
295
|
+
// it costs megabytes, and resolving one is now per spawned subagent too.
|
|
296
|
+
const named = (f: string) => threadIdFromName(f) === threadId;
|
|
297
|
+
for (const file of [...files.filter(named), ...files.filter((f) => !named(f))]) {
|
|
298
|
+
const byName = named(file);
|
|
299
|
+
const meta = byName && target == null ? null : readSessionMeta(file);
|
|
300
|
+
if (!byName && meta?.id !== threadId) continue;
|
|
301
|
+
if (target != null && (!meta?.cwd || normPath(meta.cwd) !== target)) continue; // fail-closed
|
|
299
302
|
return file;
|
|
300
303
|
}
|
|
301
304
|
return null;
|
|
302
305
|
}
|
|
303
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Why a thread stopped, when it produced no message of its own.
|
|
309
|
+
*
|
|
310
|
+
* A subagent that dies on startup (a model its account cannot use, a transport
|
|
311
|
+
* failure) writes a transcript with nothing in it but the error, so without this
|
|
312
|
+
* its card in the parent is blank and says nothing about what went wrong.
|
|
313
|
+
*/
|
|
314
|
+
function terminalError(text: string): string {
|
|
315
|
+
for (const line of completeLines(text).reverse()) {
|
|
316
|
+
const rec = parseLine(line);
|
|
317
|
+
if (rec?.type !== "event_msg") continue;
|
|
318
|
+
const p = rec.payload ?? {};
|
|
319
|
+
if (p.type !== "task_complete" && p.type !== "error") continue;
|
|
320
|
+
const err = p.error;
|
|
321
|
+
const message = typeof err === "string" ? err
|
|
322
|
+
: (err && typeof err === "object" ? (err as Record<string, unknown>).message : undefined);
|
|
323
|
+
if (typeof message === "string" && message) return unwrapErrorJson(message);
|
|
324
|
+
}
|
|
325
|
+
return "";
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Codex nests the upstream API error verbatim, as JSON, inside its own message. */
|
|
329
|
+
function unwrapErrorJson(message: string): string {
|
|
330
|
+
if (!message.startsWith("{")) return message;
|
|
331
|
+
try {
|
|
332
|
+
const inner = (JSON.parse(message) as { error?: { message?: unknown } }).error?.message;
|
|
333
|
+
return typeof inner === "string" && inner ? inner : message;
|
|
334
|
+
} catch { return message; }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** How deep a chain of spawned agents is followed into the parent's transcript. */
|
|
338
|
+
const SUBAGENT_DEPTH = 2;
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Reader for a spawned thread's transcript, bound to the same sessions dir and
|
|
342
|
+
* the same fail-closed cwd guard as the conversation that spawned it.
|
|
343
|
+
*
|
|
344
|
+
* `seen` covers both the cycle a malformed pair of rollouts could describe and
|
|
345
|
+
* the same agent being named by its start and its completion.
|
|
346
|
+
*/
|
|
347
|
+
function subagentLoader(
|
|
348
|
+
sessionsDir: string,
|
|
349
|
+
requestedCwd: string | undefined,
|
|
350
|
+
seen: Set<string>,
|
|
351
|
+
depth: number,
|
|
352
|
+
): (threadId: string) => SubagentTranscript | null {
|
|
353
|
+
return (threadId) => {
|
|
354
|
+
if (depth <= 0 || seen.has(threadId)) return null;
|
|
355
|
+
seen.add(threadId);
|
|
356
|
+
const file = findRolloutByThreadId(sessionsDir, threadId, requestedCwd);
|
|
357
|
+
if (!file) return null;
|
|
358
|
+
try {
|
|
359
|
+
const text = readFileSync(file, "utf-8");
|
|
360
|
+
const msgs = parseRolloutJsonl(text, {
|
|
361
|
+
loadSubagent: subagentLoader(sessionsDir, requestedCwd, seen, depth - 1),
|
|
362
|
+
});
|
|
363
|
+
const finalText = finalAssistantText(msgs) || terminalError(text);
|
|
364
|
+
const events = transcriptToEvents(msgs);
|
|
365
|
+
// An agent that died before saying anything is answered by its own error
|
|
366
|
+
// record, and the parent may never have written a completion for it — so
|
|
367
|
+
// the reason goes inside the card rather than waiting for a result event
|
|
368
|
+
// that is not coming.
|
|
369
|
+
if (events.length === 0 && finalText) events.push({ type: "text", content: finalText });
|
|
370
|
+
return { events, finalText };
|
|
371
|
+
} catch { return null; }
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
304
375
|
/** The compaction summary text if this rollout was compacted, else null. */
|
|
305
376
|
function compactionSummary(text: string): string | null {
|
|
306
377
|
for (const line of completeLines(text)) {
|
|
@@ -324,7 +395,9 @@ export function getRolloutMessages(sessionsDir: string, threadId: string, reques
|
|
|
324
395
|
if (!file) return [];
|
|
325
396
|
try {
|
|
326
397
|
const text = readFileSync(file, "utf-8");
|
|
327
|
-
const msgs = parseRolloutJsonl(text
|
|
398
|
+
const msgs = parseRolloutJsonl(text, {
|
|
399
|
+
loadSubagent: subagentLoader(sessionsDir, requestedCwd, new Set([threadId]), SUBAGENT_DEPTH),
|
|
400
|
+
});
|
|
328
401
|
const summary = compactionSummary(text);
|
|
329
402
|
if (summary !== null) {
|
|
330
403
|
msgs.unshift({
|