remcodex 0.1.0-beta.1 → 0.1.0-beta.11

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/README.md CHANGED
@@ -6,38 +6,6 @@ RemCodex is a local-first web UI for running, reviewing, approving, and resuming
6
6
 
7
7
  It is built for the real workflow: long-running sessions, mobile check-ins, approval prompts, imported rollout history, and timeline-style execution flow.
8
8
 
9
- ```mermaid
10
- flowchart LR
11
- subgraph D[Your devices]
12
- P[Phone]
13
- B[Browser]
14
- end
15
-
16
- subgraph M[Your work machine]
17
- subgraph R[RemCodex]
18
- UI[Web UI]
19
- S[Server]
20
- T[Timeline + approvals + sync]
21
- end
22
-
23
- subgraph C[Local Codex runtime]
24
- X[Codex CLI / app-server]
25
- F[Workspace files]
26
- H[~/.codex sessions]
27
- end
28
- end
29
-
30
- P --> UI
31
- B --> UI
32
- UI --> S
33
- S --> T
34
- S --> X
35
- X --> F
36
- X --> H
37
- H --> S
38
- T --> UI
39
- ```
40
-
41
9
  - Watch live Codex runs without staying in the terminal
42
10
  - Approve sensitive actions from a cleaner UI
43
11
  - Pick up the same session again after refresh, sleep, or reconnect
@@ -46,10 +14,6 @@ flowchart LR
46
14
 
47
15
  This project is currently a **beta / developer preview**.
48
16
 
49
- MIT licensed — free for personal and commercial use.
50
-
51
- Cloud version coming soon.
52
-
53
17
  It is already usable for local and internal workflows, but it is not yet packaged as a one-click desktop app.
54
18
 
55
19
  ## Why People Use It
@@ -134,7 +98,7 @@ RemCodex turns Codex's event stream into a browser-based workspace that is easie
134
98
 
135
99
  Before running this project, you should have:
136
100
 
137
- - Node.js installed
101
+ - Node.js 20.x installed
138
102
  - Codex CLI installed and already working locally
139
103
  - A machine where this app can access your local Codex data and working directories
140
104
 
@@ -151,6 +115,8 @@ npm link
151
115
  remcodex start
152
116
  ```
153
117
 
118
+ If you switch Node.js versions later, reinstall dependencies and relink or reinstall `remcodex`.
119
+
154
120
  Then open:
155
121
 
156
122
  ```text
@@ -177,11 +143,16 @@ node dist/server/src/cli.js start --no-open
177
143
  node dist/server/src/cli.js version
178
144
  ```
179
145
 
146
+ Published package note:
147
+
148
+ - the npm package currently expects Node.js 20.x
149
+ - if `better-sqlite3` or `node-pty` reports a native module / `NODE_MODULE_VERSION` error, reinstall `remcodex` under the same Node.js version you will use to run it
150
+
180
151
  Use a specific database:
181
152
 
182
153
  ```bash
183
- node dist/server/src/cli.js start --db ~/.remcodex/remcodex-alt.db --no-open
184
- node dist/server/src/cli.js doctor --db ~/.remcodex/remcodex-alt.db
154
+ node dist/server/src/cli.js start --db ~/.remcodex/remcodex-demo.db --no-open
155
+ node dist/server/src/cli.js doctor --db ~/.remcodex/remcodex-demo.db
185
156
  ```
186
157
 
187
158
  Planned install target after the npm package is published:
@@ -197,6 +168,18 @@ npm install
197
168
  npm run dev
198
169
  ```
199
170
 
171
+ Smoke-test the published-package shape locally:
172
+
173
+ ```bash
174
+ npm run smoke:tarball
175
+ ```
176
+
177
+ Smoke-test a real isolated startup and `/health` check:
178
+
179
+ ```bash
180
+ npm run smoke:start
181
+ ```
182
+
200
183
  ## How It Works
201
184
 
202
185
  The app uses `codex app-server` as the primary runtime path.
@@ -249,6 +232,12 @@ Supported environment variables:
249
232
  - `PROJECT_ROOTS`
250
233
  - `CODEX_COMMAND`
251
234
  - `CODEX_MODE`
