@agentex/agent 0.0.27 → 0.0.29
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 +138 -0
- package/README.md +98 -8
- package/dist/derived.d.ts.map +1 -1
- package/dist/derived.js +99 -2
- package/dist/derived.js.map +1 -1
- package/dist/goals/controller.d.ts.map +1 -1
- package/dist/goals/controller.js +21 -0
- package/dist/goals/controller.js.map +1 -1
- package/dist/goals/normalize.d.ts +1 -0
- package/dist/goals/normalize.d.ts.map +1 -1
- package/dist/goals/normalize.js +5 -0
- package/dist/goals/normalize.js.map +1 -1
- package/dist/history/fs.d.ts +36 -0
- package/dist/history/fs.d.ts.map +1 -0
- package/dist/history/fs.js +266 -0
- package/dist/history/fs.js.map +1 -0
- package/dist/history/index.d.ts +3 -0
- package/dist/history/index.d.ts.map +1 -0
- package/dist/history/index.js +2 -0
- package/dist/history/index.js.map +1 -0
- package/dist/history/types.d.ts +75 -0
- package/dist/history/types.d.ts.map +1 -0
- package/dist/history/types.js +9 -0
- package/dist/history/types.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/claude/attach.d.ts.map +1 -1
- package/dist/providers/claude/attach.js +50 -9
- package/dist/providers/claude/attach.js.map +1 -1
- package/dist/providers/claude/history.d.ts +3 -0
- package/dist/providers/claude/history.d.ts.map +1 -0
- package/dist/providers/claude/history.js +248 -0
- package/dist/providers/claude/history.js.map +1 -0
- package/dist/providers/claude/index.d.ts.map +1 -1
- package/dist/providers/claude/index.js +26 -0
- package/dist/providers/claude/index.js.map +1 -1
- package/dist/providers/claude/session.d.ts +2 -0
- package/dist/providers/claude/session.d.ts.map +1 -1
- package/dist/providers/claude/session.js +17 -10
- package/dist/providers/claude/session.js.map +1 -1
- package/dist/providers/codex/attach.d.ts +2 -2
- package/dist/providers/codex/attach.d.ts.map +1 -1
- package/dist/providers/codex/attach.js +44 -7
- package/dist/providers/codex/attach.js.map +1 -1
- package/dist/providers/codex/history.d.ts +3 -0
- package/dist/providers/codex/history.d.ts.map +1 -0
- package/dist/providers/codex/history.js +350 -0
- package/dist/providers/codex/history.js.map +1 -0
- package/dist/providers/codex/index.d.ts.map +1 -1
- package/dist/providers/codex/index.js +26 -0
- package/dist/providers/codex/index.js.map +1 -1
- package/dist/providers/codex/transcript-normalize.d.ts +3 -2
- package/dist/providers/codex/transcript-normalize.d.ts.map +1 -1
- package/dist/providers/codex/transcript-normalize.js +4 -3
- package/dist/providers/codex/transcript-normalize.js.map +1 -1
- package/dist/providers/codex/transcript.d.ts +3 -1
- package/dist/providers/codex/transcript.d.ts.map +1 -1
- package/dist/providers/codex/transcript.js +3 -1
- package/dist/providers/codex/transcript.js.map +1 -1
- package/dist/providers/cursor/discovery.d.ts +6 -0
- package/dist/providers/cursor/discovery.d.ts.map +1 -0
- package/dist/providers/cursor/discovery.js +118 -0
- package/dist/providers/cursor/discovery.js.map +1 -0
- package/dist/providers/cursor/execute.d.ts.map +1 -1
- package/dist/providers/cursor/execute.js +83 -45
- package/dist/providers/cursor/execute.js.map +1 -1
- package/dist/providers/cursor/index.d.ts.map +1 -1
- package/dist/providers/cursor/index.js +17 -4
- package/dist/providers/cursor/index.js.map +1 -1
- package/dist/providers/cursor/parse.js +1 -1
- package/dist/providers/cursor/parse.js.map +1 -1
- package/dist/providers/cursor/probe.d.ts +3 -0
- package/dist/providers/cursor/probe.d.ts.map +1 -0
- package/dist/providers/cursor/probe.js +89 -0
- package/dist/providers/cursor/probe.js.map +1 -0
- package/dist/providers/cursor/runtime.d.ts +4 -0
- package/dist/providers/cursor/runtime.d.ts.map +1 -0
- package/dist/providers/cursor/runtime.js +12 -0
- package/dist/providers/cursor/runtime.js.map +1 -0
- package/dist/providers/cursor/session.d.ts +3 -0
- package/dist/providers/cursor/session.d.ts.map +1 -0
- package/dist/providers/cursor/session.js +14 -0
- package/dist/providers/cursor/session.js.map +1 -0
- package/dist/providers/opencode/client.d.ts +14 -0
- package/dist/providers/opencode/client.d.ts.map +1 -0
- package/dist/providers/opencode/client.js +40 -0
- package/dist/providers/opencode/client.js.map +1 -0
- package/dist/providers/opencode/discovery.d.ts +4 -0
- package/dist/providers/opencode/discovery.d.ts.map +1 -0
- package/dist/providers/opencode/discovery.js +68 -0
- package/dist/providers/opencode/discovery.js.map +1 -0
- package/dist/providers/opencode/execute.d.ts.map +1 -1
- package/dist/providers/opencode/execute.js +219 -164
- package/dist/providers/opencode/execute.js.map +1 -1
- package/dist/providers/opencode/history.d.ts +24 -0
- package/dist/providers/opencode/history.d.ts.map +1 -0
- package/dist/providers/opencode/history.js +242 -0
- package/dist/providers/opencode/history.js.map +1 -0
- package/dist/providers/opencode/http-session.d.ts.map +1 -1
- package/dist/providers/opencode/http-session.js +333 -49
- package/dist/providers/opencode/http-session.js.map +1 -1
- package/dist/providers/opencode/index.d.ts.map +1 -1
- package/dist/providers/opencode/index.js +26 -3
- package/dist/providers/opencode/index.js.map +1 -1
- package/dist/providers/opencode/manager.d.ts +11 -0
- package/dist/providers/opencode/manager.d.ts.map +1 -0
- package/dist/providers/opencode/manager.js +240 -0
- package/dist/providers/opencode/manager.js.map +1 -0
- package/dist/providers/opencode/modes.d.ts +3 -0
- package/dist/providers/opencode/modes.d.ts.map +1 -0
- package/dist/providers/opencode/modes.js +30 -0
- package/dist/providers/opencode/modes.js.map +1 -0
- package/dist/providers/opencode/parse.d.ts.map +1 -1
- package/dist/providers/opencode/parse.js +2 -22
- package/dist/providers/opencode/parse.js.map +1 -1
- package/dist/providers/opencode/probe.d.ts +3 -0
- package/dist/providers/opencode/probe.d.ts.map +1 -0
- package/dist/providers/opencode/probe.js +107 -0
- package/dist/providers/opencode/probe.js.map +1 -0
- package/dist/providers/opencode/runtime.d.ts +8 -0
- package/dist/providers/opencode/runtime.d.ts.map +1 -0
- package/dist/providers/opencode/runtime.js +12 -0
- package/dist/providers/opencode/runtime.js.map +1 -0
- package/dist/providers/opencode/server.d.ts +18 -3
- package/dist/providers/opencode/server.d.ts.map +1 -1
- package/dist/providers/opencode/server.js +0 -0
- package/dist/providers/opencode/server.js.map +1 -1
- package/dist/providers/opencode/skill-config.d.ts +11 -0
- package/dist/providers/opencode/skill-config.d.ts.map +1 -0
- package/dist/providers/opencode/skill-config.js +57 -0
- package/dist/providers/opencode/skill-config.js.map +1 -0
- package/dist/sessions/exec-backed.d.ts +10 -0
- package/dist/sessions/exec-backed.d.ts.map +1 -0
- package/dist/sessions/exec-backed.js +141 -0
- package/dist/sessions/exec-backed.js.map +1 -0
- package/dist/sessions/history.d.ts +4 -0
- package/dist/sessions/history.d.ts.map +1 -0
- package/dist/sessions/history.js +59 -0
- package/dist/sessions/history.js.map +1 -0
- package/dist/sessions/index.d.ts +3 -0
- package/dist/sessions/index.d.ts.map +1 -1
- package/dist/sessions/index.js +2 -0
- package/dist/sessions/index.js.map +1 -1
- package/dist/types.d.ts +152 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/auth.d.ts.map +1 -1
- package/dist/utils/auth.js +36 -6
- package/dist/utils/auth.js.map +1 -1
- package/dist/utils/binary.d.ts.map +1 -1
- package/dist/utils/binary.js +3 -0
- package/dist/utils/binary.js.map +1 -1
- package/dist/utils/endpoint.js +3 -3
- package/dist/utils/endpoint.js.map +1 -1
- package/package.json +10 -2
- package/src/derived.ts +126 -3
- package/src/goals/controller.ts +17 -0
- package/src/goals/normalize.ts +5 -0
- package/src/history/fs.ts +307 -0
- package/src/history/index.ts +18 -0
- package/src/history/types.ts +104 -0
- package/src/index.ts +31 -0
- package/src/providers/claude/attach.ts +48 -6
- package/src/providers/claude/history.ts +290 -0
- package/src/providers/claude/index.ts +26 -0
- package/src/providers/claude/session.ts +15 -10
- package/src/providers/codex/attach.ts +52 -7
- package/src/providers/codex/history.ts +399 -0
- package/src/providers/codex/index.ts +26 -0
- package/src/providers/codex/transcript-normalize.ts +4 -3
- package/src/providers/codex/transcript.ts +5 -1
- package/src/providers/cursor/discovery.ts +114 -0
- package/src/providers/cursor/execute.ts +67 -30
- package/src/providers/cursor/index.ts +19 -5
- package/src/providers/cursor/parse.ts +1 -1
- package/src/providers/cursor/probe.ts +90 -0
- package/src/providers/cursor/runtime.ts +11 -0
- package/src/providers/cursor/session.ts +15 -0
- package/src/providers/opencode/client.ts +44 -0
- package/src/providers/opencode/discovery.ts +69 -0
- package/src/providers/opencode/execute.ts +64 -12
- package/src/providers/opencode/history.ts +270 -0
- package/src/providers/opencode/http-session.ts +319 -50
- package/src/providers/opencode/index.ts +26 -3
- package/src/providers/opencode/manager.ts +269 -0
- package/src/providers/opencode/modes.ts +31 -0
- package/src/providers/opencode/parse.ts +2 -22
- package/src/providers/opencode/probe.ts +113 -0
- package/src/providers/opencode/runtime.ts +13 -0
- package/src/providers/opencode/server.ts +0 -0
- package/src/providers/opencode/skill-config.ts +64 -0
- package/src/sessions/exec-backed.ts +169 -0
- package/src/sessions/history.ts +76 -0
- package/src/sessions/index.ts +3 -0
- package/src/types.ts +163 -4
- package/src/utils/auth.ts +35 -6
- package/src/utils/binary.ts +3 -0
- package/src/utils/endpoint.ts +3 -3
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import { getRuntimeHomeEnvVar } from "../../utils/runtime-homes.js";
|
|
17
17
|
import { codexSessionCodec } from "./codec.js";
|
|
18
18
|
import {
|
|
19
|
+
type CodexTranscriptLine,
|
|
19
20
|
getCodexTranscriptPath,
|
|
20
21
|
peekCodexTranscript,
|
|
21
22
|
readCodexCwd,
|
|
@@ -37,17 +38,61 @@ const EMPTY: AsyncIterable<CatchUpYield> = {
|
|
|
37
38
|
},
|
|
38
39
|
};
|
|
39
40
|
|
|
41
|
+
function isTurnBoundary(line: CodexTranscriptLine, sessionId: string): boolean {
|
|
42
|
+
const payloadType = typeof line.payload?.["type"] === "string" ? line.payload["type"] : null;
|
|
43
|
+
|
|
44
|
+
// These raw records start a turn but intentionally normalize to no replay
|
|
45
|
+
// event. They must still supersede an earlier task_complete when deciding
|
|
46
|
+
// whether the latest persisted turn was interrupted.
|
|
47
|
+
if (line.type === "event_msg" && (payloadType === "task_started" || payloadType === "user_message")) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
if (line.type === "response_item" && payloadType === "message" && line.payload?.["role"] === "user") {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
if (line.type === "message" && line.raw["role"] === "user") return true;
|
|
54
|
+
|
|
55
|
+
return codexLineToStreamEvents(line, { sessionId }).some((event) => ![
|
|
56
|
+
"system",
|
|
57
|
+
"permission_mode",
|
|
58
|
+
"rate_limit",
|
|
59
|
+
"goal_status",
|
|
60
|
+
"unknown",
|
|
61
|
+
].includes(event.type));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function latestTurnBoundary(
|
|
65
|
+
filePath: string,
|
|
66
|
+
sessionId: string,
|
|
67
|
+
): Promise<CodexTranscriptLine | null> {
|
|
68
|
+
const { lastEvent } = await peekCodexTranscript(filePath, {
|
|
69
|
+
accept: (line) => isTurnBoundary(line, sessionId),
|
|
70
|
+
});
|
|
71
|
+
if (lastEvent) return lastEvent;
|
|
72
|
+
let latest: CodexTranscriptLine | null = null;
|
|
73
|
+
for await (const { event } of readCodexTranscript({ filePath })) {
|
|
74
|
+
if (isTurnBoundary(event, sessionId)) latest = event;
|
|
75
|
+
}
|
|
76
|
+
return latest;
|
|
77
|
+
}
|
|
78
|
+
|
|
40
79
|
/**
|
|
41
80
|
* Read-only reattachment to a durable Codex session. Same skeleton as Claude,
|
|
42
81
|
* with two deltas: classify via the rollout's last line, and replay through
|
|
43
|
-
* `codexLineToStreamEvents
|
|
44
|
-
*
|
|
82
|
+
* `codexLineToStreamEvents`. File-backed transcript lines carry deterministic
|
|
83
|
+
* synthetic event ids so replay can be deduplicated across process restarts.
|
|
45
84
|
*/
|
|
46
85
|
export async function attachCodexSession(
|
|
47
86
|
record: SessionRecord,
|
|
48
87
|
opts?: AttachOptions,
|
|
49
88
|
): Promise<SessionAttachment> {
|
|
50
89
|
assertSessionRecord(record);
|
|
90
|
+
if (record.providerType !== "codex") {
|
|
91
|
+
throw new MalformedSessionRecordError(
|
|
92
|
+
`codex attach requires providerType "codex"; got ${JSON.stringify(record.providerType)}`,
|
|
93
|
+
"providerType",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
51
96
|
|
|
52
97
|
// 1. Normalize params through the codec.
|
|
53
98
|
const params = codexSessionCodec.deserialize(record.params);
|
|
@@ -85,9 +130,9 @@ export async function attachCodexSession(
|
|
|
85
130
|
// 3. Classify how the last persisted turn ended.
|
|
86
131
|
let lastTurn: LastTurnStatus = "unknown";
|
|
87
132
|
if (transcript) {
|
|
88
|
-
const
|
|
133
|
+
const lastEvent = await latestTurnBoundary(transcript.filePath, sessionId);
|
|
89
134
|
if (lastEvent === null) lastTurn = "unknown";
|
|
90
|
-
else if (lastEvent
|
|
135
|
+
else if (codexLineToStreamEvents(lastEvent, { sessionId }).some((event) => event.type === "result"))
|
|
91
136
|
lastTurn = "completed";
|
|
92
137
|
else lastTurn = "interrupted";
|
|
93
138
|
}
|
|
@@ -97,7 +142,7 @@ export async function attachCodexSession(
|
|
|
97
142
|
transcript,
|
|
98
143
|
lastTurn,
|
|
99
144
|
// 4. Replay: map each rollout line through the normalizer; one yield per
|
|
100
|
-
// produced event, all sharing the line's offset
|
|
145
|
+
// produced event, all sharing the line's offset and source event id.
|
|
101
146
|
catchUp(catchOpts?: CatchUpOptions): AsyncIterable<CatchUpYield> {
|
|
102
147
|
if (!transcript) return EMPTY;
|
|
103
148
|
const filePath = transcript.filePath;
|
|
@@ -109,7 +154,7 @@ export async function attachCodexSession(
|
|
|
109
154
|
...(catchOpts?.fromOffset !== undefined ? { fromOffset: catchOpts.fromOffset } : {}),
|
|
110
155
|
})) {
|
|
111
156
|
for (const event of codexLineToStreamEvents(line, { sessionId: sid })) {
|
|
112
|
-
yield { event, offset, eventId:
|
|
157
|
+
yield { event, offset, eventId: event.eventId };
|
|
113
158
|
}
|
|
114
159
|
}
|
|
115
160
|
},
|
|
@@ -117,7 +162,7 @@ export async function attachCodexSession(
|
|
|
117
162
|
},
|
|
118
163
|
// 5. Continue live — exactly `createSession` with the record's params.
|
|
119
164
|
resume(ctx?: SessionContext) {
|
|
120
|
-
return createCodexSession({ ...ctx, sessionParams: params });
|
|
165
|
+
return createCodexSession({ ...ctx, cwd: ctx?.cwd ?? cwd ?? undefined, sessionParams: params });
|
|
121
166
|
},
|
|
122
167
|
};
|
|
123
168
|
}
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
import type { BaseStreamEventFields, StreamEvent } from "../../types.js";
|
|
5
|
+
import type {
|
|
6
|
+
LocalHistoryDiscoverOptions,
|
|
7
|
+
LocalHistoryOps,
|
|
8
|
+
LocalHistoryProbeOptions,
|
|
9
|
+
LocalHistorySession,
|
|
10
|
+
LocalHistoryYield,
|
|
11
|
+
} from "../../history/types.js";
|
|
12
|
+
import {
|
|
13
|
+
ROLLOUT_UUID_RE,
|
|
14
|
+
asRecord,
|
|
15
|
+
asString,
|
|
16
|
+
cleanTitle,
|
|
17
|
+
isDirectory,
|
|
18
|
+
isoDate,
|
|
19
|
+
listJsonlFiles,
|
|
20
|
+
meaningfulHumanText,
|
|
21
|
+
readJsonlRecords,
|
|
22
|
+
recentEligible,
|
|
23
|
+
sourceFingerprint,
|
|
24
|
+
textFromContent,
|
|
25
|
+
} from "../../history/fs.js";
|
|
26
|
+
import { codexLineToStreamEvents } from "./transcript-normalize.js";
|
|
27
|
+
import { parseCodexLine, resolveCodexHome, type CodexTranscriptLine } from "./transcript.js";
|
|
28
|
+
|
|
29
|
+
const MAX_DISCOVERY_SCAN_BYTES = 4 * 1024 * 1024;
|
|
30
|
+
const DEFAULT_PROBE_LIMIT = 100;
|
|
31
|
+
const DISCOVERY_CONCURRENCY = 16;
|
|
32
|
+
|
|
33
|
+
function codexHome(options?: { env?: Record<string, string> }): string {
|
|
34
|
+
return resolveCodexHome(options?.env?.["CODEX_HOME"]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function listCodexSessionFiles(
|
|
38
|
+
home: string,
|
|
39
|
+
options: { includeArchived: boolean; limit?: number },
|
|
40
|
+
): Promise<string[]> {
|
|
41
|
+
const cap = Math.max(1, options.limit ?? Number.MAX_SAFE_INTEGER);
|
|
42
|
+
const active = await listJsonlFiles(path.join(home, "sessions"), {
|
|
43
|
+
maxDepth: 5,
|
|
44
|
+
limit: cap,
|
|
45
|
+
accept: (name) => ROLLOUT_UUID_RE.test(name),
|
|
46
|
+
});
|
|
47
|
+
if (!options.includeArchived || active.length >= cap) return active;
|
|
48
|
+
const archived = await listJsonlFiles(path.join(home, "archived_sessions"), {
|
|
49
|
+
maxDepth: 2,
|
|
50
|
+
limit: cap - active.length,
|
|
51
|
+
accept: (name) => ROLLOUT_UUID_RE.test(name),
|
|
52
|
+
});
|
|
53
|
+
return [...active, ...archived];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function codexUserText(line: CodexTranscriptLine): string | null {
|
|
57
|
+
if (line.type === "event_msg" && line.payload?.["type"] === "user_message") {
|
|
58
|
+
return meaningfulHumanText(asString(line.payload["message"]));
|
|
59
|
+
}
|
|
60
|
+
// Older unwrapped rollouts have no event_msg mirror.
|
|
61
|
+
if (line.type === "message" && line.raw["role"] === "user") {
|
|
62
|
+
return meaningfulHumanText(textFromContent(
|
|
63
|
+
line.raw["content"],
|
|
64
|
+
new Set(["text", "input_text"]),
|
|
65
|
+
));
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isSubagentMeta(payload: Record<string, unknown>): boolean {
|
|
71
|
+
if (asString(payload["parent_thread_id"])) return true;
|
|
72
|
+
if (payload["thread_source"] === "subagent") return true;
|
|
73
|
+
const source = asRecord(payload["source"]);
|
|
74
|
+
return !!source && source["subagent"] !== undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function legacyCwd(record: Record<string, unknown>): string | null {
|
|
78
|
+
if (record["type"] !== "message" || record["role"] !== "user") return null;
|
|
79
|
+
const text = textFromContent(record["content"], new Set(["text", "input_text"]));
|
|
80
|
+
const xml = text?.match(/<cwd>([^<]+)<\/cwd>/)?.[1];
|
|
81
|
+
const plain = text?.match(/Current working directory:\s*([^\n\r]+)/)?.[1];
|
|
82
|
+
const candidate = (xml ?? plain)?.trim();
|
|
83
|
+
return candidate && path.isAbsolute(candidate) ? candidate : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function loadSessionIndexTitles(home: string): Promise<Map<string, string>> {
|
|
87
|
+
const titles = new Map<string, string>();
|
|
88
|
+
try {
|
|
89
|
+
for await (const line of readJsonlRecords(path.join(home, "session_index.jsonl"))) {
|
|
90
|
+
const id = asString(line.raw["id"]);
|
|
91
|
+
const title = cleanTitle(asString(line.raw["thread_name"]));
|
|
92
|
+
if (id && title) titles.set(id, title);
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// The index is optional and absent on older Codex versions.
|
|
96
|
+
}
|
|
97
|
+
return titles;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface SqliteDatabase {
|
|
101
|
+
prepare(sql: string): { all(): unknown[] };
|
|
102
|
+
close(): void;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function stateDatabasePaths(home: string): Promise<string[]> {
|
|
106
|
+
const paths: string[] = [];
|
|
107
|
+
for (const dir of [home, path.join(home, "sqlite")]) {
|
|
108
|
+
let entries;
|
|
109
|
+
try {
|
|
110
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
111
|
+
} catch {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
if (entry.isFile() && /^state_\d+\.sqlite$/.test(entry.name)) paths.push(path.join(dir, entry.name));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return paths.sort().reverse();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function loadSqliteTitles(home: string): Promise<Map<string, string>> {
|
|
122
|
+
const titles = new Map<string, string>();
|
|
123
|
+
try {
|
|
124
|
+
// Keep Node 20 compatibility. node:sqlite is used opportunistically on
|
|
125
|
+
// runtimes that provide it and is always opened read-only.
|
|
126
|
+
const moduleName = "node:sqlite";
|
|
127
|
+
const sqlite = await import(moduleName) as unknown as {
|
|
128
|
+
DatabaseSync: new (filePath: string, options: { readOnly: boolean }) => SqliteDatabase;
|
|
129
|
+
};
|
|
130
|
+
for (const filePath of await stateDatabasePaths(home)) {
|
|
131
|
+
let database: SqliteDatabase | null = null;
|
|
132
|
+
try {
|
|
133
|
+
database = new sqlite.DatabaseSync(filePath, { readOnly: true });
|
|
134
|
+
for (const value of database.prepare("SELECT id, title FROM threads WHERE title <> ''").all()) {
|
|
135
|
+
const row = asRecord(value);
|
|
136
|
+
const id = asString(row?.["id"]);
|
|
137
|
+
const title = cleanTitle(asString(row?.["title"]));
|
|
138
|
+
if (id && title && !titles.has(id)) titles.set(id, title);
|
|
139
|
+
}
|
|
140
|
+
if (titles.size > 0) break;
|
|
141
|
+
} catch {
|
|
142
|
+
// Missing, locked, or schema-incompatible indexes are non-fatal.
|
|
143
|
+
} finally {
|
|
144
|
+
database?.close();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
// node:sqlite is unavailable on Node 20.
|
|
149
|
+
}
|
|
150
|
+
return titles;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function loadCodexTitles(home: string): Promise<Map<string, string>> {
|
|
154
|
+
const index = await loadSessionIndexTitles(home);
|
|
155
|
+
// session_index is the least invasive and most portable source. SQLite is
|
|
156
|
+
// a fallback for installations that do not maintain the JSONL index.
|
|
157
|
+
return index.size > 0 ? index : loadSqliteTitles(home);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function inspectCodexSession(
|
|
161
|
+
transcriptPath: string,
|
|
162
|
+
titles: ReadonlyMap<string, string>,
|
|
163
|
+
options: LocalHistoryDiscoverOptions,
|
|
164
|
+
): Promise<LocalHistorySession | null> {
|
|
165
|
+
let meta: Record<string, unknown> | null = null;
|
|
166
|
+
let legacyMeta: Record<string, unknown> | null = null;
|
|
167
|
+
let metaTimestamp: string | null = null;
|
|
168
|
+
let firstPrompt: string | null = null;
|
|
169
|
+
let fallbackCwd: string | null = null;
|
|
170
|
+
|
|
171
|
+
for await (const record of readJsonlRecords(transcriptPath)) {
|
|
172
|
+
const line = parseCodexLine(record.text);
|
|
173
|
+
if (!line) continue;
|
|
174
|
+
if (line.type === "session_meta" && line.payload) {
|
|
175
|
+
meta ??= line.payload;
|
|
176
|
+
metaTimestamp ??= line.timestamp;
|
|
177
|
+
}
|
|
178
|
+
if (line.type === null && asString(line.raw["id"])) legacyMeta ??= line.raw;
|
|
179
|
+
firstPrompt ??= codexUserText(line);
|
|
180
|
+
fallbackCwd ??= legacyCwd(line.raw);
|
|
181
|
+
if (meta && firstPrompt) break;
|
|
182
|
+
if (record.nextOffset >= MAX_DISCOVERY_SCAN_BYTES) break;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const fileId = path.basename(transcriptPath).match(ROLLOUT_UUID_RE)?.[1] ?? null;
|
|
186
|
+
const externalSessionId = asString(meta?.["id"])
|
|
187
|
+
?? asString(meta?.["session_id"])
|
|
188
|
+
?? asString(legacyMeta?.["id"])
|
|
189
|
+
?? fileId;
|
|
190
|
+
if (!externalSessionId) return null;
|
|
191
|
+
if ((options.mainSessionsOnly ?? true) && meta && isSubagentMeta(meta)) return null;
|
|
192
|
+
|
|
193
|
+
const metaCwd = asString(meta?.["cwd"]);
|
|
194
|
+
const cwd = metaCwd && path.isAbsolute(metaCwd) ? metaCwd : fallbackCwd;
|
|
195
|
+
if (!cwd || (options.cwd && path.normalize(cwd) !== path.normalize(options.cwd))) return null;
|
|
196
|
+
const hasUserMessage = firstPrompt !== null;
|
|
197
|
+
if ((options.requireUserMessage ?? true) && !hasUserMessage) return null;
|
|
198
|
+
|
|
199
|
+
const git = asRecord(meta?.["git"]);
|
|
200
|
+
const source = await sourceFingerprint(transcriptPath);
|
|
201
|
+
const archiveState = transcriptPath.split(path.sep).includes("archived_sessions")
|
|
202
|
+
? "archived" as const
|
|
203
|
+
: "active" as const;
|
|
204
|
+
return {
|
|
205
|
+
version: 1,
|
|
206
|
+
providerType: "codex",
|
|
207
|
+
externalSessionId,
|
|
208
|
+
transcriptPath,
|
|
209
|
+
cwd,
|
|
210
|
+
title: titles.get(externalSessionId) ?? cleanTitle(firstPrompt),
|
|
211
|
+
startedAt: isoDate(meta?.["timestamp"] ?? metaTimestamp ?? legacyMeta?.["timestamp"]),
|
|
212
|
+
updatedAt: new Date(Number(BigInt(source.modifiedAtNs) / 1_000_000n)).toISOString(),
|
|
213
|
+
branch: asString(git?.["branch"]) ?? asString(meta?.["git_branch"]),
|
|
214
|
+
gitOriginUrl: asString(git?.["repository_url"])
|
|
215
|
+
?? asString(git?.["origin_url"])
|
|
216
|
+
?? asString(meta?.["git_origin_url"]),
|
|
217
|
+
archiveState,
|
|
218
|
+
hasUserMessage,
|
|
219
|
+
source,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function baseFields(
|
|
224
|
+
session: LocalHistorySession,
|
|
225
|
+
line: CodexTranscriptLine,
|
|
226
|
+
eventId: string,
|
|
227
|
+
): BaseStreamEventFields & { eventId: string } {
|
|
228
|
+
return {
|
|
229
|
+
timestamp: isoDate(line.timestamp, session.startedAt ?? new Date(0).toISOString())!,
|
|
230
|
+
providerType: "codex",
|
|
231
|
+
sessionId: session.externalSessionId,
|
|
232
|
+
messageId: null,
|
|
233
|
+
eventId,
|
|
234
|
+
turnId: null,
|
|
235
|
+
parentToolCallId: null,
|
|
236
|
+
raw: line.raw,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function legacyCodexEvents(
|
|
241
|
+
session: LocalHistorySession,
|
|
242
|
+
line: CodexTranscriptLine,
|
|
243
|
+
eventId: string,
|
|
244
|
+
): Array<StreamEvent & { eventId: string }> {
|
|
245
|
+
if (line.payload) return [];
|
|
246
|
+
const base = baseFields(session, line, eventId);
|
|
247
|
+
if (line.type === "message" && line.raw["role"] === "assistant") {
|
|
248
|
+
const text = textFromContent(line.raw["content"], new Set(["text", "output_text"]));
|
|
249
|
+
return text ? [{ type: "assistant", text, ...base }] : [];
|
|
250
|
+
}
|
|
251
|
+
if (line.type === "reasoning") {
|
|
252
|
+
const text = textFromContent(line.raw["summary"] ?? line.raw["content"], new Set(["text", "summary_text"]));
|
|
253
|
+
return text ? [{ type: "thinking", text, ...base }] : [];
|
|
254
|
+
}
|
|
255
|
+
if (line.type === "function_call") {
|
|
256
|
+
const toolCallId = asString(line.raw["call_id"]) ?? asString(line.raw["id"]);
|
|
257
|
+
const name = asString(line.raw["name"]) ?? "function_call";
|
|
258
|
+
const rawArguments = line.raw["arguments"];
|
|
259
|
+
let input: Record<string, unknown> | string | null = asString(rawArguments);
|
|
260
|
+
if (typeof rawArguments === "string") {
|
|
261
|
+
try {
|
|
262
|
+
const parsed = JSON.parse(rawArguments);
|
|
263
|
+
if (asRecord(parsed)) input = parsed as Record<string, unknown>;
|
|
264
|
+
} catch {
|
|
265
|
+
// Preserve malformed arguments as their original string.
|
|
266
|
+
}
|
|
267
|
+
} else if (asRecord(rawArguments)) {
|
|
268
|
+
input = rawArguments as Record<string, unknown>;
|
|
269
|
+
}
|
|
270
|
+
return [{ type: "tool_call", toolCallId, name, input, ...base }];
|
|
271
|
+
}
|
|
272
|
+
if (line.type === "function_call_output") {
|
|
273
|
+
const output = line.raw["output"];
|
|
274
|
+
let content = typeof output === "string" ? output : "";
|
|
275
|
+
if (!content && output !== undefined) {
|
|
276
|
+
try {
|
|
277
|
+
content = JSON.stringify(output);
|
|
278
|
+
} catch {
|
|
279
|
+
content = "";
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return [{
|
|
283
|
+
type: "tool_result",
|
|
284
|
+
toolCallId: asString(line.raw["call_id"]),
|
|
285
|
+
toolName: null,
|
|
286
|
+
content,
|
|
287
|
+
isError: line.raw["is_error"] === true,
|
|
288
|
+
exitCode: typeof line.raw["exit_code"] === "number" ? line.raw["exit_code"] : null,
|
|
289
|
+
...base,
|
|
290
|
+
}];
|
|
291
|
+
}
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function dedupeCodexSessionFiles(filePaths: readonly string[]): string[] {
|
|
296
|
+
const seen = new Set<string>();
|
|
297
|
+
const output: string[] = [];
|
|
298
|
+
// listCodexSessionFiles returns active files first, so an active copy wins
|
|
299
|
+
// over a briefly duplicated archive copy before transcript inspection.
|
|
300
|
+
for (const filePath of filePaths) {
|
|
301
|
+
const id = path.basename(filePath).match(ROLLOUT_UUID_RE)?.[1] ?? filePath;
|
|
302
|
+
if (seen.has(id)) continue;
|
|
303
|
+
seen.add(id);
|
|
304
|
+
output.push(filePath);
|
|
305
|
+
}
|
|
306
|
+
return output;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function normalizedCodexEvents(
|
|
310
|
+
session: LocalHistorySession,
|
|
311
|
+
line: CodexTranscriptLine,
|
|
312
|
+
eventId: string,
|
|
313
|
+
): Array<StreamEvent & { eventId: string }> {
|
|
314
|
+
const events = line.payload
|
|
315
|
+
? codexLineToStreamEvents(line, { sessionId: session.externalSessionId })
|
|
316
|
+
: legacyCodexEvents(session, line, eventId);
|
|
317
|
+
return events
|
|
318
|
+
.filter((event) => !((event.type === "assistant" || event.type === "thinking") && !event.text.trim()))
|
|
319
|
+
.map((event) => ({
|
|
320
|
+
...event,
|
|
321
|
+
timestamp: isoDate(line.timestamp, event.timestamp)!,
|
|
322
|
+
providerType: "codex",
|
|
323
|
+
sessionId: session.externalSessionId,
|
|
324
|
+
eventId,
|
|
325
|
+
raw: line.raw,
|
|
326
|
+
} as StreamEvent & { eventId: string }));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export const codexLocalHistory: LocalHistoryOps = {
|
|
330
|
+
async probe(options?: LocalHistoryProbeOptions) {
|
|
331
|
+
const home = codexHome(options);
|
|
332
|
+
const homeAvailable = await isDirectory(home);
|
|
333
|
+
const files = homeAvailable
|
|
334
|
+
? await listCodexSessionFiles(home, {
|
|
335
|
+
includeArchived: true,
|
|
336
|
+
limit: options?.limit ?? DEFAULT_PROBE_LIMIT,
|
|
337
|
+
})
|
|
338
|
+
: [];
|
|
339
|
+
return {
|
|
340
|
+
providerType: "codex",
|
|
341
|
+
homeAvailable,
|
|
342
|
+
historyAvailable: files.length > 0,
|
|
343
|
+
approximateCount: files.length,
|
|
344
|
+
};
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
async *discover(options: LocalHistoryDiscoverOptions = {}) {
|
|
348
|
+
const home = codexHome(options);
|
|
349
|
+
const [files, titles] = await Promise.all([
|
|
350
|
+
listCodexSessionFiles(home, { includeArchived: options.includeArchived ?? true }),
|
|
351
|
+
loadCodexTitles(home),
|
|
352
|
+
]);
|
|
353
|
+
const sessions = await recentEligible(
|
|
354
|
+
dedupeCodexSessionFiles(files),
|
|
355
|
+
{ limit: options.limit, concurrency: DISCOVERY_CONCURRENCY },
|
|
356
|
+
async (filePath) => {
|
|
357
|
+
try {
|
|
358
|
+
return await inspectCodexSession(filePath, titles, options);
|
|
359
|
+
} catch {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
);
|
|
364
|
+
for (const session of sessions) yield session;
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
async *read(session, options = {}): AsyncIterable<LocalHistoryYield> {
|
|
368
|
+
if (session.providerType !== "codex" || !path.isAbsolute(session.transcriptPath)) {
|
|
369
|
+
throw new Error("Invalid Codex local history session");
|
|
370
|
+
}
|
|
371
|
+
for await (const record of readJsonlRecords(
|
|
372
|
+
session.transcriptPath,
|
|
373
|
+
options.fromOffset ?? 0,
|
|
374
|
+
{ rejectChanges: true },
|
|
375
|
+
)) {
|
|
376
|
+
const line = parseCodexLine(record.text);
|
|
377
|
+
if (!line) continue;
|
|
378
|
+
const eventId = `codex:${session.externalSessionId}:${record.lineStartOffset}`;
|
|
379
|
+
line.eventId = eventId;
|
|
380
|
+
const events: LocalHistoryYield["event"][] = [];
|
|
381
|
+
const userText = codexUserText(line);
|
|
382
|
+
if (userText) events.push({ type: "user", text: userText, ...baseFields(session, line, eventId) });
|
|
383
|
+
events.push(...normalizedCodexEvents(session, line, eventId));
|
|
384
|
+
for (let partIndex = 0; partIndex < events.length; partIndex++) {
|
|
385
|
+
yield {
|
|
386
|
+
event: events[partIndex]!,
|
|
387
|
+
lineStartOffset: record.lineStartOffset,
|
|
388
|
+
nextOffset: record.nextOffset,
|
|
389
|
+
partIndex,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
fingerprint(session, options) {
|
|
396
|
+
if (session.providerType !== "codex") throw new Error("Invalid Codex local history session");
|
|
397
|
+
return sourceFingerprint(session.transcriptPath, options?.sha256 ?? false);
|
|
398
|
+
},
|
|
399
|
+
};
|
|
@@ -23,6 +23,8 @@ export const codexProvider: ProviderModule = {
|
|
|
23
23
|
modes: true,
|
|
24
24
|
goals: codexGoalCapability,
|
|
25
25
|
durableSessions: true,
|
|
26
|
+
durableHistory: true,
|
|
27
|
+
localHistory: true,
|
|
26
28
|
},
|
|
27
29
|
// Heavy machinery (execute.ts, session.ts, modes.ts, attach.ts) loads lazily
|
|
28
30
|
// on first use — every ProviderModule method is already async.
|
|
@@ -35,6 +37,30 @@ export const codexProvider: ProviderModule = {
|
|
|
35
37
|
listModes: async (opts) => (await import("./modes.js")).listCodexModes(opts),
|
|
36
38
|
attachSession: async (record, opts) =>
|
|
37
39
|
(await import("./attach.js")).attachCodexSession(record, opts),
|
|
40
|
+
attachHistory: async (record, opts) => {
|
|
41
|
+
const [{ attachCodexSession }, { historyFromSessionAttachment }] = await Promise.all([
|
|
42
|
+
import("./attach.js"),
|
|
43
|
+
import("../../sessions/history.js"),
|
|
44
|
+
]);
|
|
45
|
+
return historyFromSessionAttachment("codex", await attachCodexSession(record, opts));
|
|
46
|
+
},
|
|
47
|
+
localHistory: {
|
|
48
|
+
probe: (options) => import("./history.js").then((module) => module.codexLocalHistory.probe(options)),
|
|
49
|
+
discover: (options) => ({
|
|
50
|
+
async *[Symbol.asyncIterator]() {
|
|
51
|
+
const module = await import("./history.js");
|
|
52
|
+
yield* module.codexLocalHistory.discover(options);
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
read: (session, options) => ({
|
|
56
|
+
async *[Symbol.asyncIterator]() {
|
|
57
|
+
const module = await import("./history.js");
|
|
58
|
+
yield* module.codexLocalHistory.read(session, options);
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
fingerprint: (session, options) => import("./history.js")
|
|
62
|
+
.then((module) => module.codexLocalHistory.fingerprint(session, options)),
|
|
63
|
+
},
|
|
38
64
|
};
|
|
39
65
|
|
|
40
66
|
export {
|
|
@@ -20,8 +20,9 @@ import type { CodexTranscriptLine } from "./transcript.js";
|
|
|
20
20
|
*
|
|
21
21
|
* The on-disk vocabulary is Codex-internal and version-shifting, so every field
|
|
22
22
|
* is read defensively and a weird line NEVER throws — it returns `[]`. Codex
|
|
23
|
-
* emits no per-line wire id
|
|
24
|
-
*
|
|
23
|
+
* emits no native per-line wire id. File readers attach a deterministic
|
|
24
|
+
* synthetic `line.eventId`, which this normalizer preserves. Standalone line
|
|
25
|
+
* parsing still produces a null event id.
|
|
25
26
|
*/
|
|
26
27
|
export function codexLineToStreamEvents(
|
|
27
28
|
line: CodexTranscriptLine,
|
|
@@ -47,7 +48,7 @@ function mapLine(line: CodexTranscriptLine, sessionId: string | null): StreamEve
|
|
|
47
48
|
providerType: "codex",
|
|
48
49
|
sessionId,
|
|
49
50
|
messageId: null,
|
|
50
|
-
eventId:
|
|
51
|
+
eventId: line.eventId,
|
|
51
52
|
turnId: null,
|
|
52
53
|
parentToolCallId: null,
|
|
53
54
|
raw: line.raw,
|
|
@@ -415,7 +415,10 @@ export interface CodexPeekResult {
|
|
|
415
415
|
* Cheap drift-check: reads up to {@link PEEK_TAIL_BYTES} from the tail,
|
|
416
416
|
* walks back to the last parseable line, returns it plus the file size.
|
|
417
417
|
*/
|
|
418
|
-
export async function peekCodexTranscript(
|
|
418
|
+
export async function peekCodexTranscript(
|
|
419
|
+
filePath: string,
|
|
420
|
+
options: { accept?: (event: CodexTranscriptLine) => boolean } = {},
|
|
421
|
+
): Promise<CodexPeekResult> {
|
|
419
422
|
let size: number;
|
|
420
423
|
try {
|
|
421
424
|
const s = await stat(filePath);
|
|
@@ -452,6 +455,7 @@ export async function peekCodexTranscript(filePath: string): Promise<CodexPeekRe
|
|
|
452
455
|
if (!trimmed) continue;
|
|
453
456
|
const parsed = parseCodexLine(trimmed);
|
|
454
457
|
if (!parsed) continue;
|
|
458
|
+
if (options.accept && !options.accept(parsed)) continue;
|
|
455
459
|
return { lastEvent: parsed, size };
|
|
456
460
|
}
|
|
457
461
|
|