remcodex 0.1.0-beta.8 → 0.1.0-beta.9
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/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 +26 -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/session-timeline-reducer.js +66 -13
|
@@ -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 ||
|
|
@@ -346,6 +348,7 @@ class SessionManager {
|
|
|
346
348
|
const runtime = {
|
|
347
349
|
runner,
|
|
348
350
|
stopRequested: false,
|
|
351
|
+
transientSeqCursor: this.options.eventStore.latestSeq(sessionId),
|
|
349
352
|
turnId,
|
|
350
353
|
appTurnId: null,
|
|
351
354
|
turnStarted: false,
|
|
@@ -945,6 +948,8 @@ class SessionManager {
|
|
|
945
948
|
cwd: payload.cwd || null,
|
|
946
949
|
stdout: "",
|
|
947
950
|
stderr: "",
|
|
951
|
+
stdoutTruncated: false,
|
|
952
|
+
stderrTruncated: false,
|
|
948
953
|
started: true,
|
|
949
954
|
completed: false,
|
|
950
955
|
});
|
|
@@ -971,14 +976,15 @@ class SessionManager {
|
|
|
971
976
|
if (!current) {
|
|
972
977
|
return;
|
|
973
978
|
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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;
|
|
979
985
|
}
|
|
980
986
|
runtime.activeCommandCallId = callId;
|
|
981
|
-
this.
|
|
987
|
+
this.publishTransientEvent(sessionId, runtime, {
|
|
982
988
|
type: "command.output.delta",
|
|
983
989
|
turnId: runtime.turnId,
|
|
984
990
|
messageId: null,
|
|
@@ -1013,6 +1019,11 @@ class SessionManager {
|
|
|
1013
1019
|
payload: {
|
|
1014
1020
|
command: payload.command || current.command,
|
|
1015
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,
|
|
1016
1027
|
status: payload.status || (payload.exitCode === 0 ? "completed" : "failed"),
|
|
1017
1028
|
exitCode: payload.exitCode ?? null,
|
|
1018
1029
|
durationMs: payload.durationMs ?? null,
|
|
@@ -1218,9 +1229,18 @@ class SessionManager {
|
|
|
1218
1229
|
}
|
|
1219
1230
|
appendEvent(sessionId, input) {
|
|
1220
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
|
+
}
|
|
1221
1236
|
this.touchSession(sessionId);
|
|
1222
1237
|
return event;
|
|
1223
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
|
+
}
|
|
1224
1244
|
touchSession(sessionId) {
|
|
1225
1245
|
this.options.db
|
|
1226
1246
|
.prepare(`
|
|
@@ -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
|
@@ -8,11 +8,35 @@ const TURN_STATUS_PRIORITY = {
|
|
|
8
8
|
failed: 3,
|
|
9
9
|
aborted: 4,
|
|
10
10
|
};
|
|
11
|
+
const MAX_COMMAND_OUTPUT_CHARS = 80 * 1024;
|
|
12
|
+
const MAX_PATCH_OUTPUT_CHARS = 48 * 1024;
|
|
13
|
+
const OUTPUT_TRUNCATION_NOTICE = "\n\n[output truncated]\n";
|
|
11
14
|
|
|
12
15
|
function createItemIndex() {
|
|
13
16
|
return new Map();
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
function clampOutputText(text, maxChars = MAX_COMMAND_OUTPUT_CHARS) {
|
|
20
|
+
const safeText = String(text || "");
|
|
21
|
+
if (!safeText) {
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
if (safeText.endsWith(OUTPUT_TRUNCATION_NOTICE)) {
|
|
25
|
+
return safeText;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const contentLimit = Math.max(0, maxChars - OUTPUT_TRUNCATION_NOTICE.length);
|
|
29
|
+
if (safeText.length <= contentLimit) {
|
|
30
|
+
return safeText;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return `${safeText.slice(0, contentLimit)}${OUTPUT_TRUNCATION_NOTICE}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function appendClampedOutput(currentText, textDelta, maxChars = MAX_COMMAND_OUTPUT_CHARS) {
|
|
37
|
+
return clampOutputText(`${String(currentText || "")}${String(textDelta || "")}`, maxChars);
|
|
38
|
+
}
|
|
39
|
+
|
|
16
40
|
function nextTurnFallbackId(event) {
|
|
17
41
|
return event?.turnId || `turn:${event?.id || crypto.randomUUID?.() || Date.now()}`;
|
|
18
42
|
}
|
|
@@ -526,10 +550,18 @@ export function reduceTimeline(state, event) {
|
|
|
526
550
|
const nextStdout =
|
|
527
551
|
event.payload?.stream === "stderr"
|
|
528
552
|
? currentCommand?.stdout || ""
|
|
529
|
-
:
|
|
553
|
+
: appendClampedOutput(
|
|
554
|
+
currentCommand?.stdout,
|
|
555
|
+
event.payload?.textDelta,
|
|
556
|
+
MAX_COMMAND_OUTPUT_CHARS,
|
|
557
|
+
);
|
|
530
558
|
const nextStderr =
|
|
531
559
|
event.payload?.stream === "stderr"
|
|
532
|
-
?
|
|
560
|
+
? appendClampedOutput(
|
|
561
|
+
currentCommand?.stderr,
|
|
562
|
+
event.payload?.textDelta,
|
|
563
|
+
MAX_COMMAND_OUTPUT_CHARS,
|
|
564
|
+
)
|
|
533
565
|
: currentCommand?.stderr || "";
|
|
534
566
|
upsertCommand(state, event, turnId, {
|
|
535
567
|
status: currentCommand?.status === "awaiting_approval" ? "awaiting_approval" : "running",
|
|
@@ -552,14 +584,22 @@ export function reduceTimeline(state, event) {
|
|
|
552
584
|
status: completedStatus,
|
|
553
585
|
command: event.payload?.command || state.commandsByCallId[event.callId]?.command || "",
|
|
554
586
|
cwd: event.payload?.cwd || state.commandsByCallId[event.callId]?.cwd || null,
|
|
555
|
-
stdout:
|
|
556
|
-
|
|
557
|
-
|
|
587
|
+
stdout: clampOutputText(
|
|
588
|
+
event.payload?.stdout || state.commandsByCallId[event.callId]?.stdout || "",
|
|
589
|
+
MAX_COMMAND_OUTPUT_CHARS,
|
|
590
|
+
),
|
|
591
|
+
stderr: clampOutputText(
|
|
592
|
+
event.payload?.stderr || state.commandsByCallId[event.callId]?.stderr || "",
|
|
593
|
+
MAX_COMMAND_OUTPUT_CHARS,
|
|
594
|
+
),
|
|
595
|
+
output: clampOutputText(
|
|
558
596
|
event.payload?.aggregatedOutput ||
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
597
|
+
event.payload?.formattedOutput ||
|
|
598
|
+
event.payload?.output ||
|
|
599
|
+
state.commandsByCallId[event.callId]?.output ||
|
|
600
|
+
"",
|
|
601
|
+
MAX_COMMAND_OUTPUT_CHARS,
|
|
602
|
+
),
|
|
563
603
|
exitCode:
|
|
564
604
|
event.payload?.exitCode ?? state.commandsByCallId[event.callId]?.exitCode ?? null,
|
|
565
605
|
duration: event.payload?.duration || state.commandsByCallId[event.callId]?.duration || null,
|
|
@@ -583,7 +623,11 @@ export function reduceTimeline(state, event) {
|
|
|
583
623
|
const currentPatch = state.patchesByCallId[patchId];
|
|
584
624
|
upsertPatch(state, event, turnId, {
|
|
585
625
|
status: currentPatch?.status || "running",
|
|
586
|
-
output:
|
|
626
|
+
output: appendClampedOutput(
|
|
627
|
+
currentPatch?.output,
|
|
628
|
+
event.payload?.textDelta,
|
|
629
|
+
MAX_PATCH_OUTPUT_CHARS,
|
|
630
|
+
),
|
|
587
631
|
outputStatus: "streaming",
|
|
588
632
|
});
|
|
589
633
|
setTurnStatus(turn, "running");
|
|
@@ -598,9 +642,18 @@ export function reduceTimeline(state, event) {
|
|
|
598
642
|
status: patchStatus,
|
|
599
643
|
patchText:
|
|
600
644
|
event.payload?.patchText || state.patchesByCallId[event.callId]?.patchText || "",
|
|
601
|
-
output:
|
|
602
|
-
|
|
603
|
-
|
|
645
|
+
output: clampOutputText(
|
|
646
|
+
event.payload?.output || state.patchesByCallId[event.callId]?.output || "",
|
|
647
|
+
MAX_PATCH_OUTPUT_CHARS,
|
|
648
|
+
),
|
|
649
|
+
stdout: clampOutputText(
|
|
650
|
+
event.payload?.stdout || state.patchesByCallId[event.callId]?.stdout || "",
|
|
651
|
+
MAX_PATCH_OUTPUT_CHARS,
|
|
652
|
+
),
|
|
653
|
+
stderr: clampOutputText(
|
|
654
|
+
event.payload?.stderr || state.patchesByCallId[event.callId]?.stderr || "",
|
|
655
|
+
MAX_PATCH_OUTPUT_CHARS,
|
|
656
|
+
),
|
|
604
657
|
changes: event.payload?.changes || state.patchesByCallId[event.callId]?.changes || {},
|
|
605
658
|
success:
|
|
606
659
|
event.payload?.success ?? state.patchesByCallId[event.callId]?.success ?? null,
|