remcodex 0.1.0-beta.1 → 0.1.0-beta.10

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.
@@ -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 ||
@@ -288,6 +290,30 @@ class SessionManager {
288
290
  runtime.runner.stop();
289
291
  return { accepted: true };
290
292
  }
293
+ retryApprovalRequest(sessionId, requestId, codexLaunch) {
294
+ const session = this.getSessionOrThrow(sessionId);
295
+ const project = this.options.projectManager.getProject(session.project_id);
296
+ if (!project) {
297
+ throw new errors_1.AppError(404, "Project not found for session.");
298
+ }
299
+ const currentRunner = this.runners.get(sessionId);
300
+ const busyStatuses = ["starting", "running", "stopping"];
301
+ if (currentRunner?.runner.isAlive() && busyStatuses.includes(session.status)) {
302
+ throw new errors_1.AppError(409, "Session already has an active task.");
303
+ }
304
+ const pending = this.pendingApprovals.get(sessionId)?.get(requestId) ??
305
+ this.restorePendingApprovalFromEvents(sessionId, requestId);
306
+ if (!pending) {
307
+ throw new errors_1.AppError(404, "Approval request not found.");
308
+ }
309
+ const turnId = (0, ids_1.createId)("turn");
310
+ const runtimePrompt = normalizeDemoPrompt(project.path, this.buildApprovalRetryRuntimePrompt(pending));
311
+ this.startRunner(sessionId, project.path, runtimePrompt, turnId, this.resolveResumeThreadId(session), codexLaunch);
312
+ return {
313
+ accepted: true,
314
+ turnId,
315
+ };
316
+ }
291
317
  resolveApproval(sessionId, requestId, decision) {
292
318
  const runtime = this.runners.get(sessionId);
293
319
  if (!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";
@@ -1,181 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SessionTimelineService = void 0;
4
- const DEFAULT_TIMELINE_LIMIT = 200;
5
- const MAX_TIMELINE_LIMIT = 400;
6
- function clampLimit(limit) {
7
- const numeric = Number(limit || DEFAULT_TIMELINE_LIMIT);
8
- if (!Number.isFinite(numeric) || numeric <= 0) {
9
- return DEFAULT_TIMELINE_LIMIT;
10
- }
11
- return Math.max(1, Math.min(Math.trunc(numeric), MAX_TIMELINE_LIMIT));
12
- }
13
- function normalizeCursor(value) {
14
- const numeric = Number(value || 0);
15
- if (!Number.isFinite(numeric) || numeric <= 0) {
16
- return 0;
17
- }
18
- return Math.trunc(numeric);
19
- }
20
- function cloneEvent(event) {
21
- return {
22
- ...event,
23
- payload: event.payload && typeof event.payload === "object"
24
- ? { ...event.payload }
25
- : event.payload,
26
- };
27
- }
28
- function appendTextDelta(currentValue, nextValue) {
29
- if (!nextValue) {
30
- return currentValue || "";
31
- }
32
- return `${currentValue || ""}${nextValue}`;
33
- }
34
- function compareEvents(left, right) {
35
- if (left.seq !== right.seq) {
36
- return left.seq - right.seq;
37
- }
38
- return String(left.id || "").localeCompare(String(right.id || ""));
39
- }
40
- function upsertTimelineEvent(items, indexById, nextEvent) {
41
- const existing = indexById.get(nextEvent.id);
42
- if (existing) {
43
- Object.assign(existing, nextEvent);
44
- return existing;
45
- }
46
- const cloned = cloneEvent(nextEvent);
47
- items.push(cloned);
48
- indexById.set(cloned.id, cloned);
49
- return cloned;
50
- }
51
- function timelineAssistantDeltaId(event) {
52
- return `timeline:assistant:delta:${event.messageId || event.id}`;
53
- }
54
- function timelineReasoningDeltaId(event) {
55
- return `timeline:reasoning:delta:${event.messageId || event.id}`;
56
- }
57
- function timelineCommandOutputId(event) {
58
- return `timeline:command:output:${event.callId || event.id}:${event.stream || "stdout"}`;
59
- }
60
- function timelinePatchOutputId(event) {
61
- return `timeline:patch:output:${event.callId || event.id}`;
62
- }
63
- function aggregateSemanticTimeline(rawEvents) {
64
- const items = [];
65
- const indexById = new Map();
66
- rawEvents.forEach((event) => {
67
- switch (event.type) {
68
- case "message.assistant.delta": {
69
- const syntheticId = timelineAssistantDeltaId(event);
70
- const existing = indexById.get(syntheticId);
71
- upsertTimelineEvent(items, indexById, {
72
- ...cloneEvent(event),
73
- id: syntheticId,
74
- payload: {
75
- ...(event.payload || {}),
76
- textDelta: appendTextDelta(existing?.payload?.textDelta || "", String(event.payload?.textDelta || "")),
77
- },
78
- });
79
- break;
80
- }
81
- case "reasoning.delta": {
82
- const syntheticId = timelineReasoningDeltaId(event);
83
- const existing = indexById.get(syntheticId);
84
- const nextText = appendTextDelta(existing?.payload?.textDelta || "", String(event.payload?.textDelta || ""));
85
- upsertTimelineEvent(items, indexById, {
86
- ...cloneEvent(event),
87
- id: syntheticId,
88
- payload: {
89
- ...(event.payload || {}),
90
- textDelta: nextText,
91
- summary: event.payload?.summary ||
92
- existing?.payload?.summary ||
93
- nextText ||
94
- null,
95
- },
96
- });
97
- break;
98
- }
99
- case "command.output.delta": {
100
- const syntheticId = timelineCommandOutputId(event);
101
- const existing = indexById.get(syntheticId);
102
- upsertTimelineEvent(items, indexById, {
103
- ...cloneEvent(event),
104
- id: syntheticId,
105
- payload: {
106
- ...(event.payload || {}),
107
- textDelta: appendTextDelta(existing?.payload?.textDelta || "", String(event.payload?.textDelta || "")),
108
- stream: event.payload?.stream || event.stream || "stdout",
109
- },
110
- });
111
- break;
112
- }
113
- case "patch.output.delta": {
114
- const syntheticId = timelinePatchOutputId(event);
115
- const existing = indexById.get(syntheticId);
116
- upsertTimelineEvent(items, indexById, {
117
- ...cloneEvent(event),
118
- id: syntheticId,
119
- payload: {
120
- ...(event.payload || {}),
121
- textDelta: appendTextDelta(existing?.payload?.textDelta || "", String(event.payload?.textDelta || "")),
122
- },
123
- });
124
- break;
125
- }
126
- default:
127
- upsertTimelineEvent(items, indexById, event);
128
- break;
129
- }
130
- });
131
- return items.sort(compareEvents);
132
- }
133
- function paginateTimelineItems(items, options, lastSeq) {
134
- const limit = clampLimit(options.limit);
135
- const after = normalizeCursor(options.after);
136
- const before = normalizeCursor(options.before);
137
- if (before > 0) {
138
- const matches = items.filter((item) => item.seq < before);
139
- const hasMoreBefore = matches.length > limit;
140
- const pageItems = matches.slice(Math.max(0, matches.length - limit));
141
- return {
142
- items: pageItems,
143
- nextCursor: pageItems.at(-1)?.seq || after,
144
- beforeCursor: pageItems[0]?.seq || before,
145
- hasMoreBefore,
146
- lastSeq,
147
- };
148
- }
149
- if (after > 0) {
150
- const pageItems = items.filter((item) => item.seq > after).slice(0, limit);
151
- return {
152
- items: pageItems,
153
- nextCursor: pageItems.at(-1)?.seq || after,
154
- beforeCursor: pageItems[0]?.seq || 0,
155
- hasMoreBefore: pageItems.length > 0 ? pageItems[0].seq > 1 : items.length > 0,
156
- lastSeq,
157
- };
158
- }
159
- const hasMoreBefore = items.length > limit;
160
- const pageItems = items.slice(Math.max(0, items.length - limit));
161
- return {
162
- items: pageItems,
163
- nextCursor: pageItems.at(-1)?.seq || 0,
164
- beforeCursor: pageItems[0]?.seq || 0,
165
- hasMoreBefore,
166
- lastSeq,
167
- };
168
- }
169
4
  class SessionTimelineService {
170
5
  eventStore;
171
6
  constructor(eventStore) {
172
7
  this.eventStore = eventStore;
173
8
  }
174
9
  list(sessionId, options = {}) {
175
- const rawEvents = this.eventStore.listAll(sessionId);
176
- const lastSeq = rawEvents.at(-1)?.seq || 0;
177
- const aggregatedItems = aggregateSemanticTimeline(rawEvents);
178
- return paginateTimelineItems(aggregatedItems, options, lastSeq);
10
+ const page = this.eventStore.list(sessionId, options);
11
+ return {
12
+ ...page,
13
+ // The initial detail load only needs the latest observed seq so resume sync
14
+ // can continue from the newest page we fetched.
15
+ lastSeq: page.nextCursor || Math.max(0, Number(options.after || 0)),
16
+ };
179
17
  }
180
18
  }
181
19
  exports.SessionTimelineService = SessionTimelineService;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.COMMAND_STREAM_TRUNCATION_NOTICE = exports.MAX_PERSISTED_COMMAND_STREAM_CHARS = void 0;
4
+ exports.appendCappedText = appendCappedText;
5
+ exports.capTextValue = capTextValue;
6
+ exports.MAX_PERSISTED_COMMAND_STREAM_CHARS = 80 * 1024;
7
+ exports.COMMAND_STREAM_TRUNCATION_NOTICE = "\n\n[command output truncated]\n";
8
+ function normalizeMaxChars(maxChars, notice) {
9
+ const numeric = Number(maxChars || exports.MAX_PERSISTED_COMMAND_STREAM_CHARS);
10
+ if (!Number.isFinite(numeric) || numeric <= notice.length + 1) {
11
+ return exports.MAX_PERSISTED_COMMAND_STREAM_CHARS;
12
+ }
13
+ return Math.trunc(numeric);
14
+ }
15
+ function appendCappedText(currentText, nextDelta, options = {}) {
16
+ const notice = String(options.notice || exports.COMMAND_STREAM_TRUNCATION_NOTICE);
17
+ const safeCurrent = String(currentText || "");
18
+ const safeDelta = String(nextDelta || "");
19
+ if (!safeDelta) {
20
+ return {
21
+ nextText: safeCurrent,
22
+ appendedText: "",
23
+ truncated: false,
24
+ };
25
+ }
26
+ const maxChars = normalizeMaxChars(options.maxChars, notice);
27
+ const contentLimit = Math.max(0, maxChars - notice.length);
28
+ if (safeCurrent.endsWith(notice) || safeCurrent.length >= maxChars) {
29
+ return {
30
+ nextText: safeCurrent.length > maxChars ? safeCurrent.slice(0, maxChars) : safeCurrent,
31
+ appendedText: "",
32
+ truncated: true,
33
+ };
34
+ }
35
+ if (safeCurrent.length >= contentLimit) {
36
+ return {
37
+ nextText: `${safeCurrent.slice(0, contentLimit)}${notice}`,
38
+ appendedText: notice,
39
+ truncated: true,
40
+ };
41
+ }
42
+ if (safeCurrent.length + safeDelta.length <= contentLimit) {
43
+ return {
44
+ nextText: safeCurrent + safeDelta,
45
+ appendedText: safeDelta,
46
+ truncated: false,
47
+ };
48
+ }
49
+ const available = Math.max(0, contentLimit - safeCurrent.length);
50
+ const preserved = safeDelta.slice(0, available);
51
+ const appendedText = `${preserved}${notice}`;
52
+ return {
53
+ nextText: `${safeCurrent}${appendedText}`,
54
+ appendedText,
55
+ truncated: true,
56
+ };
57
+ }
58
+ function capTextValue(text, options = {}) {
59
+ const notice = String(options.notice || exports.COMMAND_STREAM_TRUNCATION_NOTICE);
60
+ const safeText = String(text || "");
61
+ const maxChars = normalizeMaxChars(options.maxChars, notice);
62
+ const contentLimit = Math.max(0, maxChars - notice.length);
63
+ if (safeText.length <= contentLimit) {
64
+ return {
65
+ text: safeText,
66
+ truncated: false,
67
+ };
68
+ }
69
+ return {
70
+ text: `${safeText.slice(0, contentLimit)}${notice}`,
71
+ truncated: true,
72
+ };
73
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remcodex",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0-beta.10",
4
4
  "description": "Control Codex from anywhere. Even on your phone.",
5
5
  "license": "MIT",
6
6
  "bin": {
package/web/api.js CHANGED
@@ -138,6 +138,13 @@ export function resolveSessionApproval(sessionId, requestId, decision) {
138
138
  });
139
139
  }
140
140
 
141
+ export function retrySessionApproval(sessionId, requestId, payload = {}) {
142
+ return request(`/api/sessions/${sessionId}/approvals/${encodeURIComponent(requestId)}/retry`, {
143
+ method: "POST",
144
+ body: JSON.stringify(payload),
145
+ });
146
+ }
147
+
141
148
  export function getHealth() {
142
149
  return request("/health");
143
150
  }