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 +30 -39
- package/dist/server/src/app.js +3 -26
- package/dist/server/src/cli.js +65 -7
- package/dist/server/src/controllers/session.controller.js +12 -0
- package/dist/server/src/db/migrations.js +30 -1
- package/dist/server/src/db/schema.sql +48 -0
- package/dist/server/src/services/codex-rollout-sync.js +5 -16
- package/dist/server/src/services/event-store.js +28 -5
- package/dist/server/src/services/session-manager.js +79 -6
- package/dist/server/src/services/session-timeline-service.js +7 -169
- package/dist/server/src/utils/output-limits.js +73 -0
- package/dist/server/src/utils/runtime-paths.js +31 -0
- package/docs/assets/approval-flow.png +0 -0
- package/docs/assets/hero-desktop.png +0 -0
- package/docs/assets/imported-session.png +0 -0
- package/docs/assets/mobile-session.png +0 -0
- package/package.json +13 -5
- package/scripts/check-node-version.js +14 -0
- package/web/api.js +7 -0
- package/web/app.js +241 -37
- package/web/i18n/locales/en.js +3 -2
- package/web/i18n/locales/zh-CN.js +3 -2
- package/web/session-timeline-reducer.js +66 -13
- package/web/styles.css +22 -0
- package/LICENSE +0 -21
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-
|
|
184
|
-
node dist/server/src/cli.js doctor --db ~/.remcodex/remcodex-
|
|
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
|
package/dist/server/src/app.js
CHANGED
|
@@ -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;
|
package/dist/server/src/cli.js
CHANGED
|
@@ -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
|
|
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,
|
|
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,
|
|
131
|
-
const databasePath = flags.databasePath ?? process.env.DATABASE_PATH ?? (0,
|
|
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
|
|
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
|
|
246
|
-
const
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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.
|
|
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";
|