remcodex 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +331 -0
  3. package/dist/server/src/app.js +186 -0
  4. package/dist/server/src/cli.js +270 -0
  5. package/dist/server/src/controllers/codex-options.controller.js +199 -0
  6. package/dist/server/src/controllers/message.controller.js +21 -0
  7. package/dist/server/src/controllers/project.controller.js +44 -0
  8. package/dist/server/src/controllers/session.controller.js +175 -0
  9. package/dist/server/src/db/client.js +10 -0
  10. package/dist/server/src/db/migrations.js +32 -0
  11. package/dist/server/src/gateways/ws.gateway.js +60 -0
  12. package/dist/server/src/services/codex-app-server-runner.js +363 -0
  13. package/dist/server/src/services/codex-exec-runner.js +147 -0
  14. package/dist/server/src/services/codex-rollout-sync.js +977 -0
  15. package/dist/server/src/services/codex-runner.js +11 -0
  16. package/dist/server/src/services/codex-stream-events.js +478 -0
  17. package/dist/server/src/services/event-store.js +328 -0
  18. package/dist/server/src/services/project-manager.js +130 -0
  19. package/dist/server/src/services/pty-runner.js +72 -0
  20. package/dist/server/src/services/session-manager.js +1586 -0
  21. package/dist/server/src/services/session-timeline-service.js +181 -0
  22. package/dist/server/src/types/codex-launch.js +2 -0
  23. package/dist/server/src/types/models.js +37 -0
  24. package/dist/server/src/utils/ansi.js +143 -0
  25. package/dist/server/src/utils/codex-launch.js +102 -0
  26. package/dist/server/src/utils/codex-quota.js +179 -0
  27. package/dist/server/src/utils/codex-status.js +163 -0
  28. package/dist/server/src/utils/codex-ui-options.js +114 -0
  29. package/dist/server/src/utils/command.js +46 -0
  30. package/dist/server/src/utils/errors.js +16 -0
  31. package/dist/server/src/utils/ids.js +7 -0
  32. package/dist/server/src/utils/node-pty.js +29 -0
  33. package/package.json +36 -0
  34. package/scripts/fix-node-pty-helper.js +36 -0
  35. package/web/api.js +175 -0
  36. package/web/app.js +8082 -0
  37. package/web/components/composer.js +627 -0
  38. package/web/components/session-workbench.js +173 -0
  39. package/web/i18n/index.js +171 -0
  40. package/web/i18n/locales/de.js +50 -0
  41. package/web/i18n/locales/en.js +320 -0
  42. package/web/i18n/locales/es.js +50 -0
  43. package/web/i18n/locales/fr.js +50 -0
  44. package/web/i18n/locales/ja.js +50 -0
  45. package/web/i18n/locales/ko.js +50 -0
  46. package/web/i18n/locales/pt-BR.js +50 -0
  47. package/web/i18n/locales/ru.js +50 -0
  48. package/web/i18n/locales/zh-CN.js +320 -0
  49. package/web/i18n/locales/zh-Hant.js +53 -0
  50. package/web/index.html +23 -0
  51. package/web/message-rich-text.js +218 -0
  52. package/web/session-command-activity.js +980 -0
  53. package/web/session-event-adapter.js +826 -0
  54. package/web/session-timeline-reducer.js +728 -0
  55. package/web/session-timeline-renderer.js +656 -0
  56. package/web/session-ws.js +31 -0
  57. package/web/styles.css +5665 -0
  58. package/web/vendor/markdown-it.js +6969 -0
