fix: event-loop freezes, plugin-install boot crash, tmux attach/scrollbar, Codex transcripts
- GET /api/workflows: one LEAD() window pass in a read worker (read-pool.js, workflow-stats.js); idx_events_session restored, idx_events_tool added - compat-sqlite gains function/pluck/readonly so a plugin install without a better-sqlite3 build boots again; antigravity-usage uses db.js's Database - install-hooks adds no checkout hooks while the agenthub plugin is enabled (events were doubled) - terminal attach runs the live tmux server's own binary; the pane scrollbar gets a visible 2-column style, and claude panes start with CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 so tmux keeps history to scroll - Codex sessions' conversation in GET /api/sessions/:id/transcript - Codex/Antigravity sessions claimed by their workspace without hooks - UI-audit follow-ups, docs and OpenAPI
This commit is contained in:
@@ -677,7 +677,14 @@ router.get("/:id/transcript", async (req, res) => {
|
||||
// the live file is gone once Claude Code prunes it under cleanupPeriodDays
|
||||
// (default 30 days), but the snapshot taken at import time survives.
|
||||
let jsonlPath;
|
||||
if (agentId && agentId !== "main") {
|
||||
// A Codex session's conversation is its root thread's rollout journal
|
||||
// (`codex_rollouts`, recorded by codex-usage.js), never a Claude transcript.
|
||||
const isCodex = session.tool === "codex";
|
||||
if (isCodex) {
|
||||
jsonlPath = db
|
||||
.prepare("SELECT path FROM codex_rollouts WHERE session_id = ? AND thread_id = ?")
|
||||
.get(req.params.id, req.params.id)?.path;
|
||||
} else if (agentId && agentId !== "main") {
|
||||
jsonlPath =
|
||||
getSubagentTranscriptPath(req.params.id, session.cwd, agentId, runId) ||
|
||||
findSubagentTranscriptPath(req.params.id, agentId, runId) ||
|
||||
@@ -707,6 +714,10 @@ router.get("/:id/transcript", async (req, res) => {
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
const renders = isCodex
|
||||
? (e) => e.type === "response_item"
|
||||
: (e) => TRANSCRIPT_RENDER_TYPES.has(e.type);
|
||||
|
||||
// Dedupe state for synthetic rename markers: custom-title lines can repeat
|
||||
// with the same value across a transcript, so only emit when the title
|
||||
// actually changes from the last one we surfaced.
|
||||
@@ -870,14 +881,14 @@ router.get("/:id/transcript", async (req, res) => {
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (!TRANSCRIPT_RENDER_TYPES.has(entry.type)) continue;
|
||||
if (!renders(entry)) continue;
|
||||
|
||||
if (!foundStart) {
|
||||
if (lineNum <= afterLine) continue;
|
||||
foundStart = true;
|
||||
}
|
||||
|
||||
const message = parseMessage(entry, lineNum);
|
||||
const message = isCodex ? codexMessage(entry, lineNum) : parseMessage(entry, lineNum);
|
||||
if (!message) continue;
|
||||
total++;
|
||||
messages.push(message);
|
||||
@@ -904,7 +915,7 @@ router.get("/:id/transcript", async (req, res) => {
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (!TRANSCRIPT_RENDER_TYPES.has(entry.type)) continue;
|
||||
if (!renders(entry)) continue;
|
||||
if (lineNum >= beforeLine) {
|
||||
// Reached the boundary — stop reading
|
||||
rl.close();
|
||||
@@ -912,7 +923,7 @@ router.get("/:id/transcript", async (req, res) => {
|
||||
break;
|
||||
}
|
||||
|
||||
const message = parseMessage(entry, lineNum);
|
||||
const message = isCodex ? codexMessage(entry, lineNum) : parseMessage(entry, lineNum);
|
||||
if (!message) continue;
|
||||
total++;
|
||||
messages.push(message);
|
||||
@@ -934,9 +945,9 @@ router.get("/:id/transcript", async (req, res) => {
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (!TRANSCRIPT_RENDER_TYPES.has(entry.type)) continue;
|
||||
if (!renders(entry)) continue;
|
||||
|
||||
const message = parseMessage(entry, lineNum);
|
||||
const message = isCodex ? codexMessage(entry, lineNum) : parseMessage(entry, lineNum);
|
||||
if (!message) continue;
|
||||
total++;
|
||||
|
||||
@@ -963,9 +974,9 @@ router.get("/:id/transcript", async (req, res) => {
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (!TRANSCRIPT_RENDER_TYPES.has(entry.type)) continue;
|
||||
if (!renders(entry)) continue;
|
||||
|
||||
const message = parseMessage(entry, lineNum);
|
||||
const message = isCodex ? codexMessage(entry, lineNum) : parseMessage(entry, lineNum);
|
||||
if (!message) continue;
|
||||
total++;
|
||||
messages.push(message);
|
||||
@@ -998,6 +1009,80 @@ router.get("/:id/transcript", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Context Codex injects as a user message: AGENTS.md and the `<snake_case>`
|
||||
// blocks (environment_context, user_instructions, …).
|
||||
const CODEX_INJECTED = /^(# AGENTS\.md instructions for |<[a-z]+(?:_[a-z]+)+[\s>])/;
|
||||
|
||||
const codexText = (parts) =>
|
||||
Array.isArray(parts)
|
||||
? parts.map((p) => (p && typeof p.text === "string" ? p.text : "")).join("")
|
||||
: typeof parts === "string"
|
||||
? parts
|
||||
: "";
|
||||
|
||||
/**
|
||||
* One Codex rollout `response_item` → a transcript message in the Claude shape
|
||||
* the client renders, or null. Developer messages, injected context and
|
||||
* encrypted-only reasoning are left out.
|
||||
*/
|
||||
function codexMessage(entry, num) {
|
||||
const p = entry.payload || {};
|
||||
const at = entry.timestamp || null;
|
||||
const msg = (type, sender, content) => ({ type, sender, timestamp: at, content, line: num });
|
||||
const out = (o) => truncate(codexText(o), 10240);
|
||||
switch (p.type) {
|
||||
case "message": {
|
||||
if (p.role !== "user" && p.role !== "assistant") return null;
|
||||
const content = (p.content || [])
|
||||
.map((c) => (c && typeof c.text === "string" ? c.text : ""))
|
||||
.filter((t) => t.trim() && !(p.role === "user" && CODEX_INJECTED.test(t.trimStart())))
|
||||
.map((t) => ({ type: "text", text: truncate(t, 10240) }));
|
||||
return content.length ? msg(p.role, p.role, content) : null;
|
||||
}
|
||||
case "reasoning": {
|
||||
const t = codexText(p.summary);
|
||||
return t.trim()
|
||||
? msg("assistant", "assistant", [{ type: "thinking", text: truncate(t, 10240) }])
|
||||
: null;
|
||||
}
|
||||
case "function_call":
|
||||
case "custom_tool_call": {
|
||||
let input = p.input;
|
||||
if (p.type === "function_call") {
|
||||
try {
|
||||
input = JSON.parse(p.arguments);
|
||||
} catch {
|
||||
input = { arguments: p.arguments };
|
||||
}
|
||||
} else input = { input };
|
||||
return msg("assistant", "assistant", [
|
||||
{
|
||||
type: "tool_use",
|
||||
name: p.name || "unknown",
|
||||
id: p.call_id || null,
|
||||
input: truncateObj(input, 10240),
|
||||
},
|
||||
]);
|
||||
}
|
||||
case "function_call_output":
|
||||
case "custom_tool_call_output": {
|
||||
const o =
|
||||
p.output && !Array.isArray(p.output) && typeof p.output === "object"
|
||||
? p.output.content
|
||||
: p.output;
|
||||
return msg("user", "tool", [
|
||||
{ type: "tool_result", id: p.call_id || null, output: out(o), is_error: false },
|
||||
]);
|
||||
}
|
||||
case "agent_message": {
|
||||
const t = codexText(p.content);
|
||||
return t.trim() ? msg("user", "system", [{ type: "text", text: truncate(t, 10240) }]) : null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function truncate(str, maxLen) {
|
||||
if (!str || str.length <= maxLen) return str;
|
||||
return str.slice(0, maxLen) + "[truncated]";
|
||||
|
||||
Reference in New Issue
Block a user