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.
- package/README.md +225 -240
- package/dist/server/src/app.js +3 -1
- package/dist/server/src/cli.js +4 -3
- package/dist/server/src/controllers/session.controller.js +12 -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/package.json +1 -1
- 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
|
@@ -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 ||
|
|
@@ -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
|
-
|
|
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";
|
|
@@ -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
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
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
|
}
|