235
+
236
+ For launch screenshots or demo data, you can rebuild a clean demo database with:
237
+
238
+ ```bash
239
+ DATABASE_PATH="$HOME/.remcodex/remcodex-demo.db" ~/.nvm/versions/node/v20.19.5/bin/node scripts/seed-launch-demo-data.js --clean
240
+ ```
252
241
  - `REMOTE_HOSTS`
253
242
  - `ACTIVE_REMOTE_HOST`
254
243
 
@@ -278,6 +267,8 @@ web/
278
267
  app.js
279
268
  scripts/
280
269
  fix-node-pty-helper.js
270
+ import-codex-rollout.js
271
+ reset-semantic-demo-data.js
281
272
  ```
282
273
 
283
274
  ## Main Endpoints
@@ -3,12 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.resolvePackageRoot = resolvePackageRoot;
7
- exports.resolveDefaultDatabasePath = resolveDefaultDatabasePath;
8
6
  exports.startRemCodexServer = startRemCodexServer;
9
7
  const node_fs_1 = require("node:fs");
10
8
  const node_http_1 = __importDefault(require("node:http"));
11
- const node_os_1 = require("node:os");
12
9
  const node_path_1 = __importDefault(require("node:path"));
13
10
  const express_1 = __importDefault(require("express"));
14
11
  const codex_options_controller_1 = require("./controllers/codex-options.controller");
@@ -23,35 +20,15 @@ const codex_rollout_sync_1 = require("./services/codex-rollout-sync");
23
20
  const project_manager_1 = require("./services/project-manager");
24
21
  const session_manager_1 = require("./services/session-manager");
25
22
  const session_timeline_service_1 = require("./services/session-timeline-service");
23
+ const runtime_paths_1 = require("./utils/runtime-paths");
26
24
  const command_1 = require("./utils/command");
27
25
  const errors_1 = require("./utils/errors");
28
- function isPackageRoot(root) {
29
- return ((0, node_fs_1.existsSync)(node_path_1.default.join(root, "package.json")) &&
30
- (0, node_fs_1.existsSync)(node_path_1.default.join(root, "web", "index.html")));
31
- }
32
- function resolvePackageRoot(startDir = __dirname) {
33
- let current = node_path_1.default.resolve(startDir);
34
- while (true) {
35
- if (isPackageRoot(current)) {
36
- return current;
37
- }
38
- const parent = node_path_1.default.dirname(current);
39
- if (parent === current) {
40
- break;
41
- }
42
- current = parent;
43
- }
44
- return process.cwd();
45
- }
46
- function resolveDefaultDatabasePath() {
47
- return node_path_1.default.join((0, node_os_1.homedir)(), ".remcodex", "remcodex.db");
48
- }
49
26
  function buildRemCodexServer(options = {}) {
50
- const repoRoot = options.repoRoot ? node_path_1.default.resolve(options.repoRoot) : resolvePackageRoot();
27
+ const repoRoot = options.repoRoot ? node_path_1.default.resolve(options.repoRoot) : (0, runtime_paths_1.resolvePackageRoot)();
51
28
  const port = options.port ?? Number.parseInt(process.env.PORT ?? "3000", 10);
52
29
  const databasePath = options.databasePath ??
53
30
  process.env.DATABASE_PATH ??
54
- resolveDefaultDatabasePath();
31
+ (0, runtime_paths_1.resolveDefaultDatabasePath)();
55
32
  const codexCommand = (0, command_1.resolveExecutable)(options.codexCommand ?? process.env.CODEX_COMMAND ?? "codex");
56
33
  const codexMode = options.codexMode ?? (process.env.CODEX_MODE === "exec-json" ? "exec-json" : "app-server");
57
34
  const projectRootsEnv = options.projectRootsEnv ?? process.env.PROJECT_ROOTS;
@@ -1,5 +1,38 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
3
36
  var __importDefault = (this && this.__importDefault) || function (mod) {
4
37
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
38
  };
@@ -8,7 +41,7 @@ const node_fs_1 = require("node:fs");
8
41
  const node_child_process_1 = require("node:child_process");
9
42
  const node_os_1 = require("node:os");
10
43
  const node_path_1 = __importDefault(require("node:path"));
11
- const app_1 = require("./app");
44
+ const runtime_paths_1 = require("./utils/runtime-paths");
12
45
  const command_1 = require("./utils/command");
13
46
  function print(message = "") {
14
47
  process.stdout.write(`${message}\n`);
@@ -18,7 +51,7 @@ function printError(message = "") {
18
51
  }
19
52
  function readPackageVersion() {
20
53
  try {
21
- const packageRoot = (0, app_1.resolvePackageRoot)();
54
+ const packageRoot = (0, runtime_paths_1.resolvePackageRoot)();
22
55
  const packageJson = JSON.parse((0, node_fs_1.readFileSync)(node_path_1.default.join(packageRoot, "package.json"), "utf8"));
23
56
  return typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
24
57
  }
@@ -123,12 +156,24 @@ function usage() {
123
156
  print(" --db <path> Use a specific SQLite database path");
124
157
  print(" --no-open Do not open a browser automatically");
125
158
  }
159
+ function formatNativeModuleError(error) {
160
+ const message = error instanceof Error ? error.message : String(error);
161
+ if (!message.includes("NODE_MODULE_VERSION")) {
162
+ return null;
163
+ }
164
+ return [
165
+ "Native module failed to load. This usually means RemCodex was installed with a different Node.js version.",
166
+ `Current Node: ${process.version}`,
167
+ "Reinstall remcodex with the same Node.js version you will use to run it.",
168
+ "If you use nvm/fnm/asdf, switch to the target Node version first, then reinstall.",
169
+ ].join("\n");
170
+ }
126
171
  async function runDoctor(flags) {
127
172
  const version = readPackageVersion();
128
173
  const rawCodexCommand = process.env.CODEX_COMMAND ?? "codex";
129
174
  const codex = commandExists(rawCodexCommand);
130
- const packageRoot = (0, app_1.resolvePackageRoot)();
131
- const databasePath = flags.databasePath ?? process.env.DATABASE_PATH ?? (0, app_1.resolveDefaultDatabasePath)();
175
+ const packageRoot = (0, runtime_paths_1.resolvePackageRoot)();
176
+ const databasePath = flags.databasePath ?? process.env.DATABASE_PATH ?? (0, runtime_paths_1.resolveDefaultDatabasePath)();
132
177
  const databaseDir = node_path_1.default.dirname(databasePath);
133
178
  const databaseDirExists = (0, node_fs_1.existsSync)(databaseDir);
134
179
  const databaseDirWritable = databaseDirExists && (() => {
@@ -161,6 +206,18 @@ async function runDoctor(flags) {
161
206
  return 0;
162
207
  }
163
208
  async function runStart(flags) {
209
+ let startRemCodexServer;
210
+ try {
211
+ ({ startRemCodexServer } = await Promise.resolve().then(() => __importStar(require("./app"))));
212
+ }
213
+ catch (error) {
214
+ const nativeModuleMessage = formatNativeModuleError(error);
215
+ if (nativeModuleMessage) {
216
+ printError(nativeModuleMessage);
217
+ return 1;
218
+ }
219
+ throw error;
220
+ }
164
221
  const version = readPackageVersion();
165
222
  const rawCodexCommand = process.env.CODEX_COMMAND ?? "codex";
166
223
  const codex = commandExists(rawCodexCommand);
@@ -176,7 +233,7 @@ async function runStart(flags) {
176
233
  for (let offset = 0; offset < 20; offset += 1) {
177
234
  const candidate = preferredPort + offset;
178
235
  try {
179
- started = await (0, app_1.startRemCodexServer)({
236
+ started = await startRemCodexServer({
180
237
  port: candidate,
181
238
  databasePath: flags.databasePath,
182
239
  codexCommand: rawCodexCommand,
@@ -242,8 +299,9 @@ async function main() {
242
299
  usage();
243
300
  return;
244
301
  }
245
- const command = argv[0] && !argv[0].startsWith("-") ? argv[0] : "start";
246
- const flagArgs = command === "start" ? argv.slice(1) : argv;
302
+ const hasExplicitCommand = Boolean(argv[0] && !argv[0].startsWith("-"));
303
+ const command = hasExplicitCommand ? argv[0] : "start";
304
+ const flagArgs = hasExplicitCommand ? argv.slice(1) : argv;
247
305
  const flags = parseFlags(flagArgs);
248
306
  switch (command) {
249
307
  case "start":
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createSessionRouter = createSessionRouter;
4
4
  const express_1 = require("express");
5
+ const codex_launch_1 = require("../utils/codex-launch");
5
6
  function createSessionRouter(sessionManager, eventStore, projectManager, codexRolloutSync, sessionTimeline) {
6
7
  const router = (0, express_1.Router)();
7
8
  router.get("/", (_request, response) => {
@@ -171,5 +172,16 @@ function createSessionRouter(sessionManager, eventStore, projectManager, codexRo
171
172
  next(error);
172
173
  }
173
174
  });
175
+ router.post("/:sessionId/approvals/:requestId/retry", (request, response, next) => {
176
+ try {
177
+ const body = request.body;
178
+ const launch = (0, codex_launch_1.normalizeCodexExecLaunchInput)(body.codex);
179
+ const result = sessionManager.retryApprovalRequest(request.params.sessionId, request.params.requestId, launch);
180
+ response.json(result);
181
+ }
182
+ catch (error) {
183
+ next(error);
184
+ }
185
+ });
174
186
  return router;
175
187
  }
@@ -6,6 +6,35 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runMigrations = runMigrations;
7
7
  const node_fs_1 = require("node:fs");
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
+ function isPackageRoot(root) {
10
+ return (0, node_fs_1.existsSync)(node_path_1.default.join(root, "package.json")) && (0, node_fs_1.existsSync)(node_path_1.default.join(root, "web", "index.html"));
11
+ }
12
+ function resolvePackageRoot(startDir = __dirname) {
13
+ let current = node_path_1.default.resolve(startDir);
14
+ while (true) {
15
+ if (isPackageRoot(current)) {
16
+ return current;
17
+ }
18
+ const parent = node_path_1.default.dirname(current);
19
+ if (parent === current) {
20
+ break;
21
+ }
22
+ current = parent;
23
+ }
24
+ return process.cwd();
25
+ }
26
+ function resolveSchemaFile() {
27
+ const packageRoot = resolvePackageRoot();
28
+ const candidates = [
29
+ node_path_1.default.join(packageRoot, "server", "src", "db", "schema.sql"),
30
+ node_path_1.default.join(packageRoot, "dist", "server", "src", "db", "schema.sql"),
31
+ ];
32
+ const resolved = candidates.find((candidate) => (0, node_fs_1.existsSync)(candidate));
33
+ if (!resolved) {
34
+ throw new Error(`Database schema file not found. Tried: ${candidates.join(", ")}`);
35
+ }
36
+ return resolved;
37
+ }
9
38
  function ensureColumn(db, table, column, definition) {
10
39
  const rows = db.prepare(`PRAGMA table_info(${table})`).all();
11
40
  if (rows.some((row) => row.name === column)) {
@@ -14,7 +43,7 @@ function ensureColumn(db, table, column, definition) {
14
43
  db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
15
44
  }
16
45
  function runMigrations(db) {
17
- const schemaFile = node_path_1.default.join(process.cwd(), "server", "src", "db", "schema.sql");
46
+ const schemaFile = resolveSchemaFile();
18
47
  const schema = (0, node_fs_1.readFileSync)(schemaFile, "utf8");
19
48
  db.exec(schema);
20
49
  ensureColumn(db, "sessions", "source_kind", "TEXT NOT NULL DEFAULT 'native'");
@@ -0,0 +1,48 @@
1
+ CREATE TABLE IF NOT EXISTS projects (
2
+ id TEXT PRIMARY KEY,
3
+ name TEXT NOT NULL,
4
+ path TEXT NOT NULL,
5
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
6
+ );
7
+
8
+ CREATE TABLE IF NOT EXISTS sessions (
9
+ id TEXT PRIMARY KEY,
10
+ title TEXT,
11
+ project_id TEXT NOT NULL,
12
+ status TEXT NOT NULL,
13
+ pid INTEGER,
14
+ codex_thread_id TEXT,
15
+ source_kind TEXT NOT NULL DEFAULT 'native',
16
+ source_rollout_path TEXT,
17
+ source_thread_id TEXT,
18
+ source_sync_cursor INTEGER,
19
+ source_last_synced_at TEXT,
20
+ source_rollout_has_open_turn INTEGER NOT NULL DEFAULT 0,
21
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
22
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
23
+ FOREIGN KEY (project_id) REFERENCES projects(id)
24
+ );
25
+
26
+ CREATE TABLE IF NOT EXISTS session_events (
27
+ id TEXT PRIMARY KEY,
28
+ session_id TEXT NOT NULL,
29
+ turn_id TEXT,
30
+ seq INTEGER NOT NULL,
31
+ event_type TEXT NOT NULL,
32
+ message_id TEXT,
33
+ call_id TEXT,
34
+ request_id TEXT,
35
+ phase TEXT,
36
+ stream TEXT,
37
+ payload_json TEXT NOT NULL,
38
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
39
+ FOREIGN KEY (session_id) REFERENCES sessions(id),
40
+ UNIQUE (session_id, seq)
41
+ );
42
+
43
+ CREATE INDEX IF NOT EXISTS idx_sessions_project_id ON sessions(project_id);
44
+ CREATE INDEX IF NOT EXISTS idx_session_events_session_seq ON session_events(session_id, seq);
45
+ CREATE INDEX IF NOT EXISTS idx_session_events_type_seq ON session_events(session_id, event_type, seq);
46
+ CREATE INDEX IF NOT EXISTS idx_session_events_message_id ON session_events(session_id, message_id, seq);
47
+ CREATE INDEX IF NOT EXISTS idx_session_events_call_id ON session_events(session_id, call_id, seq);
48
+ CREATE INDEX IF NOT EXISTS idx_session_events_request_id ON session_events(session_id, request_id, seq);
@@ -9,6 +9,7 @@ const node_os_1 = __importDefault(require("node:os"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
10
  const errors_1 = require("../utils/errors");
11
11
  const ids_1 = require("../utils/ids");
12
+ const output_limits_1 = require("../utils/output-limits");
12
13
  function computeSourceRolloutHasOpenTurnFromRecords(records) {
13
14
  const openTurnIds = new Set();
14
15
  for (const record of records) {
@@ -571,22 +572,7 @@ function translateRolloutRecords(records, emitFromRecordIndex = 0) {
571
572
  : `call_${index}`;
572
573
  const started = commandStarts.get(callId) || null;
573
574
  const parsed = parseExecOutput(payload.output);
574
- if (parsed.outputText) {
575
- appendSemantic(index, {
576
- type: "command.output.delta",
577
- turnId: currentTurnId,
578
- messageId: null,
579
- callId,
580
- requestId: null,
581
- phase: null,
582
- stream: "stdout",
583
- payload: {
584
- stream: "stdout",
585
- textDelta: parsed.outputText,
586
- },
587
- timestamp,
588
- });
589
- }
575
+ const cappedOutput = parsed.outputText ? (0, output_limits_1.capTextValue)(parsed.outputText) : null;
590
576
  appendSemantic(index, {
591
577
  type: "command.end",
592
578
  turnId: currentTurnId,
@@ -598,6 +584,9 @@ function translateRolloutRecords(records, emitFromRecordIndex = 0) {
598
584
  payload: {
599
585
  command: parsed.commandLine || started?.commandPayload.command || null,
600
586
  cwd: started?.commandPayload.cwd || null,
587
+ stdout: cappedOutput?.text || null,
588
+ aggregatedOutput: cappedOutput?.text || null,
589
+ stdoutTruncated: cappedOutput?.truncated || undefined,
601
590
  status: parsed.exitCode == null
602
591
  ? "completed"
603
592
  : parsed.exitCode === 0
@@ -55,9 +55,24 @@ class EventStore {
55
55
  `)
