@hienlh/ppm 0.20.2 → 0.20.4
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/.release-node-shim.sh +10 -0
- package/CHANGELOG.md +13 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/cli-reference.md +4 -4
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/dist/web/assets/{accounts-settings-section-DzDmIsPe.js → accounts-settings-section-BcI3ithh.js} +1 -1
- package/dist/web/assets/{ai-resource-editor-D7C9wmwA.js → ai-resource-editor-C3i7VkUS.js} +1 -1
- package/dist/web/assets/{ai-settings-section-DZuwllLy.js → ai-settings-section-COdsN_5h.js} +1 -1
- package/dist/web/assets/{ai-settings-section-BW5a07-Z.js → ai-settings-section-DIqSLISN.js} +1 -1
- package/dist/web/assets/{appearance-settings-section-RfH2N1rj.js → appearance-settings-section-B9n42QPH.js} +1 -1
- package/dist/web/assets/{chat-tab-Bz0pk0_E.js → chat-tab-D9sk38e8.js} +3 -3
- package/dist/web/assets/{code-editor-C4o1SyuO.js → code-editor-iTD62P14.js} +2 -2
- package/dist/web/assets/{conflict-editor-Dk65mBIe.js → conflict-editor-idh_kBSa.js} +1 -1
- package/dist/web/assets/{csv-preview-DENi3CFz.js → csv-preview-B2PR7z0o.js} +1 -1
- package/dist/web/assets/{database-viewer-BiBC7ulB.js → database-viewer-DSv_pvEq.js} +1 -1
- package/dist/web/assets/{diff-viewer-CSvZJHak.js → diff-viewer-6X5S1lw3.js} +1 -1
- package/dist/web/assets/{explorer-body-QXS0h4AF.js → explorer-body-CrtXpmZM.js} +1 -1
- package/dist/web/assets/{explorer-window-content-CQgqoRsj.js → explorer-window-content-D11FZUgq.js} +1 -1
- package/dist/web/assets/{extension-manager-section-CyC7wX35.js → extension-manager-section-Cz8fvK1u.js} +1 -1
- package/dist/web/assets/{extension-webview-Da8SIlRa.js → extension-webview-DzZeXQsO.js} +1 -1
- package/dist/web/assets/{files-settings-section-D8cebM51.js → files-settings-section-pc4jOun8.js} +1 -1
- package/dist/web/assets/{general-settings-section-1HVTvlIw.js → general-settings-section-DIZIA54q.js} +1 -1
- package/dist/web/assets/{git-log-panel-C7a4rp8f.js → git-log-panel-BkcIfZIE.js} +1 -1
- package/dist/web/assets/{glide-data-grid-BT9p_4AN.js → glide-data-grid-nA5LsoJK.js} +1 -1
- package/dist/web/assets/{group-chat-tab-BUHGnUyW.js → group-chat-tab-DyzfaPol.js} +1 -1
- package/dist/web/assets/{index-kGmcC9cb.js → index-DJM59ogE.js} +4 -4
- package/dist/web/assets/{jira-watcher-section-BxzwW4uQ.js → jira-watcher-section-UEtpcxM9.js} +1 -1
- package/dist/web/assets/{keybindings-store-DjJum4OS.js → keybindings-store-BV0MMt1c.js} +1 -1
- package/dist/web/assets/{keyboard-shortcuts-section-DDKG3BU1.js → keyboard-shortcuts-section-sY56ThaI.js} +1 -1
- package/dist/web/assets/{markdown-renderer-dHeak2mE.js → markdown-renderer-Bkrk39pO.js} +1 -1
- package/dist/web/assets/{markdown-renderer-iBw1fCI_.js → markdown-renderer-Bthd7hxl.js} +1 -1
- package/dist/web/assets/{mobile-explorer-sheet-CzL8SLtz.js → mobile-explorer-sheet--NKsnNJn.js} +1 -1
- package/dist/web/assets/{notification-store-DVi6Ds71.js → notification-store-Bv4fnyvR.js} +1 -1
- package/dist/web/assets/{notifications-settings-section-shQ-ugKh.js → notifications-settings-section-BGSLkMuE.js} +1 -1
- package/dist/web/assets/{pdf-preview-DB26KSzm.js → pdf-preview-DE0wVfyM.js} +1 -1
- package/dist/web/assets/{postgres-viewer-CJe2meyF.js → postgres-viewer-C0S8dCbA.js} +1 -1
- package/dist/web/assets/{ppmbot-settings-section-Ba4c9AI9.js → ppmbot-settings-section-B2AZ8whe.js} +1 -1
- package/dist/web/assets/{process-table-PXwLsMuG.js → process-table-BmT34-_s.js} +1 -1
- package/dist/web/assets/{proxy-settings-section-BHBhK6HC.js → proxy-settings-section-BYwWFWvn.js} +1 -1
- package/dist/web/assets/{query-audit-section-BD5ZVqDz.js → query-audit-section-Dj1-npew.js} +1 -1
- package/dist/web/assets/{remote-desktop-mobile-sheet-Di4QG8VX.js → remote-desktop-mobile-sheet-bFIYIQIj.js} +2 -2
- package/dist/web/assets/{remote-desktop-mobile-view-DzoZvAqc.js → remote-desktop-mobile-view-BbrHPmPU.js} +1 -1
- package/dist/web/assets/{remote-desktop-readiness-gate-CHO-KJ9Z.js → remote-desktop-readiness-gate-Dme8r7Jp.js} +1 -1
- package/dist/web/assets/{remote-desktop-window-content-X324gYbW.js → remote-desktop-window-content-BdbszoSx.js} +1 -1
- package/dist/web/assets/{schedules-settings-section-a4-h6THc.js → schedules-settings-section-rczhjkeM.js} +1 -1
- package/dist/web/assets/{settings-body-Fq7BVDw2.js → settings-body-CwaQg_uc.js} +2 -2
- package/dist/web/assets/{settings-tab-CVLQy-ie.js → settings-tab-C8l9-_9E.js} +1 -1
- package/dist/web/assets/{settings-window-content-Bi7M0iaZ.js → settings-window-content-EA1489gi.js} +1 -1
- package/dist/web/assets/{sql-query-editor-D1r265fl.js → sql-query-editor-BQsKZVtz.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-DHkIotRl.js → sqlite-viewer-Di8s3Z1m.js} +1 -1
- package/dist/web/assets/{system-monitor-body-y6VUkYTL.js → system-monitor-body-Dlknm4Uo.js} +2 -2
- package/dist/web/assets/{system-monitor-tab-DRAOejhk.js → system-monitor-tab-LZRO6_DP.js} +1 -1
- package/dist/web/assets/{system-monitor-window-content-B5i57xHD.js → system-monitor-window-content-BFDfhqDd.js} +1 -1
- package/dist/web/assets/{tab-host-window-content-SkyPFFlc.js → tab-host-window-content-Bh7064Yw.js} +1 -1
- package/dist/web/assets/{team-member-sheet-BB63u-Lf.js → team-member-sheet-CU68IqU0.js} +2 -2
- package/dist/web/assets/{team-member-window-content-D6tVTXuV.js → team-member-window-content-BYtro89j.js} +1 -1
- package/dist/web/assets/{terminal-tab-BYaHAkI5.js → terminal-tab-qfSPxRtZ.js} +1 -1
- package/dist/web/assets/{tool-cards-DRJu20sL.js → tool-cards-CY9J4ux4.js} +2 -2
- package/dist/web/assets/{use-accounts-data-Ba1_uvEE.js → use-accounts-data-pAQw6nCa.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-Uakgj3hJ.js → use-monaco-theme-DT5Q8kuf.js} +1 -1
- package/dist/web/assets/{use-remote-desktop-display-choice-Cd3Xalca.js → use-remote-desktop-display-choice-CYQsrOMC.js} +1 -1
- package/dist/web/assets/{use-websocket-BC_EGf5q.js → use-websocket-mkNA1M8U.js} +1 -1
- package/dist/web/assets/{video-preview-BUb43zWb.js → video-preview-B2dk48oQ.js} +1 -1
- package/dist/web/index.html +1 -1
- package/dist/web/sw.js +1 -1
- package/docs/lessons-learned.md +14 -0
- package/package.json +1 -1
- package/src/providers/codex-app-server/codex-event-mapper.ts +4 -0
- package/src/providers/codex-app-server/codex-history.ts +4 -2
- package/src/providers/codex-app-server/codex-protocol.ts +4 -0
- package/src/providers/codex-app-server/codex-provider.ts +92 -28
- package/src/server/routes/chat.ts +22 -3
- package/src/server/ws/chat.ts +5 -0
- package/src/services/edge-forwarder.ts +191 -191
- package/src/services/windows-process-tree.ts +351 -351
- package/src/web/hooks/use-chat.ts +22 -11
- package/packages/vscode-compat/node_modules/.bin/tsc.bunx +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsc.exe +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsserver.bunx +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsserver.exe +0 -0
package/docs/lessons-learned.md
CHANGED
|
@@ -4,6 +4,20 @@ Knowledge and gotchas discovered during PPM development.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## A missing Codex rollout must not restart an existing conversation
|
|
8
|
+
|
|
9
|
+
Each Codex account has its own history directory. Search all managed accounts,
|
|
10
|
+
preferring the session's current account, and copy the located rollout into the
|
|
11
|
+
serving account's directory before resuming. Only a session freshly created by
|
|
12
|
+
PPM may start a new thread without a rollout. An existing thread with missing
|
|
13
|
+
history must report an error, including during account rotation.
|
|
14
|
+
|
|
15
|
+
Starting a replacement thread and recording `migrated_to` hides the original
|
|
16
|
+
history behind the replacement while a tab can retain the original title.
|
|
17
|
+
For affected historical data, verify both transcripts and the migration log,
|
|
18
|
+
take a verified SQLite snapshot, then remove only the incorrect redirect.
|
|
19
|
+
Keep both transcripts; verify the live messages API returns each independently.
|
|
20
|
+
|
|
7
21
|
## Chat usage must stay scoped to its provider and session
|
|
8
22
|
|
|
9
23
|
Usage API responses are snapshots: replace them instead of merging into previous
|
package/package.json
CHANGED
|
@@ -168,6 +168,10 @@ export function mapCodexEvent(notif: Notif, sessionId: string): ChatEvent[] {
|
|
|
168
168
|
return typeof p.delta === "string" ? [{ type: "text", content: p.delta }] : [];
|
|
169
169
|
|
|
170
170
|
case "item/reasoning/textDelta":
|
|
171
|
+
// `summary` is what turn/start requests for PPM's Thinking switch. Recent
|
|
172
|
+
// app-server builds stream it under this method, while older builds used
|
|
173
|
+
// textDelta directly. Both are safe, user-visible reasoning summaries.
|
|
174
|
+
case "item/reasoning/summaryTextDelta":
|
|
171
175
|
return typeof p.delta === "string" ? [{ type: "thinking", content: p.delta }] : [];
|
|
172
176
|
|
|
173
177
|
case "item/started": {
|
|
@@ -411,10 +411,12 @@ export function getRolloutMessages(sessionsDir: string, threadId: string, reques
|
|
|
411
411
|
} catch { return []; }
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
-
/** True when a path points at a codex rollout
|
|
414
|
+
/** True when a path points at a codex rollout in the ambient or a PPM account home. */
|
|
415
415
|
export function isCodexRolloutPath(p: string): boolean {
|
|
416
416
|
const n = normPath(p);
|
|
417
|
-
|
|
417
|
+
const ambient = normPath(join(homedir(), ".codex", "sessions"));
|
|
418
|
+
const managed = normPath(join(homedir(), ".ppm", "codex-accounts"));
|
|
419
|
+
return n.endsWith(".jsonl") && (n.includes(ambient) || (n.includes(managed) && n.includes("/sessions/")));
|
|
418
420
|
}
|
|
419
421
|
|
|
420
422
|
/**
|
|
@@ -56,6 +56,10 @@ export interface TurnStartParams {
|
|
|
56
56
|
approvalPolicy?: AskForApproval;
|
|
57
57
|
sandboxPolicy?: unknown;
|
|
58
58
|
model?: string;
|
|
59
|
+
/** Codex's per-turn reasoning depth (low through ultra, model dependent). */
|
|
60
|
+
effort?: string;
|
|
61
|
+
/** Controls whether Codex streams a safe reasoning summary to the client. */
|
|
62
|
+
summary?: "none" | "auto" | "concise" | "detailed";
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
// ── Decisions (approval responses) ──
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
UsageInfo,
|
|
11
11
|
} from "../provider.interface.ts";
|
|
12
12
|
import { configService } from "../../services/config.service.ts";
|
|
13
|
-
import { setSessionMetadata, getSessionProjectPath, setSessionProvider, setSessionCodexAccount, getSessionCodexAccount, getSessionTitles } from "../../services/db.service.ts";
|
|
13
|
+
import { setSessionMetadata, getSessionProjectPath, setSessionProvider, setSessionCodexAccount, getSessionCodexAccount, getSessionTitles, insertTurnUsage } from "../../services/db.service.ts";
|
|
14
14
|
import {
|
|
15
15
|
resolveCodexAccountForSession,
|
|
16
16
|
getCodexAccount,
|
|
@@ -29,6 +29,7 @@ import { join } from "node:path";
|
|
|
29
29
|
import { CodexJsonRpcClient, CONTROL_REQUEST_TIMEOUT_MS } from "./codex-jsonrpc-client.ts";
|
|
30
30
|
import { permissionModeToCodex, type CodexPermission } from "./codex-permission-map.ts";
|
|
31
31
|
import { mapCodexEvent, parseTokenUsage } from "./codex-event-mapper.ts";
|
|
32
|
+
import { subagentCardId } from "./codex-subagent-thread.ts";
|
|
32
33
|
import { decisionFor, isApprovalMethod, type ApprovalMethod } from "./codex-approval-decision.ts";
|
|
33
34
|
import { parseModelList } from "./codex-model-parser.ts";
|
|
34
35
|
import { getOrFetchUsage, registerUsageSource } from "../../services/provider-usage/usage-registry.ts";
|
|
@@ -147,6 +148,8 @@ interface LiveSession {
|
|
|
147
148
|
compactRequested?: boolean;
|
|
148
149
|
/** Token counts from the most recent usage notification, attached to `done`. */
|
|
149
150
|
lastUsage?: import("../../shared/turn-usage.ts").TurnUsage;
|
|
151
|
+
/** Spawned thread ids. Notifications from these threads belong inside their Agent card. */
|
|
152
|
+
subagentThreadIds: Set<string>;
|
|
150
153
|
/** A turn is running on the thread. Codex accepts exactly one at a time: a
|
|
151
154
|
* second `turn/start` resolves with the ALREADY-RUNNING turn (same id,
|
|
152
155
|
* status `inProgress`) and silently discards the new input — no error, so
|
|
@@ -219,6 +222,21 @@ function createEventChannel(): EventChannel {
|
|
|
219
222
|
};
|
|
220
223
|
}
|
|
221
224
|
|
|
225
|
+
/** The app-server puts the owner thread on every item notification. An
|
|
226
|
+
* `agentThreadId` inside a SubAgentActivity is the newly spawned child, so it
|
|
227
|
+
* must never be used here: that would nest the launch card inside itself. */
|
|
228
|
+
export function threadIdFromNotification(params: unknown): string | null {
|
|
229
|
+
if (!params || typeof params !== "object") return null;
|
|
230
|
+
const p = params as Record<string, unknown>;
|
|
231
|
+
const direct = p.threadId ?? p.thread_id;
|
|
232
|
+
if (typeof direct === "string" && direct) return direct;
|
|
233
|
+
const thread = p.thread;
|
|
234
|
+
if (thread && typeof thread === "object" && typeof (thread as Record<string, unknown>).id === "string") {
|
|
235
|
+
return (thread as Record<string, unknown>).id as string;
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
222
240
|
/**
|
|
223
241
|
* Drop a model id that isn't a codex model. PPM's per-session model can fall back
|
|
224
242
|
* to the global Claude default (e.g. `claude-opus-4-8`), which codex rejects
|
|
@@ -235,6 +253,10 @@ function extractThreadId(result: unknown): string | null {
|
|
|
235
253
|
return r?.thread?.id ?? r?.threadId ?? r?.id ?? null;
|
|
236
254
|
}
|
|
237
255
|
|
|
256
|
+
function missingRolloutError(sessionId: string): Error {
|
|
257
|
+
return new Error(`Cannot resume Codex session ${sessionId}: its transcript was not found in the available account homes for this project. Restore the original account/transcript and retry, or explicitly open a new chat.`);
|
|
258
|
+
}
|
|
259
|
+
|
|
238
260
|
/** Human label for an approval prompt (dormant in MVP under default bypass). */
|
|
239
261
|
function approvalToolLabel(method: string, params: unknown): string {
|
|
240
262
|
const p = (params && typeof params === "object" ? params : {}) as Record<string, unknown>;
|
|
@@ -273,6 +295,8 @@ export class CodexAppServerProvider implements AIProvider {
|
|
|
273
295
|
readonly name = "Codex";
|
|
274
296
|
|
|
275
297
|
private sessions = new Map<string, Session>();
|
|
298
|
+
/** Only IDs minted here may start a thread. Missing history is never proof of a new chat. */
|
|
299
|
+
private unstartedSessions = new Set<string>();
|
|
276
300
|
private live = new Map<string, LiveSession>();
|
|
277
301
|
private modelsCache: { models: ModelOption[]; expiry: number } | null = null;
|
|
278
302
|
/** Keyed by `cwd\0codexHome` — skills differ per workspace AND per account. */
|
|
@@ -294,6 +318,7 @@ export class CodexAppServerProvider implements AIProvider {
|
|
|
294
318
|
createdAt: new Date().toISOString(),
|
|
295
319
|
};
|
|
296
320
|
this.sessions.set(id, session);
|
|
321
|
+
this.unstartedSessions.add(id);
|
|
297
322
|
if (config.projectPath) setSessionMetadata(id, config.projectName, config.projectPath);
|
|
298
323
|
return session;
|
|
299
324
|
}
|
|
@@ -354,6 +379,7 @@ export class CodexAppServerProvider implements AIProvider {
|
|
|
354
379
|
async deleteSession(sessionId: string): Promise<void> {
|
|
355
380
|
this.abortQuery(sessionId, "delete");
|
|
356
381
|
this.sessions.delete(sessionId);
|
|
382
|
+
this.unstartedSessions.delete(sessionId);
|
|
357
383
|
}
|
|
358
384
|
|
|
359
385
|
// ── Streaming (multi-turn) ──
|
|
@@ -363,6 +389,7 @@ export class CodexAppServerProvider implements AIProvider {
|
|
|
363
389
|
try {
|
|
364
390
|
live = await this.connect(sessionId, opts);
|
|
365
391
|
} catch (err) {
|
|
392
|
+
this.abortQuery(sessionId, "connect_failed");
|
|
366
393
|
yield { type: "error", message: redactTruncate((err as Error)?.message ?? String(err), 512) };
|
|
367
394
|
yield { type: "done", sessionId, resultSubtype: "error_during_execution" };
|
|
368
395
|
return;
|
|
@@ -416,6 +443,7 @@ export class CodexAppServerProvider implements AIProvider {
|
|
|
416
443
|
live.lastTurnInput = { message, opts };
|
|
417
444
|
live.currentAssistant = "";
|
|
418
445
|
live.currentEvents = [];
|
|
446
|
+
live.lastUsage = undefined;
|
|
419
447
|
// Codex takes an image as a path, never as a payload, so the uploaded copy is what it
|
|
420
448
|
// gets. Images lead: the text usually refers to them ("what is this?").
|
|
421
449
|
const input: UserInput[] = [
|
|
@@ -423,10 +451,17 @@ export class CodexAppServerProvider implements AIProvider {
|
|
|
423
451
|
{ type: "text" as const, text: message, text_elements: [] },
|
|
424
452
|
];
|
|
425
453
|
const turnModel = codexModel(opts?.model);
|
|
454
|
+
const turnEffort = opts?.effort ?? this.config?.effort;
|
|
455
|
+
const configuredThinking = this.config?.thinking_budget_tokens;
|
|
456
|
+
// Codex does not accept a token budget. Its equivalent is an effort level,
|
|
457
|
+
// while `summary` controls the safe thinking text the UI can show.
|
|
458
|
+
const thinkingEnabled = (opts?.thinkingBudget ?? configuredThinking ?? -1) !== 0;
|
|
426
459
|
live.client.request("turn/start", {
|
|
427
460
|
threadId: live.threadId,
|
|
428
461
|
input,
|
|
429
462
|
...(turnModel ? { model: turnModel } : {}),
|
|
463
|
+
...(turnEffort ? { effort: turnEffort } : {}),
|
|
464
|
+
summary: thinkingEnabled ? "detailed" : "none",
|
|
430
465
|
}).then((res) => {
|
|
431
466
|
// Backup for the `turn/started` notification, which is what normally names
|
|
432
467
|
// the turn — an interrupt arriving in between has nothing to address.
|
|
@@ -586,6 +621,11 @@ export class CodexAppServerProvider implements AIProvider {
|
|
|
586
621
|
* had just been repaired.
|
|
587
622
|
*/
|
|
588
623
|
private async respawnOn(live: LiveSession, threadId: string, account: CodexAccount): Promise<void> {
|
|
624
|
+
// Validate before replacing the client or its account binding. A missing file
|
|
625
|
+
// can mean inaccessible history, including on the first turn; it cannot justify
|
|
626
|
+
// migrating an existing provider thread onto an empty conversation.
|
|
627
|
+
const found = locateRollout(threadId, live.cwd);
|
|
628
|
+
if (!found) throw missingRolloutError(threadId);
|
|
589
629
|
const old = live.client;
|
|
590
630
|
old.onNotification(() => {});
|
|
591
631
|
old.onServerRequest(() => {});
|
|
@@ -596,8 +636,8 @@ export class CodexAppServerProvider implements AIProvider {
|
|
|
596
636
|
if (process.platform === "win32" && pid) killProcessTree(pid);
|
|
597
637
|
else if (proc) setTimeout(() => { try { proc.kill("SIGKILL"); } catch { /* dead */ } }, 2000).unref?.();
|
|
598
638
|
|
|
599
|
-
//
|
|
600
|
-
//
|
|
639
|
+
// Locate history while the outgoing account is still bound, so its current
|
|
640
|
+
// transcript wins. Then bind the account that will receive the localized copy.
|
|
601
641
|
setSessionCodexAccount(threadId, account.id);
|
|
602
642
|
|
|
603
643
|
const client = new CodexJsonRpcClient();
|
|
@@ -616,26 +656,7 @@ export class CodexAppServerProvider implements AIProvider {
|
|
|
616
656
|
approvalPolicy: live.permission.approvalPolicy,
|
|
617
657
|
...(live.model ? { model: live.model } : {}),
|
|
618
658
|
};
|
|
619
|
-
|
|
620
|
-
if (found) {
|
|
621
|
-
await this.resumeThread(client, threadId, found, account.home, resumeBase);
|
|
622
|
-
return;
|
|
623
|
-
}
|
|
624
|
-
// No rollout yet means the limit hit the very first turn, before codex had written one.
|
|
625
|
-
// There is nothing to resume, so the thread starts fresh — and it comes back under a
|
|
626
|
-
// new id, which every map keyed by the old one has to learn about or the next turn is
|
|
627
|
-
// sent to a thread this app-server has never heard of.
|
|
628
|
-
const started = await client.request("thread/start", resumeBase);
|
|
629
|
-
const newId = extractThreadId(started);
|
|
630
|
-
if (!newId || newId === threadId) return;
|
|
631
|
-
live.threadId = newId;
|
|
632
|
-
this.live.set(newId, live);
|
|
633
|
-
const meta = this.sessions.get(threadId);
|
|
634
|
-
if (meta) { this.sessions.delete(threadId); meta.id = newId; this.sessions.set(newId, meta); }
|
|
635
|
-
setSessionMetadata(newId, meta?.projectName, live.cwd);
|
|
636
|
-
setSessionProvider(newId, this.id);
|
|
637
|
-
setSessionCodexAccount(newId, account.id);
|
|
638
|
-
live.channel.push({ type: "session_migrated", oldSessionId: threadId, newSessionId: newId });
|
|
659
|
+
await this.resumeThread(client, threadId, found, account.home, resumeBase);
|
|
639
660
|
}
|
|
640
661
|
|
|
641
662
|
/**
|
|
@@ -672,13 +693,18 @@ export class CodexAppServerProvider implements AIProvider {
|
|
|
672
693
|
const permission = permissionModeToCodex(opts?.permissionMode ?? this.config?.permission_mode);
|
|
673
694
|
const model = codexModel(opts?.model ?? this.config?.model);
|
|
674
695
|
|
|
696
|
+
// Only resume a rollout attributable to this project. An unknown/resumed ID
|
|
697
|
+
// without one must not silently become a fresh thread with a different identity.
|
|
698
|
+
const found = locateRollout(sessionId, cwd);
|
|
699
|
+
if (!found && !this.unstartedSessions.has(sessionId)) throw missingRolloutError(sessionId);
|
|
700
|
+
|
|
675
701
|
const client = new CodexJsonRpcClient();
|
|
676
702
|
const channel = createEventChannel();
|
|
677
703
|
const live: LiveSession = {
|
|
678
704
|
client, threadId: null, cwd, channel, permission, model,
|
|
679
705
|
pendingApprovals: new Map(), answeredCodexIds: new Set(),
|
|
680
706
|
history: [], transcript: [], currentAssistant: "", currentEvents: [],
|
|
681
|
-
pendingTurns: [],
|
|
707
|
+
pendingTurns: [], subagentThreadIds: new Set(),
|
|
682
708
|
};
|
|
683
709
|
this.live.set(sessionId, live);
|
|
684
710
|
|
|
@@ -697,13 +723,13 @@ export class CodexAppServerProvider implements AIProvider {
|
|
|
697
723
|
const resumeBase = { cwd, sandbox: permission.sandbox, approvalPolicy: permission.approvalPolicy, ...(model ? { model } : {}) };
|
|
698
724
|
// Only treat as a resume when a rollout for this id is attributable to THIS
|
|
699
725
|
// project (fail-closed cwd guard) — never resume another project's thread.
|
|
700
|
-
const found = locateRollout(sessionId, cwd);
|
|
701
726
|
const result = found
|
|
702
727
|
? await this.resumeThread(client, sessionId, found, account?.home, resumeBase)
|
|
703
728
|
: await client.request("thread/start", resumeBase);
|
|
704
729
|
|
|
705
730
|
const threadId = extractThreadId(result) ?? (found ? sessionId : null);
|
|
706
731
|
if (!threadId) throw new Error("codex thread/start returned no thread id");
|
|
732
|
+
this.unstartedSessions.delete(sessionId);
|
|
707
733
|
live.threadId = threadId;
|
|
708
734
|
|
|
709
735
|
if (threadId !== sessionId) {
|
|
@@ -750,14 +776,26 @@ export class CodexAppServerProvider implements AIProvider {
|
|
|
750
776
|
const usage = parseTokenUsage(notif.params, live.model);
|
|
751
777
|
if (usage) live.lastUsage = usage;
|
|
752
778
|
}
|
|
779
|
+
const notificationThreadId = threadIdFromNotification(notif.params);
|
|
780
|
+
const parentToolUseId = notificationThreadId && live.subagentThreadIds.has(notificationThreadId)
|
|
781
|
+
? subagentCardId(notificationThreadId)
|
|
782
|
+
: undefined;
|
|
753
783
|
const events = mapCodexEvent(notif, live.threadId ?? "");
|
|
754
784
|
for (const ev of events) {
|
|
755
785
|
// The counts arrive on their own notification just before the turn ends,
|
|
756
786
|
// so `done` is where they become visible to a consumer.
|
|
757
|
-
if (ev.type === "done" && live.lastUsage)
|
|
758
|
-
|
|
787
|
+
if (ev.type === "done" && live.lastUsage) {
|
|
788
|
+
ev.usage = live.lastUsage;
|
|
789
|
+
this.recordTurnUsage(live, live.lastUsage);
|
|
790
|
+
}
|
|
791
|
+
const nested = parentToolUseId ? { ...ev, parentToolUseId } as ChatEvent : ev;
|
|
792
|
+
live.channel.push(nested);
|
|
759
793
|
// Accumulate tool calls into the turn so getMessages (live) keeps them.
|
|
760
|
-
if (
|
|
794
|
+
if (nested.type === "tool_use" || nested.type === "tool_result") live.currentEvents.push(nested);
|
|
795
|
+
if (nested.type === "tool_use" && nested.tool === "Agent" && typeof nested.toolUseId === "string"
|
|
796
|
+
&& nested.toolUseId.startsWith("subagent-")) {
|
|
797
|
+
live.subagentThreadIds.add(nested.toolUseId.slice("subagent-".length));
|
|
798
|
+
}
|
|
761
799
|
}
|
|
762
800
|
if (notif.method === "turn/completed") {
|
|
763
801
|
if (live.currentAssistant || live.currentEvents.length) {
|
|
@@ -798,6 +836,32 @@ export class CodexAppServerProvider implements AIProvider {
|
|
|
798
836
|
}
|
|
799
837
|
}
|
|
800
838
|
|
|
839
|
+
/** Persist Codex's per-turn token and prompt-cache split for the debug panel. */
|
|
840
|
+
private recordTurnUsage(live: LiveSession, usage: import("../../shared/turn-usage.ts").TurnUsage): void {
|
|
841
|
+
const accountId = live.threadId ? getSessionCodexAccount(live.threadId) ?? undefined : undefined;
|
|
842
|
+
const account = accountId ? getCodexAccount(accountId) : null;
|
|
843
|
+
try {
|
|
844
|
+
insertTurnUsage({
|
|
845
|
+
sessionId: live.threadId ?? "",
|
|
846
|
+
model: usage.model,
|
|
847
|
+
inputTokens: usage.inputTokens,
|
|
848
|
+
outputTokens: usage.outputTokens,
|
|
849
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
850
|
+
cacheWriteTokens: usage.cacheWriteTokens,
|
|
851
|
+
contextWindow: usage.contextWindow,
|
|
852
|
+
costUsd: usage.costUsd,
|
|
853
|
+
// A cache miss alone does not prove a process restart: a first turn and
|
|
854
|
+
// an evicted prompt cache look the same on the wire. Keep this false
|
|
855
|
+
// until the provider has an explicit lifecycle reason to record.
|
|
856
|
+
coldStart: false,
|
|
857
|
+
...(accountId ? { accountId } : {}),
|
|
858
|
+
...(account?.label ? { accountLabel: account.label } : {}),
|
|
859
|
+
});
|
|
860
|
+
} catch (err) {
|
|
861
|
+
console.warn(`[usage] failed to persist codex usage: ${(err as Error).message}`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
801
865
|
private handleServerRequest(live: LiveSession, req: ServerRequest): void {
|
|
802
866
|
const method = req.method;
|
|
803
867
|
if (isApprovalMethod(method)) {
|
|
@@ -19,6 +19,8 @@ import { parseJsonlTranscript, validateJsonlPath } from "../../services/jsonl-tr
|
|
|
19
19
|
import { aggregateTasks } from "../../services/task-status-aggregator.ts";
|
|
20
20
|
import { MANY_IMAGE_DIMENSION_LIMIT, type StripMode } from "../../services/transcript-images.ts";
|
|
21
21
|
import { auditTranscriptImagesFile, stripTranscriptImagesFile } from "../../services/transcript-images-file.ts";
|
|
22
|
+
import { listCodexAccounts } from "../../services/codex-account.service.ts";
|
|
23
|
+
import { findRolloutByThreadId } from "../../providers/codex-app-server/codex-history.ts";
|
|
22
24
|
import { getSessionProjectPath, setSessionMetadata, setSessionTitle, getSessionTitle, getPinnedSessionIds, pinSession, unpinSession, deleteSessionMapping, deleteSessionMetadata, deleteSessionTitle, getAllUnread, clearSessionUnread, setSessionUnread } from "../../services/db.service.ts";
|
|
23
25
|
import { setSessionTag, bulkSetSessionTag, getTagById, getSessionTags, getProjectDefaultTagId } from "../../services/tag.service.ts";
|
|
24
26
|
import { recordBranch, resolveVersionGroup, resolveVersionMap, collapseTreesToHeads, hasChildren, deleteBranchesFor, getRootId } from "../../services/session-branch.service.ts";
|
|
@@ -469,8 +471,11 @@ chatRoutes.patch("/sessions/:id", async (c) => {
|
|
|
469
471
|
const projectPath = c.get("projectPath");
|
|
470
472
|
// Persist to PPM DB (authoritative source for user-set titles)
|
|
471
473
|
setSessionTitle(id, title);
|
|
472
|
-
//
|
|
473
|
-
|
|
474
|
+
// Codex owns its rollout title and has no Claude SDK session to rename.
|
|
475
|
+
// PPM's title table is the authoritative title for both providers.
|
|
476
|
+
if (getSessionProvider(id) !== "codex") {
|
|
477
|
+
await sdkRenameSession(id, title, { dir: projectPath });
|
|
478
|
+
}
|
|
474
479
|
// Also update in-memory session
|
|
475
480
|
const session = chatService.getSession(id);
|
|
476
481
|
if (session) session.title = title;
|
|
@@ -763,9 +768,22 @@ chatRoutes.get("/sessions/:id/logs", (c) => {
|
|
|
763
768
|
*/
|
|
764
769
|
function resolveSessionJsonlPath(sessionId: string): { jsonlPath: string; jsonlDir: string; projectPath: string; exists: boolean } {
|
|
765
770
|
const homedir = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
771
|
+
const providerId = getSessionProvider(sessionId);
|
|
772
|
+
const storedProjectPath = getSessionProjectPath(sessionId) ?? "";
|
|
773
|
+
if (providerId === "codex") {
|
|
774
|
+
const dirs = [
|
|
775
|
+
...listCodexAccounts().map((account) => join(account.home, "sessions")),
|
|
776
|
+
...(homedir ? [resolve(homedir, ".codex", "sessions")] : []),
|
|
777
|
+
];
|
|
778
|
+
for (const dir of dirs) {
|
|
779
|
+
const found = findRolloutByThreadId(dir, sessionId, storedProjectPath || undefined);
|
|
780
|
+
if (found) return { jsonlPath: found, jsonlDir: dir, projectPath: storedProjectPath, exists: true };
|
|
781
|
+
}
|
|
782
|
+
return { jsonlPath: "", jsonlDir: "", projectPath: storedProjectPath, exists: false };
|
|
783
|
+
}
|
|
766
784
|
const provider = providerRegistry.get("claude") as any;
|
|
767
785
|
const projectPath = provider?.activeSessions?.get(sessionId)?.projectPath
|
|
768
|
-
??
|
|
786
|
+
?? storedProjectPath
|
|
769
787
|
?? "";
|
|
770
788
|
const projectsRoot = homedir ? resolve(homedir, ".claude", "projects") : "";
|
|
771
789
|
// SDK encodes cwd by replacing path separators + drive colon with "-".
|
|
@@ -815,6 +833,7 @@ chatRoutes.get("/sessions/:id/debug", (c) => {
|
|
|
815
833
|
return c.json(ok({
|
|
816
834
|
ppmSessionId: sessionId,
|
|
817
835
|
sdkSessionId: sessionId,
|
|
836
|
+
providerId: getSessionProvider(sessionId) ?? "claude",
|
|
818
837
|
sessionId,
|
|
819
838
|
jsonlPath: exists ? jsonlPath : null,
|
|
820
839
|
jsonlDir,
|
package/src/server/ws/chat.ts
CHANGED
|
@@ -1286,10 +1286,15 @@ export const chatWebSocket = {
|
|
|
1286
1286
|
} else {
|
|
1287
1287
|
// Follow-up: push into existing generator via provider
|
|
1288
1288
|
if (provider && "pushMessage" in provider && parsed.type === "message") {
|
|
1289
|
+
const effort = getSessionEffort(sessionId) ?? undefined;
|
|
1290
|
+
const thinkingBudget = getSessionThinking(sessionId);
|
|
1289
1291
|
(provider as any).pushMessage(sessionId, parsed.content, {
|
|
1290
1292
|
priority: parsed.priority ?? 'next',
|
|
1291
1293
|
images: parsed.images,
|
|
1292
1294
|
imagePaths: parsed.imagePaths,
|
|
1295
|
+
...(entry.model ? { model: entry.model } : {}),
|
|
1296
|
+
...(effort ? { effort } : {}),
|
|
1297
|
+
...(thinkingBudget != null ? { thinkingBudget } : {}),
|
|
1293
1298
|
});
|
|
1294
1299
|
}
|
|
1295
1300
|
// Clear turn events for new turn display + transition phase
|