@makerbi/remodex 2.0.1 → 2.3.1
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/package.json +2 -2
- package/src/account-status.js +5 -4
- package/src/bridge.js +1813 -670
- package/src/codex-desktop-refresher.js +35 -7
- package/src/cursor-acp-client.js +242 -0
- package/src/cursor-models.js +134 -0
- package/src/cursor-provider.js +1197 -0
- package/src/desktop-ipc-action-follower.js +2331 -964
- package/src/desktop-ipc-conversation-adapter.js +1132 -0
- package/src/desktop-ipc-conversation-projector.js +1169 -0
- package/src/desktop-ipc-live-owner.js +1790 -0
- package/src/desktop-ipc-owner-transport.js +750 -0
- package/src/desktop-ipc-shared.js +473 -0
- package/src/desktop-ipc-state-patches.js +218 -0
- package/src/opencode-models.js +108 -0
- package/src/opencode-provider.js +1151 -0
- package/src/project-handler.js +50 -7
- package/src/project-registry.js +466 -0
- package/src/push-notification-tracker.js +4 -4
- package/src/rollout-live-mirror.js +930 -218
- package/src/rollout-turn-semantics.js +20 -0
- package/src/runtime-provider-models.js +164 -0
- package/src/runtime-provider-router.js +365 -0
- package/src/scripts/codex-refresh.applescript +26 -15
- package/src/secure-transport.js +204 -9
- package/src/session-jsonl-history.js +429 -39
- package/src/thread-context-handler.js +8 -6
- package/src/thread-runtime-settings-store.js +247 -0
- package/src/voice-audio.js +344 -0
- package/src/voice-handler.js +363 -173
|
@@ -13,15 +13,38 @@ const {
|
|
|
13
13
|
} = require("./rollout-watch");
|
|
14
14
|
const { resolveCodexGeneratedImagesRoot } = require("./codex-home");
|
|
15
15
|
const { buildApplyPatchFileChangeItem } = require("./apply-patch-changes");
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
const {
|
|
17
|
+
TERMINAL_TASK_EVENT_TYPES,
|
|
18
|
+
terminalEventClosesTrackedTurn,
|
|
19
|
+
} = require("./rollout-turn-semantics");
|
|
20
|
+
const {
|
|
21
|
+
hasVisiblePlanUpdate,
|
|
22
|
+
buildRemodexSourceItemKey,
|
|
23
|
+
visibleUserPromptFromInputEntries,
|
|
24
|
+
visibleUserPromptText,
|
|
25
|
+
responseItemMessageText,
|
|
26
|
+
} = require("./desktop-ipc-shared");
|
|
27
|
+
|
|
28
|
+
// The phone batches each poll tick's notifications and settles its timeline
|
|
29
|
+
// ~80ms after the batch ends (CodexService liveMirrorBatchFlushNanoseconds).
|
|
30
|
+
// Keep this interval comfortably above that settle window, or lower both
|
|
31
|
+
// together, so consecutive ticks never merge into one batch.
|
|
32
|
+
const DEFAULT_POLL_INTERVAL_MS = 250;
|
|
18
33
|
const DEFAULT_LOOKUP_TIMEOUT_MS = 5_000;
|
|
19
34
|
const DEFAULT_IDLE_TIMEOUT_MS = 60_000;
|
|
20
35
|
const DEFAULT_ACTIVITY_HEARTBEAT_MS = 5_000;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
36
|
+
// Bootstrap replay must not resurrect runs whose rollout stopped growing long ago
|
|
37
|
+
// (aborted/killed desktop runs never write task_complete).
|
|
38
|
+
const DEFAULT_STALE_ACTIVE_RUN_MAX_AGE_MS = 10 * 60_000;
|
|
39
|
+
const DEFAULT_SYNTHETIC_TERMINAL_GRACE_MS = 1_000;
|
|
40
|
+
// Rollouts can be tens of megabytes. They are a live-delta fallback, not the
|
|
41
|
+
// durable conversation history, so bootstrap must never synchronously parse
|
|
42
|
+
// the entire file just to discover an active turn.
|
|
43
|
+
const DEFAULT_BOOTSTRAP_METADATA_HEAD_BYTES = 256 * 1024;
|
|
44
|
+
const DEFAULT_BOOTSTRAP_TAIL_BYTES = 4 * 1024 * 1024;
|
|
45
|
+
// Keep a hard bound, but match the JSONL history reader's 64MB recovery window
|
|
46
|
+
// so a long active turn does not degrade permanently to no live context.
|
|
47
|
+
const DEFAULT_BOOTSTRAP_MAX_BYTES = 64 * 1024 * 1024;
|
|
25
48
|
const DESKTOP_RESUME_METHODS = new Set(["thread/read", "thread/resume"]);
|
|
26
49
|
|
|
27
50
|
// Observes desktop-authored rollout files and replays the currently active run as
|
|
@@ -33,19 +56,21 @@ function createRolloutLiveMirrorController({
|
|
|
33
56
|
now = () => Date.now(),
|
|
34
57
|
setIntervalFn = setInterval,
|
|
35
58
|
clearIntervalFn = clearInterval,
|
|
36
|
-
setTimeoutFn = setTimeout,
|
|
37
|
-
clearTimeoutFn = clearTimeout,
|
|
38
59
|
pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
|
|
39
60
|
lookupTimeoutMs = DEFAULT_LOOKUP_TIMEOUT_MS,
|
|
40
61
|
idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS,
|
|
41
62
|
activityHeartbeatMs = DEFAULT_ACTIVITY_HEARTBEAT_MS,
|
|
42
|
-
|
|
63
|
+
staleActiveRunMaxAgeMs = DEFAULT_STALE_ACTIVE_RUN_MAX_AGE_MS,
|
|
64
|
+
syntheticTerminalGraceMs = DEFAULT_SYNTHETIC_TERMINAL_GRACE_MS,
|
|
65
|
+
// Rollout tailing is the fallback mirror; when another live source already
|
|
66
|
+
// streams a thread (IPC follower state or bridge-owned app-server stream),
|
|
67
|
+
// emitting from the file too would double every row on the phone.
|
|
68
|
+
shouldSuppressThread = null,
|
|
43
69
|
} = {}) {
|
|
44
70
|
const mirrorsByThreadId = new Map();
|
|
45
|
-
const bootstrapReplayCache = new Map();
|
|
46
71
|
|
|
47
|
-
function observeInbound(rawMessage) {
|
|
48
|
-
const request = safeParseJSON(rawMessage);
|
|
72
|
+
function observeInbound(rawMessage, parsedMessage = null) {
|
|
73
|
+
const request = parsedMessage ?? safeParseJSON(rawMessage);
|
|
49
74
|
const method = readString(request?.method);
|
|
50
75
|
if (!DESKTOP_RESUME_METHODS.has(method)) {
|
|
51
76
|
return;
|
|
@@ -63,22 +88,34 @@ function createRolloutLiveMirrorController({
|
|
|
63
88
|
}
|
|
64
89
|
|
|
65
90
|
let mirror;
|
|
91
|
+
let suppressionContext = {};
|
|
92
|
+
const isThreadSuppressed = () => Boolean(shouldSuppressThread?.(threadId, suppressionContext));
|
|
66
93
|
mirror = createThreadRolloutLiveMirror({
|
|
67
94
|
threadId,
|
|
68
|
-
sendApplicationResponse
|
|
95
|
+
sendApplicationResponse: typeof shouldSuppressThread === "function"
|
|
96
|
+
? (rawNotification) => {
|
|
97
|
+
if (!isThreadSuppressed()) {
|
|
98
|
+
sendApplicationResponse(rawNotification);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
: sendApplicationResponse,
|
|
102
|
+
isSuppressed: typeof shouldSuppressThread === "function"
|
|
103
|
+
? (context) => {
|
|
104
|
+
suppressionContext = context || {};
|
|
105
|
+
return isThreadSuppressed();
|
|
106
|
+
}
|
|
107
|
+
: () => false,
|
|
69
108
|
logPrefix,
|
|
70
109
|
fsModule,
|
|
71
110
|
now,
|
|
72
111
|
setIntervalFn,
|
|
73
112
|
clearIntervalFn,
|
|
74
|
-
setTimeoutFn,
|
|
75
|
-
clearTimeoutFn,
|
|
76
113
|
pollIntervalMs,
|
|
77
114
|
lookupTimeoutMs,
|
|
78
115
|
idleTimeoutMs,
|
|
79
116
|
activityHeartbeatMs,
|
|
80
|
-
|
|
81
|
-
|
|
117
|
+
staleActiveRunMaxAgeMs,
|
|
118
|
+
syntheticTerminalGraceMs,
|
|
82
119
|
onStop() {
|
|
83
120
|
if (mirrorsByThreadId.get(threadId) === mirror) {
|
|
84
121
|
mirrorsByThreadId.delete(threadId);
|
|
@@ -106,32 +143,35 @@ function createRolloutLiveMirrorController({
|
|
|
106
143
|
function createThreadRolloutLiveMirror({
|
|
107
144
|
threadId,
|
|
108
145
|
sendApplicationResponse,
|
|
146
|
+
isSuppressed = () => false,
|
|
109
147
|
logPrefix,
|
|
110
148
|
fsModule,
|
|
111
149
|
now,
|
|
112
150
|
setIntervalFn,
|
|
113
151
|
clearIntervalFn,
|
|
114
|
-
setTimeoutFn,
|
|
115
|
-
clearTimeoutFn,
|
|
116
152
|
pollIntervalMs,
|
|
117
153
|
lookupTimeoutMs,
|
|
118
154
|
idleTimeoutMs,
|
|
119
155
|
activityHeartbeatMs,
|
|
120
|
-
|
|
121
|
-
|
|
156
|
+
staleActiveRunMaxAgeMs,
|
|
157
|
+
syntheticTerminalGraceMs,
|
|
122
158
|
onStop = () => {},
|
|
123
159
|
}) {
|
|
124
160
|
const startedAt = now();
|
|
125
161
|
const state = createMirrorState(threadId);
|
|
126
|
-
const bootstrapReplayTimeouts = new Set();
|
|
127
162
|
|
|
128
163
|
let isStopped = false;
|
|
129
164
|
let rolloutPath = null;
|
|
130
165
|
let lastSize = 0;
|
|
131
166
|
let partialLine = "";
|
|
132
167
|
let lastActivityAt = startedAt;
|
|
168
|
+
// Rollout growth only: heartbeats deliberately never refresh this clock, so a
|
|
169
|
+
// desktop process that died mid-run (no terminal event, file frozen) cannot
|
|
170
|
+
// keep the mirror heartbeating "running" forever.
|
|
171
|
+
let lastGrowthAt = startedAt;
|
|
133
172
|
let lastHeartbeatAt = 0;
|
|
134
173
|
let didBootstrap = false;
|
|
174
|
+
let wasSuppressed = false;
|
|
135
175
|
|
|
136
176
|
const intervalId = setIntervalFn(tick, pollIntervalMs);
|
|
137
177
|
tick();
|
|
@@ -159,7 +199,22 @@ function createThreadRolloutLiveMirror({
|
|
|
159
199
|
}
|
|
160
200
|
}
|
|
161
201
|
|
|
162
|
-
const
|
|
202
|
+
const rolloutStat = fsModule.statSync(rolloutPath);
|
|
203
|
+
const fileSize = rolloutStat.size;
|
|
204
|
+
// While another live source streams this thread the tail keeps consuming
|
|
205
|
+
// rollout lines with its emissions muted. Compare per-thread activity so
|
|
206
|
+
// a quiet Desktop turn stays owned, while newer rollout growth can recover
|
|
207
|
+
// from a stale connected snapshot.
|
|
208
|
+
const suppressed = isSuppressed({
|
|
209
|
+
fallbackActivityAt: Number(rolloutStat.mtimeMs) || 0,
|
|
210
|
+
});
|
|
211
|
+
if (wasSuppressed && !suppressed && didBootstrap) {
|
|
212
|
+
lastSize = 0;
|
|
213
|
+
partialLine = "";
|
|
214
|
+
didBootstrap = false;
|
|
215
|
+
resetRunState(state);
|
|
216
|
+
}
|
|
217
|
+
wasSuppressed = suppressed;
|
|
163
218
|
if (!didBootstrap) {
|
|
164
219
|
didBootstrap = true;
|
|
165
220
|
bootstrapFromExistingRollout({
|
|
@@ -168,13 +223,12 @@ function createThreadRolloutLiveMirror({
|
|
|
168
223
|
state,
|
|
169
224
|
fsModule,
|
|
170
225
|
sendApplicationResponse,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
bootstrapReplayBatchIntervalMs,
|
|
174
|
-
pendingTimeouts: bootstrapReplayTimeouts,
|
|
226
|
+
nowMs: currentTime,
|
|
227
|
+
staleActiveRunMaxAgeMs,
|
|
175
228
|
});
|
|
176
229
|
lastSize = fileSize;
|
|
177
230
|
lastActivityAt = currentTime;
|
|
231
|
+
lastGrowthAt = currentTime;
|
|
178
232
|
lastHeartbeatAt = currentTime;
|
|
179
233
|
if (state.isDesktopOrigin === false) {
|
|
180
234
|
stop();
|
|
@@ -182,10 +236,28 @@ function createThreadRolloutLiveMirror({
|
|
|
182
236
|
return;
|
|
183
237
|
}
|
|
184
238
|
|
|
239
|
+
if (fileSize < lastSize) {
|
|
240
|
+
// Rollout files can be rewritten/truncated by desktop recovery. The
|
|
241
|
+
// rewritten contents are a different history, not live growth: reset the
|
|
242
|
+
// cursor and re-run the bootstrap path (tagged catch-up / terminal
|
|
243
|
+
// catch-up) instead of replaying the whole file as untagged live events.
|
|
244
|
+
lastSize = 0;
|
|
245
|
+
partialLine = "";
|
|
246
|
+
didBootstrap = false;
|
|
247
|
+
resetRunState(state);
|
|
248
|
+
lastGrowthAt = currentTime;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
185
252
|
if (fileSize > lastSize) {
|
|
253
|
+
// A capped bootstrap has no verified active-turn opener. Never append
|
|
254
|
+
// arbitrary deltas to that unknown state: wait for growth, then retry
|
|
255
|
+
// a bounded coherent bootstrap. A new task_started+prompt near EOF
|
|
256
|
+
// recovers immediately; an old huge run remains canonical-history only.
|
|
186
257
|
const chunk = readFileSlice(rolloutPath, lastSize, fileSize, fsModule);
|
|
187
258
|
lastSize = fileSize;
|
|
188
259
|
lastActivityAt = currentTime;
|
|
260
|
+
lastGrowthAt = currentTime;
|
|
189
261
|
lastHeartbeatAt = currentTime;
|
|
190
262
|
if (!chunk) {
|
|
191
263
|
return;
|
|
@@ -200,16 +272,51 @@ function createThreadRolloutLiveMirror({
|
|
|
200
272
|
searchStart = nlIndex + 1;
|
|
201
273
|
}
|
|
202
274
|
partialLine = searchStart < combined.length ? combined.substring(searchStart) : "";
|
|
203
|
-
|
|
275
|
+
if (state.awaitingCoherentBoundary) {
|
|
276
|
+
if (processAwaitingCoherentBoundary(lines, state, sendApplicationResponse, currentTime)) {
|
|
277
|
+
state.awaitingCoherentBoundary = false;
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
// Real growth proves the run is alive again; resume normal mirroring.
|
|
282
|
+
state.suppressLiveActivityUntilGrowth = false;
|
|
283
|
+
processRolloutLines(lines, state, sendApplicationResponse, { nowMs: currentTime });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const syntheticTerminalNotifications = finalizePendingSyntheticTerminalIfReady(
|
|
288
|
+
state,
|
|
289
|
+
currentTime,
|
|
290
|
+
syntheticTerminalGraceMs
|
|
291
|
+
);
|
|
292
|
+
if (syntheticTerminalNotifications.length > 0) {
|
|
293
|
+
for (const notification of syntheticTerminalNotifications) {
|
|
294
|
+
sendApplicationResponse(JSON.stringify(notification));
|
|
295
|
+
}
|
|
296
|
+
lastActivityAt = currentTime;
|
|
297
|
+
lastHeartbeatAt = currentTime;
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// A frozen rollout with a still-open turn means the desktop process died
|
|
302
|
+
// mid-run (crash / kill: no terminal event will ever arrive). Stop before
|
|
303
|
+
// heartbeating so the phone is not kept in "running" forever.
|
|
304
|
+
if (state.activeTurnId && currentTime - lastGrowthAt >= staleActiveRunMaxAgeMs) {
|
|
305
|
+
stop();
|
|
204
306
|
return;
|
|
205
307
|
}
|
|
206
308
|
|
|
207
309
|
if (
|
|
208
310
|
state.isDesktopOrigin !== false
|
|
209
311
|
&& state.activeTurnId
|
|
312
|
+
&& !state.suppressLiveActivityUntilGrowth
|
|
210
313
|
&& currentTime - lastHeartbeatAt >= activityHeartbeatMs
|
|
211
314
|
) {
|
|
212
315
|
lastHeartbeatAt = currentTime;
|
|
316
|
+
// Heartbeats keep the idle timeout from killing a quiet-but-alive run
|
|
317
|
+
// (long thinking stretches legitimately exceed the 60s idle window);
|
|
318
|
+
// the growth-stale guard above still bounds crashed runs.
|
|
319
|
+
lastActivityAt = currentTime;
|
|
213
320
|
sendApplicationResponse(JSON.stringify(createNotification("turn/activity", {
|
|
214
321
|
threadId: state.threadId,
|
|
215
322
|
turnId: state.activeTurnId,
|
|
@@ -235,12 +342,19 @@ function createThreadRolloutLiveMirror({
|
|
|
235
342
|
return;
|
|
236
343
|
}
|
|
237
344
|
|
|
345
|
+
// Mark stopped and clear the interval first: a throwing send during the
|
|
346
|
+
// final partial-line flush must never leak the poll interval.
|
|
238
347
|
isStopped = true;
|
|
239
348
|
clearIntervalFn(intervalId);
|
|
240
|
-
|
|
241
|
-
|
|
349
|
+
if (partialLine) {
|
|
350
|
+
const flushLine = partialLine;
|
|
351
|
+
partialLine = "";
|
|
352
|
+
try {
|
|
353
|
+
processRolloutLines([flushLine], state, sendApplicationResponse, { nowMs: now() });
|
|
354
|
+
} catch (error) {
|
|
355
|
+
console.warn(`${logPrefix} rollout live mirror final flush failed for ${threadId}: ${error.message}`);
|
|
356
|
+
}
|
|
242
357
|
}
|
|
243
|
-
bootstrapReplayTimeouts.clear();
|
|
244
358
|
onStop();
|
|
245
359
|
}
|
|
246
360
|
|
|
@@ -256,12 +370,55 @@ function bootstrapFromExistingRollout({
|
|
|
256
370
|
state,
|
|
257
371
|
fsModule,
|
|
258
372
|
sendApplicationResponse,
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
bootstrapReplayBatchIntervalMs = DEFAULT_BOOTSTRAP_REPLAY_BATCH_INTERVAL_MS,
|
|
262
|
-
pendingTimeouts,
|
|
373
|
+
nowMs = Date.now(),
|
|
374
|
+
staleActiveRunMaxAgeMs = DEFAULT_STALE_ACTIVE_RUN_MAX_AGE_MS,
|
|
263
375
|
}) {
|
|
264
|
-
|
|
376
|
+
// Read metadata independently from the tail. session_meta is written at the
|
|
377
|
+
// beginning, while the active run lives at the end. This keeps reopening a
|
|
378
|
+
// 30MB rollout bounded and avoids treating a partial tail as history.
|
|
379
|
+
const metadataContents = readFileSlice(
|
|
380
|
+
rolloutPath,
|
|
381
|
+
0,
|
|
382
|
+
Math.min(fileSize, DEFAULT_BOOTSTRAP_METADATA_HEAD_BYTES),
|
|
383
|
+
fsModule
|
|
384
|
+
);
|
|
385
|
+
for (const rawLine of metadataContents.split("\n")) {
|
|
386
|
+
const parsed = safeParseJSON(rawLine.trim());
|
|
387
|
+
if (parsed?.type === "session_meta") {
|
|
388
|
+
populateSessionMetaState(state, parsed.payload);
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (!isDesktopRolloutOrigin(state.sessionMeta)) {
|
|
393
|
+
state.isDesktopOrigin = false;
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
state.isDesktopOrigin = true;
|
|
397
|
+
|
|
398
|
+
const bootstrapWindow = readCoherentBootstrapWindow({
|
|
399
|
+
rolloutPath,
|
|
400
|
+
fileSize,
|
|
401
|
+
fsModule,
|
|
402
|
+
});
|
|
403
|
+
if (!bootstrapWindow) {
|
|
404
|
+
// The active run starts outside the bounded bootstrap window. Do not emit
|
|
405
|
+
// a plausible-looking tail: canonical history remains the baseline and
|
|
406
|
+
// this mirror will still consume future growth normally.
|
|
407
|
+
state.awaitingCoherentBoundary = true;
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const { tailStart, contents: bootstrapContents } = bootstrapWindow;
|
|
411
|
+
let initialContents = bootstrapContents;
|
|
412
|
+
if (!initialContents) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
// The first bytes may be the end of a JSON record. Drop that fragment rather
|
|
416
|
+
// than guessing, because an incomplete task_started record would lose the
|
|
417
|
+
// user opener and recreate the exact tail-only regression we are fixing.
|
|
418
|
+
if (tailStart > 0) {
|
|
419
|
+
const firstNewline = initialContents.indexOf("\n");
|
|
420
|
+
initialContents = firstNewline >= 0 ? initialContents.slice(firstNewline + 1) : "";
|
|
421
|
+
}
|
|
265
422
|
if (!initialContents) {
|
|
266
423
|
return;
|
|
267
424
|
}
|
|
@@ -271,6 +428,7 @@ function bootstrapFromExistingRollout({
|
|
|
271
428
|
let insideActiveRun = false;
|
|
272
429
|
let activeTurnId = null;
|
|
273
430
|
let pendingUserPreludeLine = null;
|
|
431
|
+
let latestTerminalRun = null;
|
|
274
432
|
|
|
275
433
|
for (const rawLine of lines) {
|
|
276
434
|
const line = rawLine.trim();
|
|
@@ -283,14 +441,20 @@ function bootstrapFromExistingRollout({
|
|
|
283
441
|
continue;
|
|
284
442
|
}
|
|
285
443
|
|
|
286
|
-
if (parsed.type === "session_meta") {
|
|
287
|
-
populateSessionMetaState(state, parsed.payload);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
444
|
const taskEventType = parsed?.type === "event_msg"
|
|
291
445
|
? readString(parsed?.payload?.type)
|
|
292
446
|
: "";
|
|
293
|
-
|
|
447
|
+
const eventUserMessage = taskEventType === "user_message"
|
|
448
|
+
&& Boolean(visibleUserPromptFromInputEntries(
|
|
449
|
+
readString(parsed?.payload?.message) || readString(parsed?.payload?.text)
|
|
450
|
+
));
|
|
451
|
+
const responseUserMessage = parsed?.type === "response_item"
|
|
452
|
+
&& readString(parsed?.payload?.role).toLowerCase() === "user"
|
|
453
|
+
&& Boolean(
|
|
454
|
+
visibleUserPromptFromInputEntries(extractResponseItemMessageText(parsed?.payload || {}))
|
|
455
|
+
|| responseItemHasUserImage(parsed?.payload)
|
|
456
|
+
);
|
|
457
|
+
if (eventUserMessage || responseUserMessage) {
|
|
294
458
|
pendingUserPreludeLine = line;
|
|
295
459
|
}
|
|
296
460
|
if (taskEventType === "task_started") {
|
|
@@ -298,11 +462,11 @@ function bootstrapFromExistingRollout({
|
|
|
298
462
|
activeTurnId = readString(parsed?.payload?.turn_id)
|
|
299
463
|
|| readString(parsed?.payload?.turnId)
|
|
300
464
|
|| "";
|
|
465
|
+
latestTerminalRun = null;
|
|
301
466
|
activeRunLines.length = 0;
|
|
302
467
|
if (pendingUserPreludeLine) {
|
|
303
468
|
activeRunLines.push(pendingUserPreludeLine);
|
|
304
469
|
}
|
|
305
|
-
pendingUserPreludeLine = null;
|
|
306
470
|
activeRunLines.push(line);
|
|
307
471
|
continue;
|
|
308
472
|
}
|
|
@@ -312,173 +476,309 @@ function bootstrapFromExistingRollout({
|
|
|
312
476
|
}
|
|
313
477
|
|
|
314
478
|
activeRunLines.push(line);
|
|
315
|
-
if (
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
479
|
+
if (TERMINAL_TASK_EVENT_TYPES.has(taskEventType)) {
|
|
480
|
+
// A sibling parallel turn's terminal event must not close the newest
|
|
481
|
+
// run's window; its own terminal event is still honored later.
|
|
482
|
+
const terminalTurnId = readString(parsed?.payload?.turn_id)
|
|
483
|
+
|| readString(parsed?.payload?.turnId);
|
|
484
|
+
if (terminalEventClosesTrackedTurn(terminalTurnId, activeTurnId)) {
|
|
485
|
+
latestTerminalRun = terminalRunFromEvent(parsed, activeTurnId);
|
|
486
|
+
insideActiveRun = false;
|
|
487
|
+
activeTurnId = "";
|
|
488
|
+
activeRunLines.length = 0;
|
|
489
|
+
pendingUserPreludeLine = null;
|
|
490
|
+
}
|
|
320
491
|
}
|
|
321
492
|
}
|
|
322
493
|
|
|
323
|
-
if (
|
|
324
|
-
state.
|
|
494
|
+
if (activeRunLines.length === 0 && latestTerminalRun) {
|
|
495
|
+
sendApplicationResponse(JSON.stringify(terminalCatchUpNotification(state.threadId, latestTerminalRun)));
|
|
325
496
|
return;
|
|
326
497
|
}
|
|
327
498
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
rememberBootstrapReplayBatches(bootstrapReplayCache, replayId, batches);
|
|
345
|
-
emitBootstrapReplayBatches(state, replayId, batches, sendApplicationResponse, {
|
|
346
|
-
setTimeoutFn,
|
|
347
|
-
batchIntervalMs: bootstrapReplayBatchIntervalMs,
|
|
348
|
-
pendingTimeouts,
|
|
349
|
-
});
|
|
350
|
-
}
|
|
499
|
+
// A run with no terminal marker whose rollout stopped growing long ago is dead
|
|
500
|
+
// (killed process / lost session); replaying it would fake a live stream and
|
|
501
|
+
// pin the reopened thread in "running" forever. Hydrate the run context
|
|
502
|
+
// silently instead, so heartbeats stay off but a run that resumes writing can
|
|
503
|
+
// still mirror its new activity live.
|
|
504
|
+
if (
|
|
505
|
+
activeRunLines.length > 0
|
|
506
|
+
&& isRolloutFileStale(rolloutPath, fsModule, nowMs, staleActiveRunMaxAgeMs)
|
|
507
|
+
) {
|
|
508
|
+
processRolloutLines(activeRunLines, state, () => {});
|
|
509
|
+
// task_started resets per-run state while hydrating. Apply the stale-run
|
|
510
|
+
// suppression afterwards so it survives until real file growth proves the
|
|
511
|
+
// desktop process is alive again.
|
|
512
|
+
state.suppressLiveActivityUntilGrowth = true;
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
351
515
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
516
|
+
// Bootstrap replay is catch-up history, not live streaming: tag it so the
|
|
517
|
+
// phone can batch-apply it, then close the burst with an explicit marker so
|
|
518
|
+
// the run still reads as active without waiting for the next heartbeat.
|
|
519
|
+
processRolloutLines(activeRunLines, state, sendApplicationResponse, {
|
|
520
|
+
tagBootstrapReplay: true,
|
|
521
|
+
});
|
|
522
|
+
if (activeRunLines.length > 0 && state.activeTurnId) {
|
|
523
|
+
sendApplicationResponse(JSON.stringify(createNotification("turn/activity", {
|
|
524
|
+
threadId: state.threadId,
|
|
525
|
+
turnId: state.activeTurnId,
|
|
526
|
+
id: state.activeTurnId,
|
|
527
|
+
remodexRolloutBootstrapComplete: true,
|
|
528
|
+
})));
|
|
529
|
+
}
|
|
356
530
|
}
|
|
357
531
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
532
|
+
// Expands backwards only until the newest active task has its opening user
|
|
533
|
+
// message. Every expansion reads just the newly needed prefix, so a 30MB file
|
|
534
|
+
// is read at most once rather than once per retry. The hard cap keeps bootstrap
|
|
535
|
+
// work/memory bounded; no coherent opener means no replay.
|
|
536
|
+
function readCoherentBootstrapWindow({ rolloutPath, fileSize, fsModule }) {
|
|
537
|
+
const maxBytes = Math.min(fileSize, DEFAULT_BOOTSTRAP_MAX_BYTES);
|
|
538
|
+
let windowBytes = Math.min(fileSize, DEFAULT_BOOTSTRAP_TAIL_BYTES);
|
|
539
|
+
let tailStart = Math.max(0, fileSize - windowBytes);
|
|
540
|
+
let contents = readFileSlice(rolloutPath, tailStart, fileSize, fsModule);
|
|
541
|
+
if (!contents) {
|
|
542
|
+
return null;
|
|
361
543
|
}
|
|
362
544
|
|
|
363
|
-
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
545
|
+
while (true) {
|
|
546
|
+
const alignedContents = alignedBootstrapContents(contents, tailStart);
|
|
547
|
+
const boundary = inspectBootstrapRunBoundary(alignedContents);
|
|
548
|
+
// When the window already reaches byte zero it is the complete rollout:
|
|
549
|
+
// some legitimate system/continuation turns have no materialized user row.
|
|
550
|
+
// The opener requirement only protects a truncated tail.
|
|
551
|
+
if (!boundary.hasActiveRun || boundary.hasOpeningUser || tailStart === 0) {
|
|
552
|
+
return { tailStart, contents };
|
|
367
553
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
if (!parsed) {
|
|
371
|
-
continue;
|
|
554
|
+
if (windowBytes >= maxBytes || tailStart === 0) {
|
|
555
|
+
return null;
|
|
372
556
|
}
|
|
373
557
|
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
558
|
+
const nextWindowBytes = Math.min(maxBytes, windowBytes * 2);
|
|
559
|
+
const nextTailStart = Math.max(0, fileSize - nextWindowBytes);
|
|
560
|
+
const prefix = readFileSlice(rolloutPath, nextTailStart, tailStart, fsModule);
|
|
561
|
+
if (!prefix) {
|
|
562
|
+
return null;
|
|
377
563
|
}
|
|
564
|
+
contents = `${prefix}${contents}`;
|
|
565
|
+
windowBytes = nextWindowBytes;
|
|
566
|
+
tailStart = nextTailStart;
|
|
378
567
|
}
|
|
379
568
|
}
|
|
380
569
|
|
|
381
|
-
function
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
batches,
|
|
385
|
-
sendApplicationResponse,
|
|
386
|
-
{
|
|
387
|
-
setTimeoutFn = setTimeout,
|
|
388
|
-
batchIntervalMs = DEFAULT_BOOTSTRAP_REPLAY_BATCH_INTERVAL_MS,
|
|
389
|
-
pendingTimeouts,
|
|
390
|
-
} = {}
|
|
391
|
-
) {
|
|
392
|
-
if (!Array.isArray(batches) || batches.length === 0) {
|
|
393
|
-
return;
|
|
570
|
+
function alignedBootstrapContents(contents, tailStart) {
|
|
571
|
+
if (tailStart === 0) {
|
|
572
|
+
return contents;
|
|
394
573
|
}
|
|
574
|
+
const firstNewline = contents.indexOf("\n");
|
|
575
|
+
return firstNewline >= 0 ? contents.slice(firstNewline + 1) : "";
|
|
576
|
+
}
|
|
395
577
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
for (
|
|
407
|
-
const
|
|
408
|
-
if (
|
|
409
|
-
sendBatch(batch, batchIndex);
|
|
578
|
+
function inspectBootstrapRunBoundary(contents) {
|
|
579
|
+
let activeTurnId = "";
|
|
580
|
+
let hasOpeningUser = false;
|
|
581
|
+
let hasTurnOutputSinceStart = false;
|
|
582
|
+
let pendingUserBeforeStart = false;
|
|
583
|
+
// A tail can begin after task_started. In that case activity without a
|
|
584
|
+
// closing terminal is evidence of an unknown active boundary, not permission
|
|
585
|
+
// to replay a partial conversation.
|
|
586
|
+
let unboundedActivitySinceTerminal = false;
|
|
587
|
+
|
|
588
|
+
for (const rawLine of contents.split("\n")) {
|
|
589
|
+
const parsed = safeParseJSON(rawLine.trim());
|
|
590
|
+
if (!parsed) {
|
|
410
591
|
continue;
|
|
411
592
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
593
|
+
const taskEventType = parsed?.type === "event_msg"
|
|
594
|
+
? readString(parsed?.payload?.type)
|
|
595
|
+
: "";
|
|
596
|
+
const isUser = taskEventType === "user_message"
|
|
597
|
+
|| (parsed?.type === "response_item" && readString(parsed?.payload?.role).toLowerCase() === "user");
|
|
598
|
+
const isResponseUser = parsed?.type === "response_item"
|
|
599
|
+
&& readString(parsed?.payload?.role).toLowerCase() === "user";
|
|
600
|
+
const userText = isResponseUser
|
|
601
|
+
? extractResponseItemMessageText(parsed?.payload || {})
|
|
602
|
+
: firstNonEmptyString([readString(parsed?.payload?.message), readString(parsed?.payload?.text)]);
|
|
603
|
+
const isVisibleUser = isUser && Boolean(
|
|
604
|
+
visibleUserPromptText(userText).trim()
|
|
605
|
+
|| (isResponseUser && responseItemHasUserImage(parsed?.payload))
|
|
606
|
+
);
|
|
607
|
+
if (taskEventType === "task_started") {
|
|
608
|
+
activeTurnId = readString(parsed?.payload?.turn_id)
|
|
609
|
+
|| readString(parsed?.payload?.turnId)
|
|
610
|
+
|| "synthetic-active-turn";
|
|
611
|
+
hasOpeningUser = pendingUserBeforeStart;
|
|
612
|
+
hasTurnOutputSinceStart = false;
|
|
613
|
+
pendingUserBeforeStart = false;
|
|
415
614
|
continue;
|
|
416
615
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
616
|
+
if (!activeTurnId) {
|
|
617
|
+
const isNeutral = isBootstrapNeutralRecord(parsed, taskEventType)
|
|
618
|
+
|| (isUser && !isVisibleUser);
|
|
619
|
+
if (TERMINAL_TASK_EVENT_TYPES.has(taskEventType)) {
|
|
620
|
+
unboundedActivitySinceTerminal = false;
|
|
621
|
+
} else if (!isNeutral) {
|
|
622
|
+
unboundedActivitySinceTerminal = true;
|
|
623
|
+
}
|
|
624
|
+
if (isVisibleUser) {
|
|
625
|
+
pendingUserBeforeStart = true;
|
|
626
|
+
} else if (!isNeutral) {
|
|
627
|
+
pendingUserBeforeStart = false;
|
|
628
|
+
}
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
// The only user item that can certify a truncated active run is the one
|
|
632
|
+
// adjacent to task_started, before any assistant/tool output. Later user
|
|
633
|
+
// messages are steering/follow-up input and must never turn a partial tail
|
|
634
|
+
// into a valid bootstrap baseline.
|
|
635
|
+
if (isVisibleUser && !hasTurnOutputSinceStart) {
|
|
636
|
+
hasOpeningUser = true;
|
|
637
|
+
}
|
|
638
|
+
if (TERMINAL_TASK_EVENT_TYPES.has(taskEventType)) {
|
|
639
|
+
const terminalTurnId = readString(parsed?.payload?.turn_id)
|
|
640
|
+
|| readString(parsed?.payload?.turnId);
|
|
641
|
+
if (terminalEventClosesTrackedTurn(terminalTurnId, activeTurnId)) {
|
|
642
|
+
activeTurnId = "";
|
|
643
|
+
hasOpeningUser = false;
|
|
644
|
+
hasTurnOutputSinceStart = false;
|
|
645
|
+
pendingUserBeforeStart = false;
|
|
646
|
+
unboundedActivitySinceTerminal = false;
|
|
647
|
+
}
|
|
648
|
+
} else if (!isUser && !isBootstrapNeutralRecord(parsed, taskEventType)) {
|
|
649
|
+
hasTurnOutputSinceStart = true;
|
|
650
|
+
}
|
|
423
651
|
}
|
|
652
|
+
|
|
653
|
+
return {
|
|
654
|
+
hasActiveRun: Boolean(activeTurnId) || unboundedActivitySinceTerminal,
|
|
655
|
+
hasOpeningUser,
|
|
656
|
+
};
|
|
424
657
|
}
|
|
425
658
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
659
|
+
// These records describe the runtime envelope around a turn. They are neither
|
|
660
|
+
// visible assistant output nor tool activity, so they must not turn the first
|
|
661
|
+
// real user prompt into a later steer during a bounded bootstrap scan.
|
|
662
|
+
function isBootstrapNeutralRecord(entry, taskEventType = "") {
|
|
663
|
+
const entryType = readString(entry?.type).toLowerCase();
|
|
664
|
+
return entryType === "session_meta"
|
|
665
|
+
|| entryType === "world_state"
|
|
666
|
+
|| entryType === "turn_context"
|
|
667
|
+
|| taskEventType === "context_updated";
|
|
668
|
+
}
|
|
430
669
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
670
|
+
// After a bounded bootstrap cannot reach the old opener, consume only new
|
|
671
|
+
// bytes. A later real user+task_started boundary safely starts a new live run;
|
|
672
|
+
// everything before it remains canonical-history territory.
|
|
673
|
+
function processAwaitingCoherentBoundary(lines, state, sendApplicationResponse, nowMs) {
|
|
674
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
675
|
+
const rawLine = lines[index];
|
|
676
|
+
const line = rawLine.trim();
|
|
677
|
+
const parsed = safeParseJSON(line);
|
|
678
|
+
if (!parsed) continue;
|
|
679
|
+
const eventType = parsed?.type === "event_msg" ? readString(parsed?.payload?.type) : "";
|
|
680
|
+
const responseUser = parsed?.type === "response_item"
|
|
681
|
+
&& readString(parsed?.payload?.role).toLowerCase() === "user";
|
|
682
|
+
const visibleUser = eventType === "user_message"
|
|
683
|
+
? Boolean(visibleUserPromptFromInputEntries(readString(parsed?.payload?.message) || readString(parsed?.payload?.text)))
|
|
684
|
+
: responseUser && Boolean(visibleUserPromptFromInputEntries(extractResponseItemMessageText(parsed?.payload || {})) || responseItemHasUserImage(parsed?.payload));
|
|
685
|
+
if (visibleUser) state.awaitingBoundaryPreludeLine = line;
|
|
686
|
+
if (eventType !== "task_started" || !state.awaitingBoundaryPreludeLine) continue;
|
|
687
|
+
const boundaryLines = [state.awaitingBoundaryPreludeLine, line];
|
|
688
|
+
state.awaitingBoundaryPreludeLine = "";
|
|
689
|
+
resetRunState(state);
|
|
690
|
+
processRolloutLines(boundaryLines, state, sendApplicationResponse, { nowMs });
|
|
691
|
+
// The boundary and its first output frequently land in the same filesystem
|
|
692
|
+
// read. Replay the remainder immediately so recovery never drops that
|
|
693
|
+
// assistant/tool burst while changing modes.
|
|
694
|
+
processRolloutLines(lines.slice(index + 1), state, sendApplicationResponse, { nowMs });
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
443
699
|
|
|
444
|
-
|
|
445
|
-
|
|
700
|
+
function isRolloutFileStale(rolloutPath, fsModule, nowMs, staleActiveRunMaxAgeMs) {
|
|
701
|
+
try {
|
|
702
|
+
const modifiedAtMs = fsModule.statSync(rolloutPath).mtimeMs;
|
|
703
|
+
return Number.isFinite(modifiedAtMs) && nowMs - modifiedAtMs >= staleActiveRunMaxAgeMs;
|
|
704
|
+
} catch {
|
|
705
|
+
return false;
|
|
446
706
|
}
|
|
707
|
+
}
|
|
447
708
|
|
|
448
|
-
|
|
449
|
-
|
|
709
|
+
function terminalRunFromEvent(entry, fallbackTurnId = "") {
|
|
710
|
+
const payload = entry?.payload || {};
|
|
711
|
+
const eventType = readString(payload.type);
|
|
712
|
+
if (!TERMINAL_TASK_EVENT_TYPES.has(eventType)) {
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const turnId = readString(payload.turn_id)
|
|
717
|
+
|| readString(payload.turnId)
|
|
718
|
+
|| readString(fallbackTurnId);
|
|
719
|
+
if (!turnId) {
|
|
720
|
+
return null;
|
|
450
721
|
}
|
|
451
|
-
|
|
722
|
+
|
|
723
|
+
return {
|
|
724
|
+
eventType,
|
|
725
|
+
turnId,
|
|
726
|
+
message: readString(payload.message),
|
|
727
|
+
};
|
|
452
728
|
}
|
|
453
729
|
|
|
454
|
-
function
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
.
|
|
458
|
-
.
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
.
|
|
463
|
-
|
|
730
|
+
function terminalCatchUpNotification(threadId, terminalRun) {
|
|
731
|
+
const params = {
|
|
732
|
+
threadId,
|
|
733
|
+
turnId: terminalRun.turnId,
|
|
734
|
+
id: terminalRun.turnId,
|
|
735
|
+
remodexRolloutTerminalCatchUp: true,
|
|
736
|
+
};
|
|
737
|
+
if (terminalRun.eventType === "turn_aborted") {
|
|
738
|
+
params.status = "aborted";
|
|
739
|
+
} else if (terminalRun.eventType === "error") {
|
|
740
|
+
params.status = "failed";
|
|
741
|
+
if (terminalRun.message) {
|
|
742
|
+
params.error = { message: terminalRun.message };
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return createNotification("turn/completed", params);
|
|
464
746
|
}
|
|
465
747
|
|
|
466
|
-
function
|
|
467
|
-
|
|
748
|
+
function processRolloutLines(lines, state, sendApplicationResponse, {
|
|
749
|
+
tagBootstrapReplay = false,
|
|
750
|
+
nowMs = Date.now(),
|
|
751
|
+
} = {}) {
|
|
752
|
+
if (!Array.isArray(lines) || lines.length === 0) {
|
|
468
753
|
return;
|
|
469
754
|
}
|
|
470
755
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
756
|
+
const emitNotification = (notification) => {
|
|
757
|
+
if (tagBootstrapReplay && notification.params && typeof notification.params === "object") {
|
|
758
|
+
notification.params.remodexRolloutBootstrapReplay = true;
|
|
759
|
+
}
|
|
760
|
+
sendApplicationResponse(JSON.stringify(notification));
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
for (const rawLine of lines) {
|
|
764
|
+
const line = rawLine.trim();
|
|
765
|
+
if (!line) {
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const parsed = safeParseJSON(line);
|
|
770
|
+
if (!parsed) {
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const notifications = synthesizeNotificationsFromRolloutEntry(parsed, state, { nowMs });
|
|
775
|
+
for (const notification of notifications) {
|
|
776
|
+
emitNotification(notification);
|
|
476
777
|
}
|
|
477
|
-
cache.delete(oldestKey);
|
|
478
778
|
}
|
|
479
779
|
}
|
|
480
780
|
|
|
481
|
-
function synthesizeNotificationsFromRolloutEntry(entry, state) {
|
|
781
|
+
function synthesizeNotificationsFromRolloutEntry(entry, state, { nowMs = Date.now() } = {}) {
|
|
482
782
|
if (entry?.type === "session_meta") {
|
|
483
783
|
populateSessionMetaState(state, entry.payload);
|
|
484
784
|
if (!isDesktopRolloutOrigin(state.sessionMeta)) {
|
|
@@ -500,12 +800,15 @@ function synthesizeNotificationsFromRolloutEntry(entry, state) {
|
|
|
500
800
|
const eventType = readString(payload.type);
|
|
501
801
|
|
|
502
802
|
if (eventType === "task_started") {
|
|
803
|
+
notifications.push(...finalizePendingSyntheticTerminal(state));
|
|
503
804
|
const explicitTurnId = readString(payload.turn_id) || readString(payload.turnId);
|
|
504
805
|
const turnId = explicitTurnId || buildSyntheticTurnId(state, entry);
|
|
505
806
|
state.activeTurnId = turnId;
|
|
506
807
|
state.activeTurnIdIsSynthetic = !explicitTurnId;
|
|
507
808
|
state.reasoningItemId = buildSyntheticItemId("thinking", state.threadId, turnId);
|
|
508
809
|
state.hasThinking = false;
|
|
810
|
+
state.hasReasoningContent = false;
|
|
811
|
+
state.emittedReasoningSummaryKeys.clear();
|
|
509
812
|
state.commandCalls.clear();
|
|
510
813
|
state.applyPatchCalls.clear();
|
|
511
814
|
state.emittedPatchApplyEndCalls.clear();
|
|
@@ -523,44 +826,66 @@ function synthesizeNotificationsFromRolloutEntry(entry, state) {
|
|
|
523
826
|
return notifications;
|
|
524
827
|
}
|
|
525
828
|
|
|
829
|
+
if (eventType && !TERMINAL_TASK_EVENT_TYPES.has(eventType)) {
|
|
830
|
+
clearPendingSyntheticTerminal(state);
|
|
831
|
+
}
|
|
832
|
+
|
|
526
833
|
if (eventType === "user_message") {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
834
|
+
// Rollouts persist injected context (AGENTS.md instructions, IDE prompt
|
|
835
|
+
// wrappers) as user_message events; only the real request is a bubble.
|
|
836
|
+
notifications.push(...userMessageNotifications(state, entry, payload));
|
|
837
|
+
return notifications;
|
|
838
|
+
}
|
|
531
839
|
|
|
532
|
-
|
|
840
|
+
if (eventType === "task_complete") {
|
|
841
|
+
const turnId = resolveRolloutEventTurnId(state, payload, { allowSyntheticPromotion: false });
|
|
533
842
|
if (!turnId) {
|
|
534
|
-
state.pendingUserMessages.push({
|
|
535
|
-
id: readString(payload.id),
|
|
536
|
-
message,
|
|
537
|
-
timestamp: readUserMessageTimestamp(entry, payload),
|
|
538
|
-
});
|
|
539
843
|
return [];
|
|
540
844
|
}
|
|
541
845
|
|
|
542
|
-
|
|
846
|
+
// Desktop runs parallel turns in one rollout: a sibling turn finishing
|
|
847
|
+
// must not wipe the tracked state of the turn that is still streaming.
|
|
848
|
+
const closesActiveRun = terminalEventClosesTrackedTurn(turnId, state.activeTurnId);
|
|
849
|
+
if (closesActiveRun) {
|
|
850
|
+
notifications.push(...turnFileChangeSnapshotNotifications(state, turnId));
|
|
851
|
+
}
|
|
852
|
+
notifications.push(createNotification("turn/completed", {
|
|
543
853
|
threadId: state.threadId,
|
|
544
854
|
turnId,
|
|
545
|
-
|
|
546
|
-
...timestampParams(readUserMessageTimestamp(entry, payload)),
|
|
855
|
+
id: turnId,
|
|
547
856
|
}));
|
|
857
|
+
if (closesActiveRun) {
|
|
858
|
+
resetRunState(state);
|
|
859
|
+
} else if (isSyntheticTerminalMismatch(state, turnId)) {
|
|
860
|
+
markPendingSyntheticTerminal(state, { status: "completed" }, nowMs);
|
|
861
|
+
}
|
|
548
862
|
return notifications;
|
|
549
863
|
}
|
|
550
864
|
|
|
551
|
-
|
|
552
|
-
|
|
865
|
+
// Aborted/failed desktop runs never write task_complete; close the mirrored
|
|
866
|
+
// turn anyway so the phone does not keep the thread pinned as running.
|
|
867
|
+
if (eventType === "turn_aborted" || eventType === "error") {
|
|
868
|
+
const turnId = resolveRolloutEventTurnId(state, payload, { allowSyntheticPromotion: false });
|
|
553
869
|
if (!turnId) {
|
|
554
870
|
return [];
|
|
555
871
|
}
|
|
556
872
|
|
|
557
|
-
|
|
558
|
-
notifications.push(createNotification("turn/completed", {
|
|
873
|
+
const terminalParams = {
|
|
559
874
|
threadId: state.threadId,
|
|
560
875
|
turnId,
|
|
561
876
|
id: turnId,
|
|
562
|
-
|
|
563
|
-
|
|
877
|
+
status: eventType === "error" ? "failed" : "aborted",
|
|
878
|
+
};
|
|
879
|
+
const errorMessage = readString(payload.message);
|
|
880
|
+
if (eventType === "error" && errorMessage) {
|
|
881
|
+
terminalParams.error = { message: errorMessage };
|
|
882
|
+
}
|
|
883
|
+
notifications.push(createNotification("turn/completed", terminalParams));
|
|
884
|
+
if (terminalEventClosesTrackedTurn(turnId, state.activeTurnId)) {
|
|
885
|
+
resetRunState(state);
|
|
886
|
+
} else if (isSyntheticTerminalMismatch(state, turnId)) {
|
|
887
|
+
markPendingSyntheticTerminal(state, terminalParams, nowMs);
|
|
888
|
+
}
|
|
564
889
|
return notifications;
|
|
565
890
|
}
|
|
566
891
|
|
|
@@ -579,18 +904,7 @@ function synthesizeNotificationsFromRolloutEntry(entry, state) {
|
|
|
579
904
|
}
|
|
580
905
|
|
|
581
906
|
if (eventType === "agent_message") {
|
|
582
|
-
|
|
583
|
-
if (!message || !shouldMirrorAgentMessage(payload)) {
|
|
584
|
-
return [];
|
|
585
|
-
}
|
|
586
|
-
const turnId = resolveRolloutEventTurnId(state, payload);
|
|
587
|
-
|
|
588
|
-
notifications.push(createNotification("codex/event/agent_message", {
|
|
589
|
-
threadId: state.threadId,
|
|
590
|
-
turnId,
|
|
591
|
-
itemId: buildAgentMessageItemId(state.threadId, turnId, entry, message),
|
|
592
|
-
message,
|
|
593
|
-
}));
|
|
907
|
+
notifications.push(...agentMessageNotifications(state, entry, payload));
|
|
594
908
|
return notifications;
|
|
595
909
|
}
|
|
596
910
|
|
|
@@ -613,9 +927,16 @@ function synthesizeNotificationsFromRolloutEntry(entry, state) {
|
|
|
613
927
|
return [];
|
|
614
928
|
}
|
|
615
929
|
|
|
930
|
+
clearPendingSyntheticTerminal(state);
|
|
931
|
+
|
|
616
932
|
const payload = entry.payload || {};
|
|
617
933
|
const itemType = normalizeRolloutItemType(payload.type);
|
|
618
934
|
|
|
935
|
+
if (itemType === "message") {
|
|
936
|
+
notifications.push(...responseItemMessageNotifications(state, entry, payload));
|
|
937
|
+
return notifications;
|
|
938
|
+
}
|
|
939
|
+
|
|
619
940
|
if (itemType === "reasoning") {
|
|
620
941
|
notifications.push(...reasoningNotifications(state, extractReasoningText(payload)));
|
|
621
942
|
return notifications;
|
|
@@ -649,12 +970,32 @@ function reasoningNotifications(state, text) {
|
|
|
649
970
|
return [];
|
|
650
971
|
}
|
|
651
972
|
|
|
652
|
-
const
|
|
653
|
-
if (!
|
|
973
|
+
const rawText = readString(text);
|
|
974
|
+
if (!rawText) {
|
|
654
975
|
return ensureThinkingNotifications(state);
|
|
655
976
|
}
|
|
656
977
|
|
|
978
|
+
const summaryEntries = summaryOnlyReasoningEntries(rawText);
|
|
979
|
+
let visibleText = rawText;
|
|
980
|
+
if (summaryEntries) {
|
|
981
|
+
const unseenEntries = summaryEntries.filter((entry) => {
|
|
982
|
+
if (state.emittedReasoningSummaryKeys.has(entry.key)) {
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
state.emittedReasoningSummaryKeys.add(entry.key);
|
|
986
|
+
return true;
|
|
987
|
+
});
|
|
988
|
+
if (unseenEntries.length === 0) {
|
|
989
|
+
return [];
|
|
990
|
+
}
|
|
991
|
+
visibleText = unseenEntries
|
|
992
|
+
.map((entry) => `**${entry.title}**\n\n<!-- -->`)
|
|
993
|
+
.join("\n\n");
|
|
994
|
+
}
|
|
995
|
+
|
|
657
996
|
state.hasThinking = true;
|
|
997
|
+
const delta = `${state.hasReasoningContent ? "\n\n" : ""}${visibleText}`;
|
|
998
|
+
state.hasReasoningContent = true;
|
|
658
999
|
return [
|
|
659
1000
|
createNotification("item/reasoning/textDelta", {
|
|
660
1001
|
threadId: state.threadId,
|
|
@@ -665,6 +1006,188 @@ function reasoningNotifications(state, text) {
|
|
|
665
1006
|
];
|
|
666
1007
|
}
|
|
667
1008
|
|
|
1009
|
+
// Newer Codex rollouts write the same cumulative reasoning summaries through
|
|
1010
|
+
// both event_msg and response_item records. Recognize only title/comment-only
|
|
1011
|
+
// payloads here; detailed reasoning remains a separate opaque stream.
|
|
1012
|
+
function summaryOnlyReasoningEntries(text) {
|
|
1013
|
+
const entries = [];
|
|
1014
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
1015
|
+
const line = rawLine.trim();
|
|
1016
|
+
if (!line || /^<!--.*-->$/.test(line)) {
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
const match = /^\*\*(.+?)\*\*$/.exec(line);
|
|
1020
|
+
if (!match) {
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
const title = match[1].trim();
|
|
1024
|
+
if (!title) {
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1027
|
+
entries.push({
|
|
1028
|
+
title,
|
|
1029
|
+
key: title.replace(/\s+/g, " ").toLowerCase(),
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
return entries.length > 0 ? entries : null;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function responseItemMessageNotifications(state, entry, payload) {
|
|
1036
|
+
const role = readString(payload?.role).toLowerCase();
|
|
1037
|
+
if (role === "user") {
|
|
1038
|
+
return userMessageNotifications(state, entry, payload, {
|
|
1039
|
+
rawMessage: extractResponseItemMessageText(payload),
|
|
1040
|
+
isResponseItem: true,
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
if (role && role !== "assistant") {
|
|
1044
|
+
return [];
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const message = extractResponseItemMessageText(payload);
|
|
1048
|
+
if (!message) {
|
|
1049
|
+
return [];
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
return agentMessageNotifications(state, entry, {
|
|
1053
|
+
message,
|
|
1054
|
+
phase: payload?.phase,
|
|
1055
|
+
itemId: readString(payload?.id),
|
|
1056
|
+
turn_id: readString(payload?.turn_id) || readString(payload?.internal_chat_message_metadata_passthrough?.turn_id),
|
|
1057
|
+
turnId: readString(payload?.turnId) || readString(payload?.internal_chat_message_metadata_passthrough?.turnId),
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function userMessageNotifications(state, entry, payload, {
|
|
1062
|
+
rawMessage = "",
|
|
1063
|
+
isResponseItem = false,
|
|
1064
|
+
} = {}) {
|
|
1065
|
+
const imagePlaceholder = responseItemHasUserImage(payload) ? "Image attachment" : "";
|
|
1066
|
+
const message = visibleUserPromptFromInputEntries(
|
|
1067
|
+
rawMessage || readString(payload?.message) || readString(payload?.text) || imagePlaceholder
|
|
1068
|
+
);
|
|
1069
|
+
if (!message) {
|
|
1070
|
+
return [];
|
|
1071
|
+
}
|
|
1072
|
+
const turnId = resolveRolloutEventTurnId(state, payload);
|
|
1073
|
+
const itemId = readString(payload?.id) || readString(payload?.itemId) || readString(payload?.item_id);
|
|
1074
|
+
const timestamp = readUserMessageTimestamp(entry, payload);
|
|
1075
|
+
if (!turnId) {
|
|
1076
|
+
// response_item(user) can precede task_started. Hold it exactly like the
|
|
1077
|
+
// event_msg form so task_started flushes the opener before thinking/output.
|
|
1078
|
+
const pendingKey = `${itemId || ""}:${message}`;
|
|
1079
|
+
if (!state.pendingUserMessages.some((pending) => `${pending.id || ""}:${pending.message}` === pendingKey)) {
|
|
1080
|
+
state.pendingUserMessages.push({ id: itemId, message, timestamp, isResponseItem });
|
|
1081
|
+
}
|
|
1082
|
+
return [];
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
const dedupeKey = userMessageOccurrenceKey(state, turnId, message, { isResponseItem });
|
|
1086
|
+
if (state.emittedUserMessageKeys.has(dedupeKey)) {
|
|
1087
|
+
return [];
|
|
1088
|
+
}
|
|
1089
|
+
state.emittedUserMessageKeys.add(dedupeKey);
|
|
1090
|
+
return [createNotification("codex/event/user_message", {
|
|
1091
|
+
threadId: state.threadId,
|
|
1092
|
+
turnId,
|
|
1093
|
+
message,
|
|
1094
|
+
...(itemId ? { id: itemId } : {}),
|
|
1095
|
+
...timestampParams(timestamp),
|
|
1096
|
+
})];
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Rollouts commonly persist one user message twice as an event_msg and a
|
|
1100
|
+
// response_item pair, in either order: desktop-started turns log event_msg
|
|
1101
|
+
// first, phone/app-server-started turns log response_item first. Pair the two
|
|
1102
|
+
// shapes by occurrence in both directions instead of collapsing every
|
|
1103
|
+
// identical text in the turn, because repeated steers are legitimate.
|
|
1104
|
+
function userMessageOccurrenceKey(state, turnId, message, { isResponseItem = false } = {}) {
|
|
1105
|
+
const baseKey = buildRemodexSourceItemKey(turnId, message);
|
|
1106
|
+
const unpairedMap = isResponseItem
|
|
1107
|
+
? state.pendingEventUserMessageOccurrencesByBaseKey
|
|
1108
|
+
: state.pendingResponseItemUserMessageOccurrencesByBaseKey;
|
|
1109
|
+
const ownPendingMap = isResponseItem
|
|
1110
|
+
? state.pendingResponseItemUserMessageOccurrencesByBaseKey
|
|
1111
|
+
: state.pendingEventUserMessageOccurrencesByBaseKey;
|
|
1112
|
+
|
|
1113
|
+
const unpaired = unpairedMap.get(baseKey) || [];
|
|
1114
|
+
if (unpaired.length > 0) {
|
|
1115
|
+
const occurrence = unpaired.shift();
|
|
1116
|
+
if (unpaired.length === 0) {
|
|
1117
|
+
unpairedMap.delete(baseKey);
|
|
1118
|
+
} else {
|
|
1119
|
+
unpairedMap.set(baseKey, unpaired);
|
|
1120
|
+
}
|
|
1121
|
+
return `user:${baseKey}:${occurrence}`;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const occurrence = (state.userMessageOccurrencesByBaseKey.get(baseKey) || 0) + 1;
|
|
1125
|
+
state.userMessageOccurrencesByBaseKey.set(baseKey, occurrence);
|
|
1126
|
+
const ownPending = ownPendingMap.get(baseKey) || [];
|
|
1127
|
+
ownPending.push(occurrence);
|
|
1128
|
+
ownPendingMap.set(baseKey, ownPending);
|
|
1129
|
+
return `user:${baseKey}:${occurrence}`;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function responseItemHasUserImage(payload) {
|
|
1133
|
+
return Array.isArray(payload?.content) && payload.content.some((part) => {
|
|
1134
|
+
const type = readString(part?.type).toLowerCase();
|
|
1135
|
+
return type === "input_image" || type === "image" || type === "image_url";
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function agentMessageNotifications(state, entry, payload) {
|
|
1140
|
+
const message = readString(payload?.message) || readString(payload?.text);
|
|
1141
|
+
if (!message) {
|
|
1142
|
+
return [];
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const turnId = resolveRolloutEventTurnId(state, payload);
|
|
1146
|
+
const baseKey = agentMessageDedupeKey(turnId, message);
|
|
1147
|
+
const providerItemId = readString(payload?.itemId);
|
|
1148
|
+
const nextOccurrence = (state.agentMessageOccurrencesByBaseKey.get(baseKey) || 0) + 1;
|
|
1149
|
+
const occurrence = providerItemId && state.pendingEventAgentMessageOccurrencesByBaseKey.has(baseKey)
|
|
1150
|
+
? state.pendingEventAgentMessageOccurrencesByBaseKey.get(baseKey)
|
|
1151
|
+
: nextOccurrence;
|
|
1152
|
+
state.agentMessageOccurrencesByBaseKey.set(baseKey, Math.max(nextOccurrence, occurrence));
|
|
1153
|
+
if (providerItemId) {
|
|
1154
|
+
state.pendingEventAgentMessageOccurrencesByBaseKey.delete(baseKey);
|
|
1155
|
+
} else {
|
|
1156
|
+
state.pendingEventAgentMessageOccurrencesByBaseKey.set(baseKey, occurrence);
|
|
1157
|
+
}
|
|
1158
|
+
const dedupeKey = `${baseKey}:${occurrence}`;
|
|
1159
|
+
if (state.emittedAgentMessageKeys.has(dedupeKey)) {
|
|
1160
|
+
return [];
|
|
1161
|
+
}
|
|
1162
|
+
state.emittedAgentMessageKeys.add(dedupeKey);
|
|
1163
|
+
|
|
1164
|
+
// Commentary (interleaved progress prose) is mirrored too: desktop renders it
|
|
1165
|
+
// between tool calls, and dropping it would glue every tool row into one burst
|
|
1166
|
+
// on the phone. The phase rides along so the app can keep commentary rows
|
|
1167
|
+
// distinct from the final answer.
|
|
1168
|
+
const params = {
|
|
1169
|
+
threadId: state.threadId,
|
|
1170
|
+
turnId,
|
|
1171
|
+
itemId: providerItemId || buildAgentMessageItemId(state.threadId, turnId, entry, message),
|
|
1172
|
+
// The same assistant item may first arrive as event_msg (without Codex's
|
|
1173
|
+
// item id) and later as response_item/history (with one). Preserve a
|
|
1174
|
+
// stable source alias across bootstrap/reconnect so the phone can merge
|
|
1175
|
+
// those representations without using unsafe global text deduplication.
|
|
1176
|
+
...(occurrence === 1 ? { remodexSourceItemKey: baseKey } : {}),
|
|
1177
|
+
message,
|
|
1178
|
+
};
|
|
1179
|
+
const phase = readString(payload?.phase);
|
|
1180
|
+
if (phase) {
|
|
1181
|
+
params.phase = phase;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
return [createNotification("codex/event/agent_message", params)];
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function extractResponseItemMessageText(payload) {
|
|
1188
|
+
return responseItemMessageText(payload);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
668
1191
|
function toolStartNotifications(state, payload) {
|
|
669
1192
|
if (!state.activeTurnId) {
|
|
670
1193
|
return [];
|
|
@@ -684,6 +1207,40 @@ function toolStartNotifications(state, payload) {
|
|
|
684
1207
|
];
|
|
685
1208
|
}
|
|
686
1209
|
|
|
1210
|
+
if (readString(toolName).toLowerCase() === "apply_patch") {
|
|
1211
|
+
const item = buildApplyPatchFileChangeItem({
|
|
1212
|
+
callId,
|
|
1213
|
+
patch: readString(argumentsObject.patch) || readString(argumentsObject.input) || readString(payload.input),
|
|
1214
|
+
status: readString(payload.status) || "completed",
|
|
1215
|
+
idFallback: buildSyntheticItemId("file-change", state.threadId, state.activeTurnId, callId),
|
|
1216
|
+
});
|
|
1217
|
+
const notifications = [...ensureThinkingNotifications(state)];
|
|
1218
|
+
if (!item) {
|
|
1219
|
+
return [
|
|
1220
|
+
...notifications,
|
|
1221
|
+
createNotification("codex/event/background_event", {
|
|
1222
|
+
threadId: state.threadId,
|
|
1223
|
+
turnId: state.activeTurnId,
|
|
1224
|
+
call_id: callId,
|
|
1225
|
+
message: genericToolActivityMessage(toolName),
|
|
1226
|
+
}),
|
|
1227
|
+
];
|
|
1228
|
+
}
|
|
1229
|
+
state.applyPatchCalls.set(callId, item);
|
|
1230
|
+
return [
|
|
1231
|
+
...notifications,
|
|
1232
|
+
createNotification("codex/event/patch_apply_begin", {
|
|
1233
|
+
threadId: state.threadId,
|
|
1234
|
+
turnId: state.activeTurnId,
|
|
1235
|
+
id: state.activeTurnId,
|
|
1236
|
+
call_id: callId,
|
|
1237
|
+
itemId: item.id,
|
|
1238
|
+
status: "inProgress",
|
|
1239
|
+
changes: item.changes,
|
|
1240
|
+
}),
|
|
1241
|
+
];
|
|
1242
|
+
}
|
|
1243
|
+
|
|
687
1244
|
state.commandCalls.set(callId, {
|
|
688
1245
|
toolName,
|
|
689
1246
|
command: resolveToolCommand(toolName, argumentsObject),
|
|
@@ -845,8 +1402,15 @@ function toolOutputNotifications(state, payload) {
|
|
|
845
1402
|
}
|
|
846
1403
|
|
|
847
1404
|
if (!isCommandToolName(toolCall.toolName)) {
|
|
1405
|
+
const notifications = [...ensureThinkingNotifications(state)];
|
|
1406
|
+
notifications.push(createNotification("codex/event/background_event", {
|
|
1407
|
+
threadId: state.threadId,
|
|
1408
|
+
turnId: state.activeTurnId,
|
|
1409
|
+
call_id: callId,
|
|
1410
|
+
message: genericToolCompletionMessage(toolCall.toolName),
|
|
1411
|
+
}));
|
|
848
1412
|
state.commandCalls.delete(callId);
|
|
849
|
-
return
|
|
1413
|
+
return notifications;
|
|
850
1414
|
}
|
|
851
1415
|
|
|
852
1416
|
const output = readString(payload.output);
|
|
@@ -945,6 +1509,75 @@ function itemCompletedNotifications(state, payload) {
|
|
|
945
1509
|
];
|
|
946
1510
|
}
|
|
947
1511
|
|
|
1512
|
+
// Synthetic turn ids are a temporary stand-in; close them if a terminal event
|
|
1513
|
+
// had no later activity proving it belonged to a sibling parallel run.
|
|
1514
|
+
function markPendingSyntheticTerminal(state, terminalParams = {}, nowMs = Date.now()) {
|
|
1515
|
+
if (state.activeTurnIdIsSynthetic && state.activeTurnId) {
|
|
1516
|
+
state.pendingSyntheticTerminalTurnId = state.activeTurnId;
|
|
1517
|
+
state.pendingSyntheticTerminalStartedAt = nowMs;
|
|
1518
|
+
state.pendingSyntheticTerminalStatus = readString(terminalParams.status) || "";
|
|
1519
|
+
state.pendingSyntheticTerminalErrorMessage = readString(terminalParams.error?.message) || "";
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
function clearPendingSyntheticTerminal(state) {
|
|
1524
|
+
state.pendingSyntheticTerminalTurnId = null;
|
|
1525
|
+
state.pendingSyntheticTerminalStartedAt = 0;
|
|
1526
|
+
state.pendingSyntheticTerminalStatus = "";
|
|
1527
|
+
state.pendingSyntheticTerminalErrorMessage = "";
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function isSyntheticTerminalMismatch(state, terminalTurnId) {
|
|
1531
|
+
return Boolean(
|
|
1532
|
+
state.activeTurnIdIsSynthetic
|
|
1533
|
+
&& state.activeTurnId
|
|
1534
|
+
&& terminalTurnId
|
|
1535
|
+
&& terminalTurnId !== state.activeTurnId
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function finalizePendingSyntheticTerminal(state) {
|
|
1540
|
+
const turnId = state.pendingSyntheticTerminalTurnId;
|
|
1541
|
+
if (!turnId) {
|
|
1542
|
+
return [];
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
const terminalParams = {
|
|
1546
|
+
threadId: state.threadId,
|
|
1547
|
+
turnId,
|
|
1548
|
+
id: turnId,
|
|
1549
|
+
};
|
|
1550
|
+
if (state.pendingSyntheticTerminalStatus) {
|
|
1551
|
+
terminalParams.status = state.pendingSyntheticTerminalStatus;
|
|
1552
|
+
}
|
|
1553
|
+
if (state.pendingSyntheticTerminalErrorMessage) {
|
|
1554
|
+
terminalParams.error = { message: state.pendingSyntheticTerminalErrorMessage };
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
const notifications = [
|
|
1558
|
+
...turnFileChangeSnapshotNotifications(state, turnId),
|
|
1559
|
+
createNotification("turn/completed", terminalParams),
|
|
1560
|
+
];
|
|
1561
|
+
resetRunState(state);
|
|
1562
|
+
return notifications;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
function finalizePendingSyntheticTerminalIfReady(state, nowMs, graceMs) {
|
|
1566
|
+
if (!state.pendingSyntheticTerminalTurnId) {
|
|
1567
|
+
return [];
|
|
1568
|
+
}
|
|
1569
|
+
const startedAt = Number.isFinite(state.pendingSyntheticTerminalStartedAt)
|
|
1570
|
+
? state.pendingSyntheticTerminalStartedAt
|
|
1571
|
+
: nowMs;
|
|
1572
|
+
const resolvedGraceMs = Number.isFinite(graceMs)
|
|
1573
|
+
? Math.max(0, graceMs)
|
|
1574
|
+
: DEFAULT_SYNTHETIC_TERMINAL_GRACE_MS;
|
|
1575
|
+
if (nowMs - startedAt < resolvedGraceMs) {
|
|
1576
|
+
return [];
|
|
1577
|
+
}
|
|
1578
|
+
return finalizePendingSyntheticTerminal(state);
|
|
1579
|
+
}
|
|
1580
|
+
|
|
948
1581
|
function ensureThinkingNotifications(state) {
|
|
949
1582
|
if (!state.activeTurnId || state.hasThinking) {
|
|
950
1583
|
return [];
|
|
@@ -973,11 +1606,29 @@ function createMirrorState(threadId) {
|
|
|
973
1606
|
activeTurnId: null,
|
|
974
1607
|
reasoningItemId: null,
|
|
975
1608
|
hasThinking: false,
|
|
1609
|
+
hasReasoningContent: false,
|
|
1610
|
+
emittedReasoningSummaryKeys: new Set(),
|
|
976
1611
|
commandCalls: new Map(),
|
|
977
1612
|
applyPatchCalls: new Map(),
|
|
978
1613
|
emittedPatchApplyEndCalls: new Set(),
|
|
1614
|
+
emittedAgentMessageKeys: new Set(),
|
|
1615
|
+
agentMessageOccurrencesByBaseKey: new Map(),
|
|
1616
|
+
pendingEventAgentMessageOccurrencesByBaseKey: new Map(),
|
|
1617
|
+
emittedUserMessageKeys: new Set(),
|
|
1618
|
+
userMessageOccurrencesByBaseKey: new Map(),
|
|
1619
|
+
pendingEventUserMessageOccurrencesByBaseKey: new Map(),
|
|
1620
|
+
pendingResponseItemUserMessageOccurrencesByBaseKey: new Map(),
|
|
979
1621
|
pendingUserMessages: [],
|
|
1622
|
+
pendingSyntheticTerminalTurnId: null,
|
|
1623
|
+
pendingSyntheticTerminalStartedAt: 0,
|
|
1624
|
+
pendingSyntheticTerminalStatus: "",
|
|
1625
|
+
pendingSyntheticTerminalErrorMessage: "",
|
|
980
1626
|
activeTurnIdIsSynthetic: false,
|
|
1627
|
+
// True after a stale bootstrap: run context is hydrated but nothing is
|
|
1628
|
+
// emitted (including heartbeats) until the rollout file grows again.
|
|
1629
|
+
suppressLiveActivityUntilGrowth: false,
|
|
1630
|
+
awaitingCoherentBoundary: false,
|
|
1631
|
+
awaitingBoundaryPreludeLine: "",
|
|
981
1632
|
};
|
|
982
1633
|
}
|
|
983
1634
|
|
|
@@ -1031,7 +1682,8 @@ function parseToolArguments(rawArguments) {
|
|
|
1031
1682
|
|
|
1032
1683
|
function planUpdateNotifications(state, argumentsObject) {
|
|
1033
1684
|
const plan = normalizeProgressPlanSteps(argumentsObject.plan);
|
|
1034
|
-
|
|
1685
|
+
const explanation = readString(argumentsObject.explanation);
|
|
1686
|
+
if (!hasVisiblePlanUpdate(explanation, plan)) {
|
|
1035
1687
|
return [];
|
|
1036
1688
|
}
|
|
1037
1689
|
|
|
@@ -1040,7 +1692,6 @@ function planUpdateNotifications(state, argumentsObject) {
|
|
|
1040
1692
|
turnId: state.activeTurnId,
|
|
1041
1693
|
plan,
|
|
1042
1694
|
};
|
|
1043
|
-
const explanation = readString(argumentsObject.explanation);
|
|
1044
1695
|
if (explanation) {
|
|
1045
1696
|
params.explanation = explanation;
|
|
1046
1697
|
}
|
|
@@ -1125,9 +1776,8 @@ function genericToolActivityMessage(toolName) {
|
|
|
1125
1776
|
}
|
|
1126
1777
|
}
|
|
1127
1778
|
|
|
1128
|
-
function
|
|
1129
|
-
|
|
1130
|
-
return phase !== "commentary";
|
|
1779
|
+
function genericToolCompletionMessage(toolName) {
|
|
1780
|
+
return `Completed ${readString(toolName)}`;
|
|
1131
1781
|
}
|
|
1132
1782
|
|
|
1133
1783
|
function createNotification(method, params = {}) {
|
|
@@ -1151,13 +1801,29 @@ function flushPendingUserMessageNotifications(state, turnId) {
|
|
|
1151
1801
|
return [];
|
|
1152
1802
|
}
|
|
1153
1803
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1804
|
+
const resolvedTurnId = readString(turnId) || readString(state.activeTurnId);
|
|
1805
|
+
return messages
|
|
1806
|
+
.map((pending) => ({ ...pending, message: visibleUserPromptFromInputEntries(pending.message) }))
|
|
1807
|
+
.filter((pending) => pending.message)
|
|
1808
|
+
.filter((pending) => {
|
|
1809
|
+
const dedupeKey = userMessageOccurrenceKey(state, resolvedTurnId, pending.message, {
|
|
1810
|
+
isResponseItem: pending.isResponseItem === true,
|
|
1811
|
+
});
|
|
1812
|
+
if (state.emittedUserMessageKeys.has(dedupeKey)) {
|
|
1813
|
+
return false;
|
|
1814
|
+
}
|
|
1815
|
+
state.emittedUserMessageKeys.add(dedupeKey);
|
|
1816
|
+
return true;
|
|
1817
|
+
})
|
|
1818
|
+
.map((pending) => createNotification("codex/event/user_message", {
|
|
1819
|
+
threadId: state.threadId,
|
|
1820
|
+
// An empty turnId reads as "no turn identity" on the phone and blocks
|
|
1821
|
+
// dedup against the turn-bound row of the same prompt; omit it instead.
|
|
1822
|
+
...(resolvedTurnId ? { turnId: resolvedTurnId } : {}),
|
|
1823
|
+
message: pending.message,
|
|
1824
|
+
...(pending.id ? { id: pending.id } : {}),
|
|
1825
|
+
...timestampParams(pending.timestamp),
|
|
1826
|
+
}));
|
|
1161
1827
|
}
|
|
1162
1828
|
|
|
1163
1829
|
function readUserMessageTimestamp(entry, payload = {}) {
|
|
@@ -1187,11 +1853,36 @@ function buildSyntheticTurnId(state, entry) {
|
|
|
1187
1853
|
return `rollout-turn:${state.threadId}:${timestamp}`;
|
|
1188
1854
|
}
|
|
1189
1855
|
|
|
1190
|
-
function resolveRolloutEventTurnId(state, payload = {}) {
|
|
1856
|
+
function resolveRolloutEventTurnId(state, payload = {}, { allowSyntheticPromotion = true } = {}) {
|
|
1857
|
+
const explicitTurnId = readString(payload.turn_id) || readString(payload.turnId);
|
|
1191
1858
|
if (state.activeTurnIdIsSynthetic && state.activeTurnId) {
|
|
1859
|
+
if (explicitTurnId) {
|
|
1860
|
+
// Terminal events must not promote: with parallel turns, a sibling's
|
|
1861
|
+
// terminal explicit id would hijack the synthetic run and wipe it. The
|
|
1862
|
+
// active run's real id is adopted from its own non-terminal events.
|
|
1863
|
+
if (allowSyntheticPromotion) {
|
|
1864
|
+
promoteSyntheticTurnId(state, explicitTurnId);
|
|
1865
|
+
}
|
|
1866
|
+
return explicitTurnId;
|
|
1867
|
+
}
|
|
1192
1868
|
return state.activeTurnId;
|
|
1193
1869
|
}
|
|
1194
|
-
return
|
|
1870
|
+
return explicitTurnId || state.activeTurnId || "";
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
function promoteSyntheticTurnId(state, explicitTurnId) {
|
|
1874
|
+
const oldTurnId = state.activeTurnId;
|
|
1875
|
+
if (!oldTurnId || oldTurnId === explicitTurnId) {
|
|
1876
|
+
state.activeTurnId = explicitTurnId;
|
|
1877
|
+
state.activeTurnIdIsSynthetic = false;
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
state.activeTurnId = explicitTurnId;
|
|
1882
|
+
state.activeTurnIdIsSynthetic = false;
|
|
1883
|
+
if (state.reasoningItemId === buildSyntheticItemId("thinking", state.threadId, oldTurnId)) {
|
|
1884
|
+
state.reasoningItemId = buildSyntheticItemId("thinking", state.threadId, explicitTurnId);
|
|
1885
|
+
}
|
|
1195
1886
|
}
|
|
1196
1887
|
|
|
1197
1888
|
function buildAgentMessageItemId(threadId, turnId, entry, message) {
|
|
@@ -1209,6 +1900,15 @@ function buildAgentMessageItemId(threadId, turnId, entry, message) {
|
|
|
1209
1900
|
);
|
|
1210
1901
|
}
|
|
1211
1902
|
|
|
1903
|
+
// Keyed on turn + text only: the same assistant text often arrives twice per
|
|
1904
|
+
// turn (event_msg agent_message and response_item message), and only one side
|
|
1905
|
+
// carries `phase`, so phase must stay out of the key for them to collide.
|
|
1906
|
+
// Legitimately repeated identical prose in one turn is rare and the phone's
|
|
1907
|
+
// item-scoped dedup covers the remainder.
|
|
1908
|
+
function agentMessageDedupeKey(turnId, message) {
|
|
1909
|
+
return buildRemodexSourceItemKey(turnId, message);
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1212
1912
|
function generatedImagePathForRolloutItem(threadId, callId) {
|
|
1213
1913
|
const resolvedThreadId = readString(threadId);
|
|
1214
1914
|
const resolvedCallId = readString(callId);
|
|
@@ -1227,11 +1927,27 @@ function resetRunState(state) {
|
|
|
1227
1927
|
state.activeTurnId = null;
|
|
1228
1928
|
state.reasoningItemId = null;
|
|
1229
1929
|
state.hasThinking = false;
|
|
1930
|
+
state.hasReasoningContent = false;
|
|
1931
|
+
state.emittedReasoningSummaryKeys.clear();
|
|
1230
1932
|
state.commandCalls.clear();
|
|
1231
1933
|
state.applyPatchCalls.clear();
|
|
1232
1934
|
state.emittedPatchApplyEndCalls.clear();
|
|
1935
|
+
state.emittedAgentMessageKeys.clear();
|
|
1936
|
+
state.agentMessageOccurrencesByBaseKey.clear();
|
|
1937
|
+
state.pendingEventAgentMessageOccurrencesByBaseKey.clear();
|
|
1938
|
+
state.emittedUserMessageKeys.clear();
|
|
1939
|
+
state.userMessageOccurrencesByBaseKey.clear();
|
|
1940
|
+
state.pendingEventUserMessageOccurrencesByBaseKey.clear();
|
|
1941
|
+
state.pendingResponseItemUserMessageOccurrencesByBaseKey.clear();
|
|
1233
1942
|
state.pendingUserMessages.length = 0;
|
|
1943
|
+
state.pendingSyntheticTerminalTurnId = null;
|
|
1944
|
+
state.pendingSyntheticTerminalStartedAt = 0;
|
|
1945
|
+
state.pendingSyntheticTerminalStatus = "";
|
|
1946
|
+
state.pendingSyntheticTerminalErrorMessage = "";
|
|
1234
1947
|
state.activeTurnIdIsSynthetic = false;
|
|
1948
|
+
state.suppressLiveActivityUntilGrowth = false;
|
|
1949
|
+
state.awaitingCoherentBoundary = false;
|
|
1950
|
+
state.awaitingBoundaryPreludeLine = "";
|
|
1235
1951
|
}
|
|
1236
1952
|
|
|
1237
1953
|
function readThreadId(params) {
|
|
@@ -1241,10 +1957,6 @@ function readThreadId(params) {
|
|
|
1241
1957
|
]) || "";
|
|
1242
1958
|
}
|
|
1243
1959
|
|
|
1244
|
-
function readFileSize(filePath, fsModule) {
|
|
1245
|
-
return fsModule.statSync(filePath).size;
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
1960
|
function readFileSlice(filePath, start, endExclusive, fsModule) {
|
|
1249
1961
|
const length = Math.max(0, endExclusive - start);
|
|
1250
1962
|
if (length === 0) {
|