@@ -0,0 +1,1586 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SessionManager = void 0;
7
+ const node_fs_1 = require("node:fs");
8
+ const node_os_1 = __importDefault(require("node:os"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const errors_1 = require("../utils/errors");
11
+ const ids_1 = require("../utils/ids");
12
+ const codex_runner_1 = require("./codex-runner");
13
+ const codex_stream_events_1 = require("./codex-stream-events");
14
+ function nowIso() {
15
+ return new Date().toISOString();
16
+ }
17
+ function shouldAutotitleSession(title) {
18
+ const normalized = String(title || "").trim();
19
+ return (!normalized ||
20
+ normalized === "未命名会话" ||
21
+ normalized === "新会话" ||
22
+ normalized === "Untitled session" ||
23
+ normalized === "New session");
24
+ }
25
+ function deriveSessionTitleFromMessage(message) {
26
+ const normalized = String(message || "").replace(/\s+/g, " ").trim();
27
+ if (!normalized) {
28
+ return "Untitled session";
29
+ }
30
+ if (normalized.length <= 28) {
31
+ return normalized;
32
+ }
33
+ return `${normalized.slice(0, 27)}…`;
34
+ }
35
+ function resolveDemoWorkspacePath() {
36
+ return node_path_1.default.join(node_os_1.default.homedir(), "remcodex-demo", "demo-workspace");
37
+ }
38
+ function resolveDemoOutsideTargetPath() {
39
+ return node_path_1.default.join(node_os_1.default.homedir(), "remcodex-demo", "outside-target", "demo-from-remcodex.txt");
40
+ }
41
+ function containsExplicitFilesystemTarget(message) {
42
+ const text = String(message || "");
43
+ return /(?:\/Users\/|\/tmp\b|~\/)/.test(text);
44
+ }
45
+ function shouldForceDemoApprovalPath(projectPath, message) {
46
+ const normalizedProjectPath = node_path_1.default.resolve(String(projectPath || "").trim());
47
+ if (normalizedProjectPath !== resolveDemoWorkspacePath()) {
48
+ return false;
49
+ }
50
+ const normalizedMessage = String(message || "").toLowerCase();
51
+ const mentionsOutsideWorkspace = normalizedMessage.includes("outside the current workspace") ||
52
+ normalizedMessage.includes("outside the workspace") ||
53
+ normalizedMessage.includes("outside current workspace") ||
54
+ normalizedMessage.includes("工作区外") ||
55
+ normalizedMessage.includes("当前工作区外");
56
+ if (!mentionsOutsideWorkspace) {
57
+ return false;
58
+ }
59
+ return !containsExplicitFilesystemTarget(message);
60
+ }
61
+ function normalizeDemoPrompt(projectPath, message) {
62
+ if (!shouldForceDemoApprovalPath(projectPath, message)) {
63
+ return message;
64
+ }
65
+ const targetPath = resolveDemoOutsideTargetPath();
66
+ return `${message.trim()}\n\nUse this exact target path: ${targetPath}`;
67
+ }
68
+ class SessionManager {
69
+ options;
70
+ runners = new Map();
71
+ pendingApprovals = new Map();
72
+ sessionWritableRoots = new Map();
73
+ constructor(options) {
74
+ this.options = options;
75
+ }
76
+ listSessions() {
77
+ return this.options.db
78
+ .prepare(`
79
+ SELECT
80
+ s.id,
81
+ s.title,
82
+ s.project_id,
83
+ s.status,
84
+ s.pid,
85
+ s.codex_thread_id,
86
+ s.source_kind,
87
+ s.source_rollout_path,
88
+ s.source_thread_id,
89
+ s.source_sync_cursor,
90
+ s.source_last_synced_at,
91
+ s.source_rollout_has_open_turn,
92
+ s.created_at,
93
+ s.updated_at,
94
+ (
95
+ SELECT e.created_at
96
+ FROM session_events e
97
+ WHERE e.session_id = s.id
98
+ ORDER BY e.seq DESC
99
+ LIMIT 1
100
+ ) AS last_event_at,
101
+ (
102
+ SELECT json_extract(e.payload_json, '$.text')
103
+ FROM session_events e
104
+ WHERE e.session_id = s.id
105
+ AND e.event_type = 'message.assistant.end'
106
+ ORDER BY e.seq DESC
107
+ LIMIT 1
108
+ ) AS last_assistant_content,
109
+ (
110
+ SELECT json_extract(e.payload_json, '$.command')
111
+ FROM session_events e
112
+ WHERE e.session_id = s.id
113
+ AND e.event_type = 'command.start'
114
+ ORDER BY e.seq DESC
115
+ LIMIT 1
116
+ ) AS last_command,
117
+ (
118
+ SELECT COUNT(*)
119
+ FROM session_events e
120
+ WHERE e.session_id = s.id
121
+ ) AS event_count
122
+ FROM sessions s
123
+ ORDER BY COALESCE(last_event_at, s.updated_at) DESC
124
+ `)
125
+ .all();
126
+ }
127
+ isLiveBusy(sessionId) {
128
+ const session = this.getSession(sessionId);
129
+ if (!session) {
130
+ return false;
131
+ }
132
+ const runtime = this.runners.get(sessionId);
133
+ if (!runtime || !runtime.runner.isAlive()) {
134
+ return false;
135
+ }
136
+ return ["starting", "running", "stopping"].includes(session.status);
137
+ }
138
+ getSession(sessionId) {
139
+ return (this.options.db
140
+ .prepare(`
141
+ SELECT
142
+ id,
143
+ title,
144
+ project_id,
145
+ status,
146
+ pid,
147
+ codex_thread_id,
148
+ source_kind,
149
+ source_rollout_path,
150
+ source_thread_id,
151
+ source_sync_cursor,
152
+ source_last_synced_at,
153
+ source_rollout_has_open_turn,
154
+ created_at,
155
+ updated_at
156
+ FROM sessions
157
+ WHERE id = ?
158
+ `)
159
+ .get(sessionId) ?? null);
160
+ }
161
+ getPendingApproval(sessionId) {
162
+ const pending = this.pendingApprovals.get(sessionId);
163
+ const live = pending
164
+ ? [...pending.values()].sort((a, b) => {
165
+ const aParsed = Number.parseInt(a.requestId, 10);
166
+ const bParsed = Number.parseInt(b.requestId, 10);
167
+ const aId = a.runnerRequestId ?? (Number.isFinite(aParsed) ? aParsed : 0);
168
+ const bId = b.runnerRequestId ?? (Number.isFinite(bParsed) ? bParsed : 0);
169
+ return aId - bId;
170
+ })[0]
171
+ : null;
172
+ if (live) {
173
+ return {
174
+ ...this.serializePendingApproval(live),
175
+ resumable: true,
176
+ source: "live",
177
+ };
178
+ }
179
+ const restored = this.options.eventStore.latestPendingApproval(sessionId);
180
+ if (!restored) {
181
+ return null;
182
+ }
183
+ return {
184
+ ...restored,
185
+ resumable: false,
186
+ source: "event-log",
187
+ };
188
+ }
189
+ createSession(input) {
190
+ const project = this.options.projectManager.getProject(input.projectId);
191
+ if (!project) {
192
+ throw new errors_1.AppError(404, "Project not found.");
193
+ }
194
+ const timestamp = nowIso();
195
+ const session = {
196
+ id: (0, ids_1.createId)("sess"),
197
+ title: input.title?.trim() || "Untitled session",
198
+ project_id: project.id,
199
+ status: "idle",
200
+ pid: null,
201
+ codex_thread_id: null,
202
+ source_kind: "native",
203
+ source_rollout_path: null,
204
+ source_thread_id: null,
205
+ source_sync_cursor: null,
206
+ source_last_synced_at: null,
207
+ source_rollout_has_open_turn: 0,
208
+ created_at: timestamp,
209
+ updated_at: timestamp,
210
+ };
211
+ this.options.db
212
+ .prepare(`
213
+ INSERT INTO sessions (
214
+ id,
215
+ title,
216
+ project_id,
217
+ status,
218
+ pid,
219
+ codex_thread_id,
220
+ source_kind,
221
+ source_rollout_path,
222
+ source_thread_id,
223
+ source_sync_cursor,
224
+ source_last_synced_at,
225
+ source_rollout_has_open_turn,
226
+ created_at,
227
+ updated_at
228
+ )
229
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
230
+ `)
231
+ .run(session.id, session.title, session.project_id, session.status, session.pid, session.codex_thread_id, session.source_kind, session.source_rollout_path, session.source_thread_id, session.source_sync_cursor, session.source_last_synced_at, session.source_rollout_has_open_turn, session.created_at, session.updated_at);
232
+ return session;
233
+ }
234
+ sendMessage(sessionId, content, codexLaunch) {
235
+ const message = content.trim();
236
+ if (!message) {
237
+ throw new errors_1.AppError(400, "Message content is required.");
238
+ }
239
+ const session = this.getSessionOrThrow(sessionId);
240
+ const project = this.options.projectManager.getProject(session.project_id);
241
+ if (!project) {
242
+ throw new errors_1.AppError(404, "Project not found for session.");
243
+ }
244
+ const runtimePrompt = normalizeDemoPrompt(project.path, message);
245
+ const currentRunner = this.runners.get(sessionId);
246
+ const busyStatuses = ["starting", "running", "stopping"];
247
+ if (currentRunner?.runner.isAlive() && busyStatuses.includes(session.status)) {
248
+ throw new errors_1.AppError(409, "Session already has an active task.");
249
+ }
250
+ if (shouldAutotitleSession(session.title)) {
251
+ this.options.db
252
+ .prepare(`
253
+ UPDATE sessions
254
+ SET title = ?, updated_at = ?
255
+ WHERE id = ?
256
+ `)
257
+ .run(deriveSessionTitleFromMessage(message), nowIso(), sessionId);
258
+ }
259
+ const turnId = (0, ids_1.createId)("turn");
260
+ const event = this.appendEvent(sessionId, {
261
+ type: "message.user",
262
+ turnId,
263
+ messageId: null,
264
+ callId: null,
265
+ requestId: null,
266
+ phase: null,
267
+ stream: null,
268
+ payload: {
269
+ text: message,
270
+ },
271
+ });
272
+ this.startRunner(sessionId, project.path, runtimePrompt, turnId, this.resolveResumeThreadId(session), codexLaunch);
273
+ return {
274
+ accepted: true,
275
+ eventId: event.id,
276
+ turnId,
277
+ seq: event.seq,
278
+ };
279
+ }
280
+ stopSession(sessionId) {
281
+ const runtime = this.runners.get(sessionId);
282
+ if (!runtime || !runtime.runner.isAlive()) {
283
+ this.setStatus(sessionId, "idle");
284
+ return { accepted: true };
285
+ }
286
+ runtime.stopRequested = true;
287
+ this.setStatus(sessionId, "stopping");
288
+ runtime.runner.stop();
289
+ return { accepted: true };
290
+ }
291
+ resolveApproval(sessionId, requestId, decision) {
292
+ const runtime = this.runners.get(sessionId);
293
+ if (!runtime?.runner.isAlive()) {
294
+ throw new errors_1.AppError(409, "Session has no active runtime.");
295
+ }
296
+ const pending = this.pendingApprovals.get(sessionId)?.get(requestId) ??
297
+ this.restorePendingApprovalFromEvents(sessionId, requestId);
298
+ if (!pending) {
299
+ throw new errors_1.AppError(404, "Approval request not found.");
300
+ }
301
+ const payload = this.buildApprovalResponsePayload(pending, decision);
302
+ const runnerRequestId = pending.runnerRequestId;
303
+ if (!Number.isFinite(runnerRequestId) || runnerRequestId === null) {
304
+ throw new errors_1.AppError(409, "Approval request can no longer be resumed.");
305
+ }
306
+ if (!runtime.runner.respond(runnerRequestId, payload)) {
307
+ throw new errors_1.AppError(409, "Current runner cannot resolve approval requests.");
308
+ }
309
+ this.consumePendingApproval(sessionId, requestId, decision);
310
+ return { accepted: true };
311
+ }
312
+ hasSession(sessionId) {
313
+ return this.getSession(sessionId) !== null;
314
+ }
315
+ startRunner(sessionId, cwd, prompt, turnId, threadId, codexLaunch) {
316
+ const existing = this.runners.get(sessionId);
317
+ if (existing?.runner.isAlive()) {
318
+ return existing;
319
+ }
320
+ const runner = (0, codex_runner_1.createCodexRunner)(this.options.codexMode, this.options.codexCommand, cwd);
321
+ const effectiveLaunch = this.withSessionWritableRoots(sessionId, codexLaunch);
322
+ const runtime = {
323
+ runner,
324
+ stopRequested: false,
325
+ turnId,
326
+ appTurnId: null,
327
+ turnStarted: false,
328
+ turnFinalized: false,
329
+ assistantByPhase: new Map(),
330
+ messagesById: new Map(),
331
+ reasoning: null,
332
+ commandsByCallId: new Map(),
333
+ patchesByCallId: new Map(),
334
+ activeCommandCallId: null,
335
+ activePatchCallId: null,
336
+ };
337
+ this.runners.set(sessionId, runtime);
338
+ this.setStatus(sessionId, "starting");
339
+ runner.onJsonEvent((raw) => {
340
+ if ((0, codex_stream_events_1.isCodexAppServerMessage)(raw)) {
341
+ this.handleAppServerMessage(sessionId, runtime, raw);
342
+ return;
343
+ }
344
+ if ((0, codex_stream_events_1.isCodexLegacyEvent)(raw)) {
345
+ this.handleLegacyEvent(sessionId, runtime, raw);
346
+ return;
347
+ }
348
+ if ((0, codex_stream_events_1.isCodexEnvelopeEvent)(raw)) {
349
+ this.handleEnvelopeEvent(sessionId, runtime, raw);
350
+ }
351
+ });
352
+ runner.onText((_stream, _text) => {
353
+ // raw stdout/stderr no longer belongs to the primary event protocol
354
+ });
355
+ runner.onExit((exitCode) => {
356
+ this.handleRunnerExit(sessionId, runtime, exitCode);
357
+ });
358
+ try {
359
+ const pid = runner.start(prompt, threadId, effectiveLaunch);
360
+ this.setPid(sessionId, pid);
361
+ }
362
+ catch (error) {
363
+ this.runners.delete(sessionId);
364
+ this.setStatus(sessionId, "failed");
365
+ this.appendError(sessionId, runtime.turnId, `Failed to start Codex CLI: ${this.messageOf(error)}`);
366
+ throw new errors_1.AppError(500, `Failed to start Codex CLI: ${this.messageOf(error)}`);
367
+ }
368
+ return runtime;
369
+ }
370
+ resolveResumeThreadId(session) {
371
+ if (session.source_kind === "imported_rollout") {
372
+ const importedThreadId = String(session.source_thread_id || "").trim();
373
+ if (importedThreadId) {
374
+ return importedThreadId;
375
+ }
376
+ }
377
+ const nativeThreadId = String(session.codex_thread_id || "").trim();
378
+ return nativeThreadId || null;
379
+ }
380
+ handleAppServerMessage(sessionId, runtime, raw) {
381
+ const signals = (0, codex_stream_events_1.translateCodexAppServerMessage)(raw);
382
+ for (const signal of signals) {
383
+ this.applySemanticSignal(sessionId, runtime, signal);
384
+ }
385
+ }
386
+ handleLegacyEvent(sessionId, runtime, event) {
387
+ if (event.type === "thread.started" && event.thread_id) {
388
+ this.setCodexThreadId(sessionId, event.thread_id);
389
+ return;
390
+ }
391
+ if (event.type === "item.started" && event.item?.type === "command_execution") {
392
+ this.ensureTurnStarted(sessionId, runtime);
393
+ const callId = this.resolveCommandCallId(runtime, event.item.id || null);
394
+ this.ensureCommandStart(sessionId, runtime, callId, {
395
+ command: event.item.command || "",
396
+ cwd: null,
397
+ });
398
+ return;
399
+ }
400
+ if (event.type === "item.completed" && event.item?.type === "agent_message") {
401
+ this.ensureTurnStarted(sessionId, runtime);
402
+ this.finishAssistantMessage(sessionId, runtime, "final_answer", event.item.id || null, event.item.text || "");
403
+ return;
404
+ }
405
+ if (event.type === "item.completed" && event.item?.type === "command_execution") {
406
+ this.ensureTurnStarted(sessionId, runtime);
407
+ const callId = this.resolveCommandCallId(runtime, event.item.id || null);
408
+ this.ensureCommandStart(sessionId, runtime, callId, {
409
+ command: event.item.command || "",
410
+ cwd: null,
411
+ });
412
+ if (event.item.aggregated_output) {
413
+ this.appendCommandOutputDelta(sessionId, runtime, callId, "stdout", event.item.aggregated_output);
414
+ }
415
+ this.finishCommand(sessionId, runtime, callId, {
416
+ command: event.item.command || null,
417
+ cwd: null,
418
+ status: event.item.status || (event.item.exit_code === 0 ? "completed" : "failed"),
419
+ exitCode: event.item.exit_code ?? null,
420
+ });
421
+ return;
422
+ }
423
+ if (event.type === "turn.completed") {
424
+ if (event.usage) {
425
+ this.appendTokenCount(sessionId, runtime.turnId, {
426
+ rateLimits: {},
427
+ totalTokenUsage: {
428
+ input_tokens: event.usage.input_tokens ?? 0,
429
+ cached_input_tokens: event.usage.cached_input_tokens ?? 0,
430
+ output_tokens: event.usage.output_tokens ?? 0,
431
+ },
432
+ receivedAt: nowIso(),
433
+ source: "live",
434
+ });
435
+ }
436
+ this.completeTurn(sessionId, runtime, "turn.completed", {
437
+ completedAt: nowIso(),
438
+ });
439
+ }
440
+ }
441
+ handleEnvelopeEvent(sessionId, runtime, raw) {
442
+ if (!raw || typeof raw !== "object") {
443
+ return;
444
+ }
445
+ const envelope = raw;
446
+ const payload = envelope.payload && typeof envelope.payload === "object"
447
+ ? envelope.payload
448
+ : null;
449
+ if (envelope.type === "event_msg" && payload?.type === "token_count") {
450
+ const normalized = this.normalizeTokenCountPayload(payload);
451
+ if (normalized) {
452
+ this.appendTokenCount(sessionId, runtime.turnId, normalized);
453
+ }
454
+ return;
455
+ }
456
+ if (envelope.type !== "response_item" || !payload) {
457
+ return;
458
+ }
459
+ const itemType = typeof payload.type === "string" ? payload.type : "";
460
+ if (itemType === "message" && payload.role === "assistant") {
461
+ const phase = this.normalizeAssistantPhase(payload.phase);
462
+ const text = this.extractResponseItemMessageText(payload);
463
+ if (text) {
464
+ this.ensureTurnStarted(sessionId, runtime);
465
+ this.finishAssistantMessage(sessionId, runtime, phase, this.readString(payload.id), text);
466
+ }
467
+ return;
468
+ }
469
+ if (itemType === "reasoning") {
470
+ const summary = this.readString(payload.summary) || "";
471
+ this.ensureTurnStarted(sessionId, runtime);
472
+ const messageId = this.resolveReasoningMessageId(runtime, this.readString(payload.id));
473
+ this.ensureReasoningStart(sessionId, runtime, messageId, summary || null);
474
+ if (summary) {
475
+ this.appendReasoningDelta(sessionId, runtime, messageId, summary, summary);
476
+ }
477
+ this.finishReasoning(sessionId, runtime, messageId, summary || null);
478
+ return;
479
+ }
480
+ if (itemType === "function_call" && payload.name === "exec_command") {
481
+ this.ensureTurnStarted(sessionId, runtime);
482
+ const callId = this.resolveCommandCallId(runtime, this.readString(payload.call_id));
483
+ const args = this.parseJsonObject(this.readString(payload.arguments));
484
+ this.ensureCommandStart(sessionId, runtime, callId, {
485
+ command: this.readString(args.cmd) || "",
486
+ cwd: this.readString(args.cwd),
487
+ justification: this.readString(args.justification),
488
+ sandboxMode: this.readString(args.sandbox) || this.readString(args.sandbox_permissions),
489
+ approvalRequired: this.readString(args.sandbox_permissions) === "require_escalated",
490
+ grantRoot: this.readString(args.grantRoot),
491
+ });
492
+ return;
493
+ }
494
+ if (itemType === "function_call_output") {
495
+ this.ensureTurnStarted(sessionId, runtime);
496
+ const callId = this.resolveCommandCallId(runtime, this.readString(payload.call_id));
497
+ const output = this.readString(payload.output) || "";
498
+ if (output) {
499
+ this.appendCommandOutputDelta(sessionId, runtime, callId, "stdout", output);
500
+ }
501
+ return;
502
+ }
503
+ if (itemType === "custom_tool_call") {
504
+ this.ensureTurnStarted(sessionId, runtime);
505
+ const callId = this.resolvePatchCallId(runtime, this.readString(payload.call_id));
506
+ this.ensurePatchStart(sessionId, runtime, callId, {
507
+ summary: this.readString(payload.name),
508
+ target: null,
509
+ });
510
+ const input = this.readString(payload.input);
511
+ if (input) {
512
+ this.appendPatchOutputDelta(sessionId, runtime, callId, input);
513
+ }
514
+ return;
515
+ }
516
+ if (itemType === "custom_tool_call_output") {
517
+ this.ensureTurnStarted(sessionId, runtime);
518
+ const callId = this.resolvePatchCallId(runtime, this.readString(payload.call_id));
519
+ const output = this.readString(payload.output) || "";
520
+ if (output) {
521
+ this.appendPatchOutputDelta(sessionId, runtime, callId, output);
522
+ }
523
+ this.finishPatch(sessionId, runtime, callId, {
524
+ status: "completed",
525
+ });
526
+ }
527
+ }
528
+ applySemanticSignal(sessionId, runtime, signal) {
529
+ switch (signal.kind) {
530
+ case "thread_started":
531
+ this.setCodexThreadId(sessionId, signal.threadId);
532
+ return;
533
+ case "approval_request":
534
+ this.registerPendingApproval(sessionId, {
535
+ requestId: String(signal.requestId),
536
+ runnerRequestId: signal.requestId,
537
+ sessionId,
538
+ turnId: signal.turnId ?? runtime.turnId,
539
+ callId: signal.callId,
540
+ method: signal.method,
541
+ params: signal.params,
542
+ createdAt: nowIso(),
543
+ });
544
+ return;
545
+ case "event":
546
+ this.applySemanticEvent(sessionId, runtime, signal.event);
547
+ return;
548
+ }
549
+ }
550
+ applySemanticEvent(sessionId, runtime, event) {
551
+ switch (event.type) {
552
+ case "turn.started":
553
+ this.ensureTurnStarted(sessionId, runtime, event.turnId || runtime.turnId, event.payload);
554
+ return;
555
+ case "turn.completed":
556
+ this.completeTurn(sessionId, runtime, "turn.completed", event.payload);
557
+ return;
558
+ case "turn.aborted":
559
+ this.completeTurn(sessionId, runtime, "turn.aborted", event.payload);
560
+ return;
561
+ case "message.assistant.start": {
562
+ this.ensureTurnStarted(sessionId, runtime, event.turnId || runtime.turnId);
563
+ const phase = event.phase || "final_answer";
564
+ const messageId = this.resolveAssistantMessageId(runtime, phase, event.messageId);
565
+ this.ensureAssistantMessageStart(sessionId, runtime, phase, messageId, this.readString(event.payload.text));
566
+ return;
567
+ }
568
+ case "message.assistant.delta": {
569
+ this.ensureTurnStarted(sessionId, runtime, event.turnId || runtime.turnId);
570
+ const phase = event.phase || "final_answer";
571
+ const messageId = this.resolveAssistantMessageId(runtime, phase, event.messageId);
572
+ this.appendAssistantDelta(sessionId, runtime, phase, messageId, this.readRawText(event.payload.textDelta) || "");
573
+ return;
574
+ }
575
+ case "message.assistant.end": {
576
+ this.ensureTurnStarted(sessionId, runtime, event.turnId || runtime.turnId);
577
+ const phase = event.phase || "final_answer";
578
+ this.finishAssistantMessage(sessionId, runtime, phase, event.messageId, this.readString(event.payload.text) || "");
579
+ return;
580
+ }
581
+ case "reasoning.start": {
582
+ this.ensureTurnStarted(sessionId, runtime, event.turnId || runtime.turnId);
583
+ const messageId = this.resolveReasoningMessageId(runtime, event.messageId);
584
+ this.ensureReasoningStart(sessionId, runtime, messageId, this.readString(event.payload.summary) || null);
585
+ return;
586
+ }
587
+ case "reasoning.delta": {
588
+ this.ensureTurnStarted(sessionId, runtime, event.turnId || runtime.turnId);
589
+ const messageId = this.resolveReasoningMessageId(runtime, event.messageId);
590
+ this.appendReasoningDelta(sessionId, runtime, messageId, this.readRawText(event.payload.textDelta) || "", this.readString(event.payload.summary) || null);
591
+ return;
592
+ }
593
+ case "reasoning.end": {
594
+ this.ensureTurnStarted(sessionId, runtime, event.turnId || runtime.turnId);
595
+ const messageId = this.resolveReasoningMessageId(runtime, event.messageId);
596
+ this.finishReasoning(sessionId, runtime, messageId, this.readString(event.payload.summary) || null);
597
+ return;
598
+ }
599
+ case "command.start": {
600
+ this.ensureTurnStarted(sessionId, runtime, event.turnId || runtime.turnId);
601
+ const callId = this.resolveCommandCallId(runtime, event.callId);
602
+ this.ensureCommandStart(sessionId, runtime, callId, event.payload);
603
+ return;
604
+ }
605
+ case "command.output.delta": {
606
+ this.ensureTurnStarted(sessionId, runtime, event.turnId || runtime.turnId);
607
+ const callId = this.resolveCommandCallId(runtime, event.callId);
608
+ this.appendCommandOutputDelta(sessionId, runtime, callId, event.stream || "stdout", this.readRawText(event.payload.textDelta) || "");
609
+ return;
610
+ }
611
+ case "command.end": {
612
+ this.ensureTurnStarted(sessionId, runtime, event.turnId || runtime.turnId);
613
+ const callId = this.resolveCommandCallId(runtime, event.callId);
614
+ this.finishCommand(sessionId, runtime, callId, event.payload);
615
+ return;
616
+ }
617
+ case "patch.start": {
618
+ this.ensureTurnStarted(sessionId, runtime, event.turnId || runtime.turnId);
619
+ const callId = this.resolvePatchCallId(runtime, event.callId);
620
+ this.ensurePatchStart(sessionId, runtime, callId, event.payload);
621
+ return;
622
+ }
623
+ case "patch.output.delta": {
624
+ this.ensureTurnStarted(sessionId, runtime, event.turnId || runtime.turnId);
625
+ const callId = this.resolvePatchCallId(runtime, event.callId);
626
+ this.appendPatchOutputDelta(sessionId, runtime, callId, this.readRawText(event.payload.textDelta) || "");
627
+ return;
628
+ }
629
+ case "patch.end": {
630
+ this.ensureTurnStarted(sessionId, runtime, event.turnId || runtime.turnId);
631
+ const callId = this.resolvePatchCallId(runtime, event.callId);
632
+ this.finishPatch(sessionId, runtime, callId, event.payload);
633
+ return;
634
+ }
635
+ case "approval.requested":
636
+ case "approval.resolved":
637
+ this.appendEvent(sessionId, {
638
+ type: event.type,
639
+ turnId: event.turnId ?? runtime.turnId,
640
+ messageId: null,
641
+ callId: event.callId ?? null,
642
+ requestId: event.requestId ?? null,
643
+ phase: null,
644
+ stream: null,
645
+ payload: event.payload,
646
+ });
647
+ return;
648
+ case "error":
649
+ this.appendError(sessionId, event.turnId ?? runtime.turnId, this.readString(event.payload.message) || "Unknown error", event.payload);
650
+ return;
651
+ case "token_count": {
652
+ const normalized = this.normalizeTokenCountPayload(event.payload);
653
+ if (normalized) {
654
+ this.appendTokenCount(sessionId, event.turnId ?? runtime.turnId, normalized);
655
+ }
656
+ return;
657
+ }
658
+ }
659
+ }
660
+ handleRunnerExit(sessionId, runtime, exitCode) {
661
+ this.runners.delete(sessionId);
662
+ this.clearPid(sessionId);
663
+ if (!runtime.turnFinalized) {
664
+ if (runtime.stopRequested) {
665
+ this.completeTurn(sessionId, runtime, "turn.aborted", {
666
+ abortedAt: nowIso(),
667
+ reason: "stopped",
668
+ });
669
+ }
670
+ else if (exitCode !== 0) {
671
+ this.appendError(sessionId, runtime.turnId, `Process exited with code ${exitCode ?? -1}.`, {
672
+ exitCode,
673
+ });
674
+ this.completeTurn(sessionId, runtime, "turn.aborted", {
675
+ abortedAt: nowIso(),
676
+ reason: `exit:${exitCode ?? -1}`,
677
+ });
678
+ }
679
+ }
680
+ if (runtime.stopRequested) {
681
+ this.advanceImportedRolloutCursorToCurrentEnd(sessionId, runtime.turnFinalized);
682
+ this.setStatus(sessionId, "idle");
683
+ return;
684
+ }
685
+ if (exitCode === 0) {
686
+ this.advanceImportedRolloutCursorToCurrentEnd(sessionId, runtime.turnFinalized);
687
+ if (this.getSession(sessionId)?.status !== "failed") {
688
+ this.setStatus(sessionId, "waiting_input");
689
+ }
690
+ return;
691
+ }
692
+ this.advanceImportedRolloutCursorToCurrentEnd(sessionId, runtime.turnFinalized);
693
+ this.setStatus(sessionId, "failed");
694
+ }
695
+ advanceImportedRolloutCursorToCurrentEnd(sessionId, turnFinalized) {
696
+ const session = this.getSession(sessionId);
697
+ if (!session || session.source_kind !== "imported_rollout" || !session.source_rollout_path) {
698
+ return;
699
+ }
700
+ const rolloutPath = node_path_1.default.resolve(session.source_rollout_path);
701
+ if (!(0, node_fs_1.existsSync)(rolloutPath)) {
702
+ return;
703
+ }
704
+ const cursor = (0, node_fs_1.readFileSync)(rolloutPath, "utf8")
705
+ .split(/\r?\n/)
706
+ .map((line) => line.trimEnd())
707
+ .filter(Boolean).length;
708
+ const syncedAt = nowIso();
709
+ this.options.db
710
+ .prepare(`
711
+ UPDATE sessions
712
+ SET
713
+ source_sync_cursor = ?,
714
+ source_last_synced_at = ?,
715
+ source_rollout_has_open_turn = CASE WHEN ? THEN 0 ELSE source_rollout_has_open_turn END,
716
+ updated_at = ?
717
+ WHERE id = ?
718
+ `)
719
+ .run(cursor, syncedAt, turnFinalized ? 1 : 0, syncedAt, sessionId);
720
+ }
721
+ ensureTurnStarted(sessionId, runtime, turnId = runtime.turnId, payload = {}) {
722
+ if (runtime.turnStarted) {
723
+ if (turnId && turnId !== runtime.turnId) {
724
+ runtime.appTurnId = turnId;
725
+ }
726
+ return;
727
+ }
728
+ runtime.turnStarted = true;
729
+ if (turnId) {
730
+ runtime.appTurnId = turnId;
731
+ }
732
+ this.appendEvent(sessionId, {
733
+ type: "turn.started",
734
+ turnId: runtime.turnId,
735
+ messageId: null,
736
+ callId: null,
737
+ requestId: null,
738
+ phase: null,
739
+ stream: null,
740
+ payload: {
741
+ createdAt: nowIso(),
742
+ ...payload,
743
+ },
744
+ });
745
+ this.setStatus(sessionId, "running");
746
+ }
747
+ completeTurn(sessionId, runtime, type, payload) {
748
+ if (runtime.turnFinalized) {
749
+ return;
750
+ }
751
+ this.ensureTurnStarted(sessionId, runtime);
752
+ runtime.turnFinalized = true;
753
+ this.appendEvent(sessionId, {
754
+ type,
755
+ turnId: runtime.turnId,
756
+ messageId: null,
757
+ callId: null,
758
+ requestId: null,
759
+ phase: null,
760
+ stream: null,
761
+ payload,
762
+ });
763
+ if (type === "turn.completed") {
764
+ this.setStatus(sessionId, "waiting_input");
765
+ return;
766
+ }
767
+ if (runtime.stopRequested) {
768
+ this.setStatus(sessionId, "idle");
769
+ return;
770
+ }
771
+ this.setStatus(sessionId, "failed");
772
+ }
773
+ ensureAssistantMessageStart(sessionId, runtime, phase, messageId, initialText) {
774
+ const current = runtime.messagesById.get(messageId);
775
+ if (current?.started) {
776
+ return;
777
+ }
778
+ runtime.assistantByPhase.set(phase, messageId);
779
+ runtime.messagesById.set(messageId, {
780
+ messageId,
781
+ phase,
782
+ text: initialText || "",
783
+ started: true,
784
+ completed: false,
785
+ });
786
+ this.appendEvent(sessionId, {
787
+ type: "message.assistant.start",
788
+ turnId: runtime.turnId,
789
+ messageId,
790
+ callId: null,
791
+ requestId: null,
792
+ phase,
793
+ stream: null,
794
+ payload: initialText ? { text: initialText } : {},
795
+ });
796
+ }
797
+ appendAssistantDelta(sessionId, runtime, phase, messageId, textDelta) {
798
+ this.ensureAssistantMessageStart(sessionId, runtime, phase, messageId);
799
+ const state = runtime.messagesById.get(messageId);
800
+ if (!state || !textDelta) {
801
+ return;
802
+ }
803
+ state.text += textDelta;
804
+ this.appendEvent(sessionId, {
805
+ type: "message.assistant.delta",
806
+ turnId: runtime.turnId,
807
+ messageId,
808
+ callId: null,
809
+ requestId: null,
810
+ phase,
811
+ stream: null,
812
+ payload: {
813
+ textDelta,
814
+ },
815
+ });
816
+ }
817
+ finishAssistantMessage(sessionId, runtime, phase, providedMessageId, finalText) {
818
+ const messageId = this.resolveAssistantMessageId(runtime, phase, providedMessageId ?? null);
819
+ this.ensureAssistantMessageStart(sessionId, runtime, phase, messageId);
820
+ const state = runtime.messagesById.get(messageId);
821
+ if (!state) {
822
+ return;
823
+ }
824
+ if (finalText && !state.text) {
825
+ this.appendAssistantDelta(sessionId, runtime, phase, messageId, finalText);
826
+ }
827
+ else if (finalText && finalText !== state.text && finalText.startsWith(state.text)) {
828
+ this.appendAssistantDelta(sessionId, runtime, phase, messageId, finalText.slice(state.text.length));
829
+ }
830
+ if (state.completed) {
831
+ return;
832
+ }
833
+ state.completed = true;
834
+ const settledText = finalText || state.text;
835
+ state.text = settledText;
836
+ this.appendEvent(sessionId, {
837
+ type: "message.assistant.end",
838
+ turnId: runtime.turnId,
839
+ messageId,
840
+ callId: null,
841
+ requestId: null,
842
+ phase,
843
+ stream: null,
844
+ payload: {
845
+ text: settledText,
846
+ },
847
+ });
848
+ if (runtime.assistantByPhase.get(phase) === messageId) {
849
+ runtime.assistantByPhase.delete(phase);
850
+ }
851
+ }
852
+ ensureReasoningStart(sessionId, runtime, messageId, summary) {
853
+ if (runtime.reasoning?.messageId === messageId && runtime.reasoning.started) {
854
+ return;
855
+ }
856
+ runtime.reasoning = {
857
+ messageId,
858
+ text: summary || "",
859
+ started: true,
860
+ completed: false,
861
+ };
862
+ this.appendEvent(sessionId, {
863
+ type: "reasoning.start",
864
+ turnId: runtime.turnId,
865
+ messageId,
866
+ callId: null,
867
+ requestId: null,
868
+ phase: null,
869
+ stream: null,
870
+ payload: summary ? { summary } : {},
871
+ });
872
+ }
873
+ appendReasoningDelta(sessionId, runtime, messageId, textDelta, summary) {
874
+ this.ensureReasoningStart(sessionId, runtime, messageId, summary);
875
+ if (!runtime.reasoning || !textDelta) {
876
+ return;
877
+ }
878
+ runtime.reasoning.text += textDelta;
879
+ this.appendEvent(sessionId, {
880
+ type: "reasoning.delta",
881
+ turnId: runtime.turnId,
882
+ messageId,
883
+ callId: null,
884
+ requestId: null,
885
+ phase: null,
886
+ stream: null,
887
+ payload: {
888
+ textDelta,
889
+ ...(summary ? { summary } : {}),
890
+ },
891
+ });
892
+ }
893
+ finishReasoning(sessionId, runtime, messageId, summary) {
894
+ this.ensureReasoningStart(sessionId, runtime, messageId, summary);
895
+ if (!runtime.reasoning || runtime.reasoning.completed) {
896
+ return;
897
+ }
898
+ runtime.reasoning.completed = true;
899
+ this.appendEvent(sessionId, {
900
+ type: "reasoning.end",
901
+ turnId: runtime.turnId,
902
+ messageId,
903
+ callId: null,
904
+ requestId: null,
905
+ phase: null,
906
+ stream: null,
907
+ payload: summary ? { summary } : {},
908
+ });
909
+ }
910
+ ensureCommandStart(sessionId, runtime, callId, payload) {
911
+ const current = runtime.commandsByCallId.get(callId);
912
+ if (current?.started) {
913
+ current.command = current.command || payload.command || null;
914
+ current.cwd = current.cwd || payload.cwd || null;
915
+ return;
916
+ }
917
+ runtime.activeCommandCallId = callId;
918
+ runtime.commandsByCallId.set(callId, {
919
+ callId,
920
+ command: payload.command || null,
921
+ cwd: payload.cwd || null,
922
+ stdout: "",
923
+ stderr: "",
924
+ started: true,
925
+ completed: false,
926
+ });
927
+ this.appendEvent(sessionId, {
928
+ type: "command.start",
929
+ turnId: runtime.turnId,
930
+ messageId: null,
931
+ callId,
932
+ requestId: null,
933
+ phase: null,
934
+ stream: null,
935
+ payload,
936
+ });
937
+ }
938
+ appendCommandOutputDelta(sessionId, runtime, callId, stream, textDelta) {
939
+ this.ensureCommandStart(sessionId, runtime, callId, {
940
+ command: runtime.commandsByCallId.get(callId)?.command || "",
941
+ cwd: runtime.commandsByCallId.get(callId)?.cwd || null,
942
+ });
943
+ if (!textDelta) {
944
+ return;
945
+ }
946
+ const current = runtime.commandsByCallId.get(callId);
947
+ if (!current) {
948
+ return;
949
+ }
950
+ if (stream === "stderr") {
951
+ current.stderr += textDelta;
952
+ }
953
+ else {
954
+ current.stdout += textDelta;
955
+ }
956
+ runtime.activeCommandCallId = callId;
957
+ this.appendEvent(sessionId, {
958
+ type: "command.output.delta",
959
+ turnId: runtime.turnId,
960
+ messageId: null,
961
+ callId,
962
+ requestId: null,
963
+ phase: null,
964
+ stream,
965
+ payload: {
966
+ stream,
967
+ textDelta,
968
+ },
969
+ });
970
+ }
971
+ finishCommand(sessionId, runtime, callId, payload) {
972
+ this.ensureCommandStart(sessionId, runtime, callId, {
973
+ command: payload.command || "",
974
+ cwd: payload.cwd || null,
975
+ });
976
+ const current = runtime.commandsByCallId.get(callId);
977
+ if (!current || current.completed) {
978
+ return;
979
+ }
980
+ current.completed = true;
981
+ this.appendEvent(sessionId, {
982
+ type: "command.end",
983
+ turnId: runtime.turnId,
984
+ messageId: null,
985
+ callId,
986
+ requestId: null,
987
+ phase: null,
988
+ stream: null,
989
+ payload: {
990
+ command: payload.command || current.command,
991
+ cwd: payload.cwd || current.cwd,
992
+ status: payload.status || (payload.exitCode === 0 ? "completed" : "failed"),
993
+ exitCode: payload.exitCode ?? null,
994
+ durationMs: payload.durationMs ?? null,
995
+ rejected: payload.rejected ?? false,
996
+ },
997
+ });
998
+ if (runtime.activeCommandCallId === callId) {
999
+ runtime.activeCommandCallId = null;
1000
+ }
1001
+ }
1002
+ ensurePatchStart(sessionId, runtime, callId, payload) {
1003
+ const current = runtime.patchesByCallId.get(callId);
1004
+ if (current?.started) {
1005
+ return;
1006
+ }
1007
+ runtime.activePatchCallId = callId;
1008
+ runtime.patchesByCallId.set(callId, {
1009
+ callId,
1010
+ text: "",
1011
+ started: true,
1012
+ completed: false,
1013
+ });
1014
+ this.appendEvent(sessionId, {
1015
+ type: "patch.start",
1016
+ turnId: runtime.turnId,
1017
+ messageId: null,
1018
+ callId,
1019
+ requestId: null,
1020
+ phase: null,
1021
+ stream: null,
1022
+ payload,
1023
+ });
1024
+ }
1025
+ appendPatchOutputDelta(sessionId, runtime, callId, textDelta) {
1026
+ this.ensurePatchStart(sessionId, runtime, callId, {});
1027
+ if (!textDelta) {
1028
+ return;
1029
+ }
1030
+ const current = runtime.patchesByCallId.get(callId);
1031
+ if (!current) {
1032
+ return;
1033
+ }
1034
+ current.text += textDelta;
1035
+ runtime.activePatchCallId = callId;
1036
+ this.appendEvent(sessionId, {
1037
+ type: "patch.output.delta",
1038
+ turnId: runtime.turnId,
1039
+ messageId: null,
1040
+ callId,
1041
+ requestId: null,
1042
+ phase: null,
1043
+ stream: null,
1044
+ payload: {
1045
+ textDelta,
1046
+ },
1047
+ });
1048
+ }
1049
+ derivePatchChanges(patchText) {
1050
+ const source = String(patchText || "");
1051
+ if (!source.trim()) {
1052
+ return null;
1053
+ }
1054
+ const fileMap = new Map();
1055
+ let currentPath = null;
1056
+ const ensureFile = (path) => {
1057
+ const normalizedPath = String(path || "").trim();
1058
+ if (!normalizedPath || normalizedPath === "/dev/null") {
1059
+ return null;
1060
+ }
1061
+ const existing = fileMap.get(normalizedPath) || { added: 0, removed: 0 };
1062
+ fileMap.set(normalizedPath, existing);
1063
+ return existing;
1064
+ };
1065
+ source.split("\n").forEach((line) => {
1066
+ const diffMatch = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
1067
+ if (diffMatch) {
1068
+ currentPath = diffMatch[2];
1069
+ ensureFile(currentPath);
1070
+ return;
1071
+ }
1072
+ const nextFileMatch = line.match(/^\+\+\+ (?:b\/)?(.+)$/);
1073
+ if (nextFileMatch && nextFileMatch[1] !== "/dev/null") {
1074
+ currentPath = nextFileMatch[1];
1075
+ ensureFile(currentPath);
1076
+ return;
1077
+ }
1078
+ if (line.startsWith("@@")) {
1079
+ ensureFile(currentPath);
1080
+ return;
1081
+ }
1082
+ if (line.startsWith("+") && !line.startsWith("+++")) {
1083
+ const current = ensureFile(currentPath);
1084
+ if (current) {
1085
+ current.added += 1;
1086
+ }
1087
+ return;
1088
+ }
1089
+ if (line.startsWith("-") && !line.startsWith("---")) {
1090
+ const current = ensureFile(currentPath);
1091
+ if (current) {
1092
+ current.removed += 1;
1093
+ }
1094
+ }
1095
+ });
1096
+ if (fileMap.size === 0) {
1097
+ currentPath = null;
1098
+ source.split("\n").forEach((line) => {
1099
+ const patchFileMatch = line.match(/^\*\*\* (?:Update|Add|Delete) File: (.+)$/);
1100
+ if (patchFileMatch) {
1101
+ currentPath = patchFileMatch[1].trim();
1102
+ ensureFile(currentPath);
1103
+ return;
1104
+ }
1105
+ const outputFileMatch = line.match(/^[AMD]\s+(.+)$/);
1106
+ if (outputFileMatch) {
1107
+ currentPath = outputFileMatch[1].trim();
1108
+ ensureFile(currentPath);
1109
+ return;
1110
+ }
1111
+ if (!currentPath) {
1112
+ return;
1113
+ }
1114
+ if (line.startsWith("*** ")) {
1115
+ currentPath = null;
1116
+ return;
1117
+ }
1118
+ if (line.startsWith("+")) {
1119
+ const current = ensureFile(currentPath);
1120
+ if (current) {
1121
+ current.added += 1;
1122
+ }
1123
+ return;
1124
+ }
1125
+ if (line.startsWith("-")) {
1126
+ const current = ensureFile(currentPath);
1127
+ if (current) {
1128
+ current.removed += 1;
1129
+ }
1130
+ }
1131
+ });
1132
+ }
1133
+ if (fileMap.size === 0) {
1134
+ return null;
1135
+ }
1136
+ return Object.fromEntries(fileMap.entries());
1137
+ }
1138
+ finishPatch(sessionId, runtime, callId, payload) {
1139
+ this.ensurePatchStart(sessionId, runtime, callId, {});
1140
+ const current = runtime.patchesByCallId.get(callId);
1141
+ if (!current || current.completed) {
1142
+ return;
1143
+ }
1144
+ current.completed = true;
1145
+ const patchText = current.text || payload.patchText || "";
1146
+ const changes = this.derivePatchChanges(patchText) || payload.changes || null;
1147
+ this.appendEvent(sessionId, {
1148
+ type: "patch.end",
1149
+ turnId: runtime.turnId,
1150
+ messageId: null,
1151
+ callId,
1152
+ requestId: null,
1153
+ phase: null,
1154
+ stream: null,
1155
+ payload: {
1156
+ status: payload.status || "completed",
1157
+ durationMs: payload.durationMs ?? null,
1158
+ success: payload.success ?? (payload.status === "failed" ? false : null),
1159
+ rejected: payload.rejected ?? false,
1160
+ patchText: patchText || null,
1161
+ changes,
1162
+ },
1163
+ });
1164
+ if (runtime.activePatchCallId === callId) {
1165
+ runtime.activePatchCallId = null;
1166
+ }
1167
+ }
1168
+ appendTokenCount(sessionId, turnId, payload) {
1169
+ this.appendEvent(sessionId, {
1170
+ type: "token_count",
1171
+ turnId,
1172
+ messageId: null,
1173
+ callId: null,
1174
+ requestId: null,
1175
+ phase: null,
1176
+ stream: null,
1177
+ payload,
1178
+ });
1179
+ }
1180
+ appendError(sessionId, turnId, message, details) {
1181
+ this.appendEvent(sessionId, {
1182
+ type: "error",
1183
+ turnId: turnId || null,
1184
+ messageId: null,
1185
+ callId: null,
1186
+ requestId: null,
1187
+ phase: null,
1188
+ stream: null,
1189
+ payload: {
1190
+ message,
1191
+ details: details ?? null,
1192
+ },
1193
+ });
1194
+ }
1195
+ appendEvent(sessionId, input) {
1196
+ const event = this.options.eventStore.append(sessionId, input);
1197
+ this.touchSession(sessionId);
1198
+ return event;
1199
+ }
1200
+ touchSession(sessionId) {
1201
+ this.options.db
1202
+ .prepare(`
1203
+ UPDATE sessions
1204
+ SET updated_at = ?
1205
+ WHERE id = ?
1206
+ `)
1207
+ .run(nowIso(), sessionId);
1208
+ }
1209
+ setStatus(sessionId, status) {
1210
+ const session = this.getSessionOrThrow(sessionId);
1211
+ if (session.status === status) {
1212
+ return;
1213
+ }
1214
+ this.options.db
1215
+ .prepare(`
1216
+ UPDATE sessions
1217
+ SET status = ?, updated_at = ?
1218
+ WHERE id = ?
1219
+ `)
1220
+ .run(status, nowIso(), sessionId);
1221
+ }
1222
+ setPid(sessionId, pid) {
1223
+ this.options.db
1224
+ .prepare(`
1225
+ UPDATE sessions
1226
+ SET pid = ?, updated_at = ?
1227
+ WHERE id = ?
1228
+ `)
1229
+ .run(pid, nowIso(), sessionId);
1230
+ }
1231
+ setCodexThreadId(sessionId, threadId) {
1232
+ this.options.db
1233
+ .prepare(`
1234
+ UPDATE sessions
1235
+ SET codex_thread_id = ?, updated_at = ?
1236
+ WHERE id = ?
1237
+ `)
1238
+ .run(threadId, nowIso(), sessionId);
1239
+ }
1240
+ clearPid(sessionId) {
1241
+ this.options.db
1242
+ .prepare(`
1243
+ UPDATE sessions
1244
+ SET pid = NULL, updated_at = ?
1245
+ WHERE id = ?
1246
+ `)
1247
+ .run(nowIso(), sessionId);
1248
+ }
1249
+ getSessionOrThrow(sessionId) {
1250
+ const session = this.getSession(sessionId);
1251
+ if (!session) {
1252
+ throw new errors_1.AppError(404, "Session not found.");
1253
+ }
1254
+ return session;
1255
+ }
1256
+ messageOf(error) {
1257
+ if (error instanceof Error) {
1258
+ return error.message;
1259
+ }
1260
+ return "Unknown error";
1261
+ }
1262
+ registerPendingApproval(sessionId, approval) {
1263
+ let sessionApprovals = this.pendingApprovals.get(sessionId);
1264
+ if (!sessionApprovals) {
1265
+ sessionApprovals = new Map();
1266
+ this.pendingApprovals.set(sessionId, sessionApprovals);
1267
+ }
1268
+ sessionApprovals.set(approval.requestId, approval);
1269
+ this.appendEvent(sessionId, {
1270
+ type: "approval.requested",
1271
+ turnId: approval.turnId,
1272
+ messageId: null,
1273
+ callId: approval.callId,
1274
+ requestId: String(approval.requestId),
1275
+ phase: null,
1276
+ stream: null,
1277
+ payload: this.serializePendingApproval(approval),
1278
+ });
1279
+ }
1280
+ restorePendingApprovalFromEvents(sessionId, requestId) {
1281
+ const payload = this.options.eventStore.latestPendingApproval(sessionId);
1282
+ if (!payload || String(payload.requestId) !== String(requestId)) {
1283
+ return null;
1284
+ }
1285
+ const restored = {
1286
+ requestId,
1287
+ runnerRequestId: Number.parseInt(String(requestId), 10),
1288
+ sessionId,
1289
+ turnId: null,
1290
+ callId: payload.callId || null,
1291
+ method: payload.method,
1292
+ params: payload.rawParams && typeof payload.rawParams === "object"
1293
+ ? payload.rawParams
1294
+ : {
1295
+ reason: payload.reason,
1296
+ command: payload.command,
1297
+ cwd: payload.cwd,
1298
+ grantRoot: payload.grantRoot,
1299
+ },
1300
+ createdAt: payload.createdAt,
1301
+ };
1302
+ let sessionApprovals = this.pendingApprovals.get(sessionId);
1303
+ if (!sessionApprovals) {
1304
+ sessionApprovals = new Map();
1305
+ this.pendingApprovals.set(sessionId, sessionApprovals);
1306
+ }
1307
+ sessionApprovals.set(requestId, restored);
1308
+ return restored;
1309
+ }
1310
+ consumePendingApproval(sessionId, requestId, decision) {
1311
+ const sessionApprovals = this.pendingApprovals.get(sessionId);
1312
+ const current = sessionApprovals?.get(requestId) ?? null;
1313
+ if (decision === "acceptForSession" && current) {
1314
+ this.promoteApprovalWritableRoot(sessionId, current);
1315
+ }
1316
+ if (sessionApprovals) {
1317
+ sessionApprovals.delete(requestId);
1318
+ if (sessionApprovals.size === 0) {
1319
+ this.pendingApprovals.delete(sessionId);
1320
+ }
1321
+ }
1322
+ const resolvedPayload = {
1323
+ requestId,
1324
+ callId: current?.callId || null,
1325
+ decision,
1326
+ resolvedAt: nowIso(),
1327
+ };
1328
+ this.appendEvent(sessionId, {
1329
+ type: "approval.resolved",
1330
+ turnId: current?.turnId || null,
1331
+ messageId: null,
1332
+ callId: current?.callId || null,
1333
+ requestId,
1334
+ phase: null,
1335
+ stream: null,
1336
+ payload: resolvedPayload,
1337
+ });
1338
+ }
1339
+ serializePendingApproval(approval) {
1340
+ const params = approval.params;
1341
+ const commandText = this.extractApprovalCommand(approval.method, params);
1342
+ return {
1343
+ requestId: approval.requestId,
1344
+ callId: approval.callId,
1345
+ method: approval.method,
1346
+ title: this.describeApprovalTitle(approval.method),
1347
+ reason: typeof params.reason === "string" && params.reason.trim() ? params.reason.trim() : null,
1348
+ command: commandText,
1349
+ cwd: typeof params.cwd === "string" && params.cwd.trim() ? params.cwd.trim() : null,
1350
+ grantRoot: typeof params.grantRoot === "string" && params.grantRoot.trim()
1351
+ ? params.grantRoot.trim()
1352
+ : null,
1353
+ createdAt: approval.createdAt,
1354
+ rawParams: params,
1355
+ resumable: true,
1356
+ source: "live",
1357
+ };
1358
+ }
1359
+ extractApprovalCommand(method, params) {
1360
+ if (typeof params.command === "string" && params.command.trim()) {
1361
+ return params.command.trim();
1362
+ }
1363
+ if (method === "execCommandApproval" && Array.isArray(params.command)) {
1364
+ const command = params.command
1365
+ .filter((item) => typeof item === "string" && item.trim().length > 0)
1366
+ .join(" ");
1367
+ return command || null;
1368
+ }
1369
+ return null;
1370
+ }
1371
+ describeApprovalTitle(method) {
1372
+ if (method === "item/commandExecution/requestApproval" || method === "execCommandApproval") {
1373
+ return "Command execution requires approval";
1374
+ }
1375
+ if (method === "item/fileChange/requestApproval" || method === "applyPatchApproval") {
1376
+ return "File changes require approval";
1377
+ }
1378
+ if (method === "item/permissions/requestApproval") {
1379
+ return "Extra permissions require approval";
1380
+ }
1381
+ return "Approval required";
1382
+ }
1383
+ withSessionWritableRoots(sessionId, launch) {
1384
+ const roots = this.sessionWritableRoots.get(sessionId);
1385
+ if (!roots || roots.size === 0) {
1386
+ return launch;
1387
+ }
1388
+ const merged = new Set(launch?.additionalWritableRoots ?? []);
1389
+ for (const root of roots) {
1390
+ merged.add(root);
1391
+ }
1392
+ return {
1393
+ ...(launch ?? {}),
1394
+ additionalWritableRoots: [...merged],
1395
+ };
1396
+ }
1397
+ promoteApprovalWritableRoot(sessionId, approval) {
1398
+ const root = this.extractApprovalWritableRoot(approval);
1399
+ if (!root) {
1400
+ return;
1401
+ }
1402
+ let roots = this.sessionWritableRoots.get(sessionId);
1403
+ if (!roots) {
1404
+ roots = new Set();
1405
+ this.sessionWritableRoots.set(sessionId, roots);
1406
+ }
1407
+ roots.add(root);
1408
+ }
1409
+ extractApprovalWritableRoot(approval) {
1410
+ const params = approval.params;
1411
+ if (typeof params.grantRoot === "string" && params.grantRoot.trim()) {
1412
+ return params.grantRoot.trim();
1413
+ }
1414
+ const reason = typeof params.reason === "string" ? params.reason.trim() : "";
1415
+ const reasonMatch = reason.match(/\sin\s(\/[^\s?]+)/);
1416
+ if (reasonMatch?.[1]) {
1417
+ return reasonMatch[1];
1418
+ }
1419
+ const command = this.extractApprovalCommand(approval.method, params);
1420
+ const commandMatch = command?.match(/\/Users\/[^\s'"]+/);
1421
+ if (commandMatch?.[0]) {
1422
+ const matchedPath = commandMatch[0];
1423
+ const slashIndex = matchedPath.lastIndexOf("/");
1424
+ return slashIndex > 0 ? matchedPath.slice(0, slashIndex) : matchedPath;
1425
+ }
1426
+ return null;
1427
+ }
1428
+ buildApprovalResponsePayload(approval, decision) {
1429
+ switch (approval.method) {
1430
+ case "item/commandExecution/requestApproval":
1431
+ case "item/fileChange/requestApproval":
1432
+ return {
1433
+ decision: decision === "decline"
1434
+ ? "decline"
1435
+ : decision === "acceptForSession"
1436
+ ? "acceptForSession"
1437
+ : "accept",
1438
+ };
1439
+ case "item/permissions/requestApproval": {
1440
+ const permissions = approval.params.permissions && typeof approval.params.permissions === "object"
1441
+ ? approval.params.permissions
1442
+ : {};
1443
+ return {
1444
+ permissions: decision === "decline" ? {} : permissions,
1445
+ scope: decision === "acceptForSession" ? "session" : "turn",
1446
+ };
1447
+ }
1448
+ case "execCommandApproval":
1449
+ case "applyPatchApproval":
1450
+ return {
1451
+ decision: decision === "decline"
1452
+ ? "denied"
1453
+ : decision === "acceptForSession"
1454
+ ? "approved_for_session"
1455
+ : "approved",
1456
+ };
1457
+ default:
1458
+ return {
1459
+ decision: "decline",
1460
+ };
1461
+ }
1462
+ }
1463
+ resolveAssistantMessageId(runtime, phase, providedMessageId) {
1464
+ if (providedMessageId) {
1465
+ runtime.assistantByPhase.set(phase, providedMessageId);
1466
+ return providedMessageId;
1467
+ }
1468
+ const current = runtime.assistantByPhase.get(phase);
1469
+ if (current) {
1470
+ const item = runtime.messagesById.get(current);
1471
+ if (item && !item.completed) {
1472
+ return current;
1473
+ }
1474
+ }
1475
+ const next = (0, ids_1.createId)("msg");
1476
+ runtime.assistantByPhase.set(phase, next);
1477
+ return next;
1478
+ }
1479
+ resolveReasoningMessageId(runtime, providedMessageId) {
1480
+ if (providedMessageId) {
1481
+ return providedMessageId;
1482
+ }
1483
+ if (runtime.reasoning && !runtime.reasoning.completed) {
1484
+ return runtime.reasoning.messageId;
1485
+ }
1486
+ return (0, ids_1.createId)("msg");
1487
+ }
1488
+ resolveCommandCallId(runtime, providedCallId) {
1489
+ if (providedCallId) {
1490
+ runtime.activeCommandCallId = providedCallId;
1491
+ return providedCallId;
1492
+ }
1493
+ if (runtime.activeCommandCallId) {
1494
+ const current = runtime.commandsByCallId.get(runtime.activeCommandCallId);
1495
+ if (current && !current.completed) {
1496
+ return runtime.activeCommandCallId;
1497
+ }
1498
+ }
1499
+ const next = (0, ids_1.createId)("call");
1500
+ runtime.activeCommandCallId = next;
1501
+ return next;
1502
+ }
1503
+ resolvePatchCallId(runtime, providedCallId) {
1504
+ if (providedCallId) {
1505
+ runtime.activePatchCallId = providedCallId;
1506
+ return providedCallId;
1507
+ }
1508
+ if (runtime.activePatchCallId) {
1509
+ const current = runtime.patchesByCallId.get(runtime.activePatchCallId);
1510
+ if (current && !current.completed) {
1511
+ return runtime.activePatchCallId;
1512
+ }
1513
+ }
1514
+ const next = (0, ids_1.createId)("call");
1515
+ runtime.activePatchCallId = next;
1516
+ return next;
1517
+ }
1518
+ normalizeAssistantPhase(value) {
1519
+ return value === "commentary" ? "commentary" : "final_answer";
1520
+ }
1521
+ extractResponseItemMessageText(payload) {
1522
+ const content = Array.isArray(payload.content) ? payload.content : [];
1523
+ return content
1524
+ .map((item) => {
1525
+ if (!item || typeof item !== "object") {
1526
+ return "";
1527
+ }
1528
+ const text = item.text;
1529
+ return typeof text === "string" ? text : "";
1530
+ })
1531
+ .filter(Boolean)
1532
+ .join("\n");
1533
+ }
1534
+ parseJsonObject(raw) {
1535
+ if (!raw) {
1536
+ return {};
1537
+ }
1538
+ try {
1539
+ const parsed = JSON.parse(raw);
1540
+ return parsed && typeof parsed === "object" ? parsed : {};
1541
+ }
1542
+ catch {
1543
+ return {};
1544
+ }
1545
+ }
1546
+ normalizeTokenCountPayload(payload) {
1547
+ const rateLimits = payload.rate_limits && typeof payload.rate_limits === "object"
1548
+ ? payload.rate_limits
1549
+ : payload.rateLimits && typeof payload.rateLimits === "object"
1550
+ ? payload.rateLimits
1551
+ : null;
1552
+ if (!rateLimits) {
1553
+ return null;
1554
+ }
1555
+ const info = payload.info && typeof payload.info === "object"
1556
+ ? payload.info
1557
+ : undefined;
1558
+ const totalTokenUsage = info?.total_token_usage && typeof info.total_token_usage === "object"
1559
+ ? info.total_token_usage
1560
+ : undefined;
1561
+ const lastTokenUsage = info?.last_token_usage && typeof info.last_token_usage === "object"
1562
+ ? info.last_token_usage
1563
+ : undefined;
1564
+ const modelContextWindow = typeof info?.model_context_window === "number"
1565
+ ? info.model_context_window
1566
+ : typeof info?.model_context_window === "string"
1567
+ ? Number.parseInt(info.model_context_window, 10)
1568
+ : undefined;
1569
+ return {
1570
+ rateLimits,
1571
+ totalTokenUsage,
1572
+ lastTokenUsage,
1573
+ modelContextWindow: Number.isFinite(modelContextWindow) ? modelContextWindow : undefined,
1574
+ receivedAt: nowIso(),
1575
+ rawPayload: payload,
1576
+ source: "live",
1577
+ };
1578
+ }
1579
+ readString(value) {
1580
+ return typeof value === "string" && value.trim() ? value.trim() : null;
1581
+ }
1582
+ readRawText(value) {
1583
+ return typeof value === "string" ? value : null;
1584
+ }
1585
+ }
1586
+ exports.SessionManager = SessionManager;