56
56
  .get(id);
57
57
  const event = this.toPayload(row);
58
- this.captureLatestQuota(sessionId, event);
59
- this.emitter.emit(this.channel(sessionId), event);
60
- return event;
58
+ return this.publish(event);
59
+ }
60
+ publishTransient(sessionId, input, seq) {
61
+ const event = {
62
+ id: input.id?.trim() || (0, ids_1.createId)("evt"),
63
+ sessionId,
64
+ type: input.type,
65
+ seq,
66
+ timestamp: input.timestamp?.trim() || new Date().toISOString(),
67
+ turnId: input.turnId ?? null,
68
+ messageId: input.messageId ?? null,
69
+ callId: input.callId ?? null,
70
+ requestId: input.requestId ?? null,
71
+ phase: input.phase ?? null,
72
+ stream: this.normalizeStream(input.stream),
73
+ payload: input.payload ?? {},
74
+ };
75
+ return this.publish(event);
61
76
  }
62
77
  list(sessionId, options = {}) {
63
78
  const safeLimit = Math.max(1, Math.min(options.limit ?? 200, 200));
@@ -241,7 +256,7 @@ class EventStore {
241
256
  this.emitter.off(channel, listener);
242
257
  };
243
258
  }
244
- nextSeq(sessionId) {
259
+ latestSeq(sessionId) {
245
260
  const row = this.db
246
261
  .prepare(`
247
262
  SELECT COALESCE(MAX(seq), 0) AS current_seq
@@ -249,7 +264,15 @@ class EventStore {
249
264
  WHERE session_id = ?
250
265
  `)
251
266
  .get(sessionId);
252
- return row.current_seq + 1;
267
+ return row.current_seq;
268
+ }
269
+ nextSeq(sessionId) {
270
+ return this.latestSeq(sessionId) + 1;
271
+ }
272
+ publish(event) {
273
+ this.captureLatestQuota(event.sessionId, event);
274
+ this.emitter.emit(this.channel(event.sessionId), event);
275
+ return event;
253
276
  }
254
277
  toPayload(row) {
255
278
  return {
@@ -9,11 +9,13 @@ const node_os_1 = __importDefault(require("node:os"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
10
  const errors_1 = require("../utils/errors");
11
11
  const ids_1 = require("../utils/ids");
12
+ const output_limits_1 = require("../utils/output-limits");
12
13
  const codex_runner_1 = require("./codex-runner");
13
14
  const codex_stream_events_1 = require("./codex-stream-events");
14
15
  function nowIso() {
15
16
  return new Date().toISOString();
16
17
  }
18
+ const TRANSIENT_SEQ_STEP = 0.00001;
17
19
  function shouldAutotitleSession(title) {
18
20
  const normalized = String(title || "").trim();
19
21
  return (!normalized ||
@@ -277,6 +279,30 @@ class SessionManager {
277
279
  seq: event.seq,
278
280
  };
279
281
  }
282
+ retryApprovalRequest(sessionId, requestId, codexLaunch) {
283
+ const session = this.getSessionOrThrow(sessionId);
284
+ const project = this.options.projectManager.getProject(session.project_id);
285
+ if (!project) {
286
+ throw new errors_1.AppError(404, "Project not found for session.");
287
+ }
288
+ const currentRunner = this.runners.get(sessionId);
289
+ const busyStatuses = ["starting", "running", "stopping"];
290
+ if (currentRunner?.runner.isAlive() && busyStatuses.includes(session.status)) {
291
+ throw new errors_1.AppError(409, "Session already has an active task.");
292
+ }
293
+ const pending = this.pendingApprovals.get(sessionId)?.get(requestId) ??
294
+ this.restorePendingApprovalFromEvents(sessionId, requestId);
295
+ if (!pending) {
296
+ throw new errors_1.AppError(404, "Approval request not found.");
297
+ }
298
+ const turnId = (0, ids_1.createId)("turn");
299
+ const runtimePrompt = normalizeDemoPrompt(project.path, this.buildApprovalRetryRuntimePrompt(pending));
300
+ this.startRunner(sessionId, project.path, runtimePrompt, turnId, this.resolveResumeThreadId(session), codexLaunch);
301
+ return {
302
+ accepted: true,
303
+ turnId,
304
+ };
305
+ }
280
306
  stopSession(sessionId) {
281
307
  const runtime = this.runners.get(sessionId);
282
308
  if (!runtime || !runtime.runner.isAlive()) {
@@ -322,6 +348,7 @@ class SessionManager {
322
348
  const runtime = {
323
349
  runner,
324
350
  stopRequested: false,
351
+ transientSeqCursor: this.options.eventStore.latestSeq(sessionId),
325
352
  turnId,
326
353
  appTurnId: null,
327
354
  turnStarted: false,
@@ -921,6 +948,8 @@ class SessionManager {
921
948
  cwd: payload.cwd || null,
922
949
  stdout: "",
923
950
  stderr: "",
951
+ stdoutTruncated: false,
952
+ stderrTruncated: false,
924
953
  started: true,
925
954
  completed: false,
926
955
  });
@@ -947,14 +976,15 @@ class SessionManager {
947
976
  if (!current) {
948
977
  return;
949
978
  }
950
- if (stream === "stderr") {
951
- current.stderr += textDelta;
952
- }
953
- else {
954
- current.stdout += textDelta;
979
+ const targetKey = stream === "stderr" ? "stderr" : "stdout";
980
+ const truncatedKey = stream === "stderr" ? "stderrTruncated" : "stdoutTruncated";
981
+ const capped = (0, output_limits_1.appendCappedText)(current[targetKey], textDelta);
982
+ current[targetKey] = capped.nextText;
983
+ if (capped.truncated) {
984
+ current[truncatedKey] = true;
955
985
  }
956
986
  runtime.activeCommandCallId = callId;
957
- this.appendEvent(sessionId, {
987
+ this.publishTransientEvent(sessionId, runtime, {
958
988
  type: "command.output.delta",
959
989
  turnId: runtime.turnId,
960
990
  messageId: null,
@@ -989,6 +1019,11 @@ class SessionManager {
989
1019
  payload: {
990
1020
  command: payload.command || current.command,
991
1021
  cwd: payload.cwd || current.cwd,
1022
+ stdout: current.stdout || null,
1023
+ stderr: current.stderr || null,
1024
+ aggregatedOutput: current.stdout || current.stderr || null,
1025
+ stdoutTruncated: current.stdoutTruncated || undefined,
1026
+ stderrTruncated: current.stderrTruncated || undefined,
992
1027
  status: payload.status || (payload.exitCode === 0 ? "completed" : "failed"),
993
1028
  exitCode: payload.exitCode ?? null,
994
1029
  durationMs: payload.durationMs ?? null,
@@ -1194,9 +1229,18 @@ class SessionManager {
1194
1229
  }
1195
1230
  appendEvent(sessionId, input) {
1196
1231
  const event = this.options.eventStore.append(sessionId, input);
1232
+ const runtime = this.runners.get(sessionId);
1233
+ if (runtime) {
1234
+ runtime.transientSeqCursor = Math.max(runtime.transientSeqCursor, Number(event.seq || 0));
1235
+ }
1197
1236
  this.touchSession(sessionId);
1198
1237
  return event;
1199
1238
  }
1239
+ publishTransientEvent(sessionId, runtime, input) {
1240
+ runtime.transientSeqCursor =
1241
+ Math.round((runtime.transientSeqCursor + TRANSIENT_SEQ_STEP) * 100000) / 100000;
1242
+ return this.options.eventStore.publishTransient(sessionId, input, runtime.transientSeqCursor);
1243
+ }
1200
1244
  touchSession(sessionId) {
1201
1245
  this.options.db
1202
1246
  .prepare(`
@@ -1368,6 +1412,35 @@ class SessionManager {
1368
1412
  }
1369
1413
  return null;
1370
1414
  }
1415
+ buildApprovalRetryRuntimePrompt(approval) {
1416
+ const commandText = this.extractApprovalCommand(approval.method, approval.params);
1417
+ const reason = typeof approval.params.reason === "string" && approval.params.reason.trim()
1418
+ ? approval.params.reason.trim()
1419
+ : "";
1420
+ if (commandText) {
1421
+ return [
1422
+ "Re-run the exact operation that previously requested approval.",
1423
+ "Do not do extra exploration.",
1424
+ "As soon as the approval prompt appears again, stop and wait for the user decision.",
1425
+ "",
1426
+ commandText,
1427
+ ].join("\n");
1428
+ }
1429
+ if (reason) {
1430
+ return [
1431
+ "Re-run the exact step that previously requested approval.",
1432
+ "Do not do extra exploration.",
1433
+ "As soon as the approval prompt appears again, stop and wait for the user decision.",
1434
+ "",
1435
+ `Original approval reason: ${reason}`,
1436
+ ].join("\n");
1437
+ }
1438
+ return [
1439
+ "Re-run the exact step that previously requested approval.",
1440
+ "Do not do extra exploration.",
1441
+ "As soon as the approval prompt appears again, stop and wait for the user decision.",
1442
+ ].join("\n");
1443
+ }
1371
1444
  describeApprovalTitle(method) {
1372
1445
  if (method === "item/commandExecution/requestApproval" || method === "execCommandApproval") {
1373
1446
  return "Command execution requires approval";