@makerbi/remodex 2.3.1 → 2.4.0
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 +1 -1
- package/src/bridge.js +34 -0
- package/src/desktop-ipc-action-follower.js +310 -87
- package/src/desktop-ipc-conversation-adapter.js +138 -2
- package/src/desktop-ipc-conversation-projector.js +147 -1
- package/src/desktop-ipc-shared.js +148 -38
- package/src/push-notification-tracker.js +134 -0
- package/src/rollout-live-mirror.js +170 -11
- package/src/session-jsonl-history.js +5 -1
|
@@ -4,14 +4,29 @@
|
|
|
4
4
|
// Exports: createPushNotificationTracker
|
|
5
5
|
// Depends on: ./push-notification-completion-dedupe
|
|
6
6
|
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const os = require("os");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
|
|
7
11
|
const {
|
|
8
12
|
createPushNotificationCompletionDedupe,
|
|
9
13
|
} = require("./push-notification-completion-dedupe");
|
|
10
14
|
|
|
15
|
+
const DEFAULT_GOAL_PUSH_STATE_PATH = path.join(os.homedir(), ".remodex", "goal-push-state.json");
|
|
16
|
+
|
|
11
17
|
const DEFAULT_PREVIEW_MAX_CHARS = 160;
|
|
12
18
|
const MAX_THREAD_TITLE_ENTRIES = 200;
|
|
13
19
|
const MAX_TURN_STATE_ENTRIES = 500;
|
|
14
20
|
const MAX_THREAD_ID_BY_TURN_ENTRIES = 500;
|
|
21
|
+
const MAX_GOAL_STATUS_ENTRIES = 500;
|
|
22
|
+
|
|
23
|
+
// Goal states worth waking the phone for: terminal or needs-user-attention.
|
|
24
|
+
const GOAL_PUSH_BODIES = new Map([
|
|
25
|
+
["complete", "Goal complete"],
|
|
26
|
+
["blocked", "Goal blocked — Codex needs your input"],
|
|
27
|
+
["usageLimited", "Goal stopped — usage limit reached"],
|
|
28
|
+
["budgetLimited", "Goal stopped — token budget reached"],
|
|
29
|
+
]);
|
|
15
30
|
|
|
16
31
|
function createPushNotificationTracker({
|
|
17
32
|
sessionId,
|
|
@@ -19,10 +34,14 @@ function createPushNotificationTracker({
|
|
|
19
34
|
previewMaxChars = DEFAULT_PREVIEW_MAX_CHARS,
|
|
20
35
|
logPrefix = "[remodex]",
|
|
21
36
|
now = () => Date.now(),
|
|
37
|
+
goalPushStatePath = DEFAULT_GOAL_PUSH_STATE_PATH,
|
|
22
38
|
} = {}) {
|
|
23
39
|
const threadTitleById = new Map();
|
|
24
40
|
const threadIdByTurnId = new Map();
|
|
25
41
|
const turnStateByKey = new Map();
|
|
42
|
+
// Persisted across bridge restarts so goal transitions that happened while the
|
|
43
|
+
// bridge was down still notify on the first post-restart snapshot.
|
|
44
|
+
const goalStatusByThreadId = loadGoalPushState(goalPushStatePath, logPrefix);
|
|
26
45
|
const completionDedupe = createPushNotificationCompletionDedupe({ now });
|
|
27
46
|
|
|
28
47
|
// ─── ENTRY POINT ─────────────────────────────────────────────
|
|
@@ -33,6 +52,18 @@ function createPushNotificationTracker({
|
|
|
33
52
|
return;
|
|
34
53
|
}
|
|
35
54
|
|
|
55
|
+
if (message.method === "thread/goal/updated") {
|
|
56
|
+
void handleGoalUpdated(message);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (message.method === "thread/goal/cleared") {
|
|
61
|
+
if (message.threadId && goalStatusByThreadId.delete(message.threadId)) {
|
|
62
|
+
saveGoalPushState(goalPushStatePath, goalStatusByThreadId, logPrefix);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
36
67
|
rememberMessageContext(message);
|
|
37
68
|
clearFallbackSuppressionForNewRun(message);
|
|
38
69
|
|
|
@@ -73,6 +104,59 @@ function createPushNotificationTracker({
|
|
|
73
104
|
}
|
|
74
105
|
}
|
|
75
106
|
|
|
107
|
+
// Pushes goal lifecycle transitions into terminal/attention states so hours-long
|
|
108
|
+
// background goals still reach the user. Resume snapshots (first observation of a
|
|
109
|
+
// status) never notify; only live status changes do.
|
|
110
|
+
async function handleGoalUpdated({ threadId, params }) {
|
|
111
|
+
const goal = objectValue(params?.goal);
|
|
112
|
+
const status = readString(goal?.status);
|
|
113
|
+
const resolvedThreadId = threadId || readString(goal?.threadId);
|
|
114
|
+
if (!resolvedThreadId || !status) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const previousSnapshot = normalizeGoalPushSnapshot(goalStatusByThreadId.get(resolvedThreadId));
|
|
119
|
+
const nextSnapshot = {
|
|
120
|
+
status,
|
|
121
|
+
updatedAt: goal?.updatedAt ?? goal?.updated_at ?? null,
|
|
122
|
+
};
|
|
123
|
+
if (!goalStatusByThreadId.has(resolvedThreadId) && goalStatusByThreadId.size >= MAX_GOAL_STATUS_ENTRIES) {
|
|
124
|
+
const oldest = goalStatusByThreadId.keys().next().value;
|
|
125
|
+
goalStatusByThreadId.delete(oldest);
|
|
126
|
+
}
|
|
127
|
+
const isFirstObservation = previousSnapshot == null;
|
|
128
|
+
const isDuplicate = previousSnapshot?.status === nextSnapshot.status
|
|
129
|
+
&& previousSnapshot?.updatedAt === nextSnapshot.updatedAt;
|
|
130
|
+
const body = GOAL_PUSH_BODIES.get(status);
|
|
131
|
+
if (isFirstObservation || isDuplicate || !body || !pushServiceClient?.hasConfiguredBaseUrl) {
|
|
132
|
+
if (!isDuplicate) {
|
|
133
|
+
goalStatusByThreadId.set(resolvedThreadId, nextSnapshot);
|
|
134
|
+
saveGoalPushState(goalPushStatePath, goalStatusByThreadId, logPrefix);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const title = normalizePreviewText(threadTitleById.get(resolvedThreadId)) || "New Thread";
|
|
140
|
+
// The goal objective intentionally stays out of push payloads and logs.
|
|
141
|
+
try {
|
|
142
|
+
await pushServiceClient.notifyCompletion({
|
|
143
|
+
threadId: resolvedThreadId,
|
|
144
|
+
turnId: null,
|
|
145
|
+
result: status === "complete" ? "completed" : "failed",
|
|
146
|
+
title,
|
|
147
|
+
body,
|
|
148
|
+
// updatedAt keeps repeated legitimate transitions (blocked -> active -> blocked) notifiable.
|
|
149
|
+
dedupeKey: [sessionId || "", resolvedThreadId, "goal", status, goal?.updatedAt ?? ""].join("|"),
|
|
150
|
+
});
|
|
151
|
+
// Commit the dedupe cursor only after delivery succeeds so a repeated
|
|
152
|
+
// app-server snapshot can retry a transient push outage.
|
|
153
|
+
goalStatusByThreadId.set(resolvedThreadId, nextSnapshot);
|
|
154
|
+
saveGoalPushState(goalPushStatePath, goalStatusByThreadId, logPrefix);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error(`${logPrefix} goal push notify failed: ${error.message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
76
160
|
// Remembers thread/turn linkage before the terminal event arrives on a different payload shape.
|
|
77
161
|
function rememberMessageContext({ threadId, turnId, params, eventObject }) {
|
|
78
162
|
if (threadId && turnId) {
|
|
@@ -273,6 +357,56 @@ function createPushNotificationTracker({
|
|
|
273
357
|
};
|
|
274
358
|
}
|
|
275
359
|
|
|
360
|
+
// Best-effort disk persistence for goal statuses; failures must never break the bridge.
|
|
361
|
+
function loadGoalPushState(filePath, logPrefix) {
|
|
362
|
+
if (!filePath) {
|
|
363
|
+
return new Map();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
368
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
369
|
+
return new Map();
|
|
370
|
+
}
|
|
371
|
+
const entries = Object.entries(parsed)
|
|
372
|
+
.map(([threadId, snapshot]) => [threadId, normalizeGoalPushSnapshot(snapshot)])
|
|
373
|
+
.filter(([threadId, snapshot]) => typeof threadId === "string" && snapshot != null)
|
|
374
|
+
.slice(-MAX_GOAL_STATUS_ENTRIES);
|
|
375
|
+
return new Map(entries);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
if (error.code !== "ENOENT") {
|
|
378
|
+
console.error(`${logPrefix} failed to load goal push state: ${error.message}`);
|
|
379
|
+
}
|
|
380
|
+
return new Map();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function normalizeGoalPushSnapshot(value) {
|
|
385
|
+
if (typeof value === "string") {
|
|
386
|
+
return { status: value, updatedAt: null };
|
|
387
|
+
}
|
|
388
|
+
if (!value || typeof value !== "object" || typeof value.status !== "string") {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
status: value.status,
|
|
393
|
+
updatedAt: value.updatedAt ?? null,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function saveGoalPushState(filePath, goalStatusByThreadId, logPrefix) {
|
|
398
|
+
if (!filePath) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
404
|
+
fs.writeFileSync(filePath, JSON.stringify(Object.fromEntries(goalStatusByThreadId)));
|
|
405
|
+
} catch (error) {
|
|
406
|
+
console.error(`${logPrefix} failed to save goal push state: ${error.message}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
276
410
|
// Normalizes the message envelope once so downstream helpers can share the same parsed view.
|
|
277
411
|
function parseOutboundMessage(rawMessage, parsedMessage = null) {
|
|
278
412
|
const parsed = parsedMessage ?? safeParseJSON(rawMessage);
|
|
@@ -132,9 +132,17 @@ function createRolloutLiveMirrorController({
|
|
|
132
132
|
mirrorsByThreadId.clear();
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// The real turn id this mirror is actively tailing, or null. Lets the bridge
|
|
136
|
+
// answer the phone's turn-state probe from mirror truth when the bounded
|
|
137
|
+
// canonical page reads a busy run as closed.
|
|
138
|
+
function getActiveTurnId(threadId) {
|
|
139
|
+
return mirrorsByThreadId.get(threadId)?.getActiveTurnId() || null;
|
|
140
|
+
}
|
|
141
|
+
|
|
135
142
|
return {
|
|
136
143
|
observeInbound,
|
|
137
144
|
stopAll,
|
|
145
|
+
getActiveTurnId,
|
|
138
146
|
};
|
|
139
147
|
}
|
|
140
148
|
|
|
@@ -358,9 +366,30 @@ function createThreadRolloutLiveMirror({
|
|
|
358
366
|
onStop();
|
|
359
367
|
}
|
|
360
368
|
|
|
369
|
+
// Only a healthy, actively-tailed run with a real id counts: synthetic ids
|
|
370
|
+
// are not actionable app-server turn ids, and suppressed/awaiting states
|
|
371
|
+
// mean the mirror does not actually know what is running. While another live
|
|
372
|
+
// source owns the thread the tail keeps parsing with its emissions muted, so
|
|
373
|
+
// reporting that turn id would resurrect exactly the state the bridge muted.
|
|
374
|
+
function getActiveTurnId() {
|
|
375
|
+
if (
|
|
376
|
+
isStopped
|
|
377
|
+
|| wasSuppressed
|
|
378
|
+
|| state.isDesktopOrigin === false
|
|
379
|
+
|| state.awaitingCoherentBoundary
|
|
380
|
+
|| state.suppressLiveActivityUntilGrowth
|
|
381
|
+
|| state.activeTurnIdIsSynthetic
|
|
382
|
+
|| state.pendingSyntheticTerminalTurnId
|
|
383
|
+
) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
return state.activeTurnId || null;
|
|
387
|
+
}
|
|
388
|
+
|
|
361
389
|
return {
|
|
362
390
|
bump,
|
|
363
391
|
stop,
|
|
392
|
+
getActiveTurnId,
|
|
364
393
|
};
|
|
365
394
|
}
|
|
366
395
|
|
|
@@ -401,12 +430,30 @@ function bootstrapFromExistingRollout({
|
|
|
401
430
|
fsModule,
|
|
402
431
|
});
|
|
403
432
|
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
433
|
state.awaitingCoherentBoundary = true;
|
|
408
434
|
return;
|
|
409
435
|
}
|
|
436
|
+
if (!bootstrapWindow.coherent) {
|
|
437
|
+
// The active run starts outside the bounded bootstrap window. Do not emit
|
|
438
|
+
// a plausible-looking tail: canonical history remains the baseline. But do
|
|
439
|
+
// not go dark either — a long busy run would stop mirroring tool activity
|
|
440
|
+
// until its next turn boundary. Attach to the run in place instead, so
|
|
441
|
+
// growth from here on keeps streaming live.
|
|
442
|
+
const attached = attachToActiveRunFromTruncatedTail({
|
|
443
|
+
contents: bootstrapWindow.alignedContents,
|
|
444
|
+
boundary: bootstrapWindow.boundary,
|
|
445
|
+
state,
|
|
446
|
+
rolloutPath,
|
|
447
|
+
fsModule,
|
|
448
|
+
sendApplicationResponse,
|
|
449
|
+
nowMs,
|
|
450
|
+
staleActiveRunMaxAgeMs,
|
|
451
|
+
});
|
|
452
|
+
if (!attached) {
|
|
453
|
+
state.awaitingCoherentBoundary = true;
|
|
454
|
+
}
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
410
457
|
const { tailStart, contents: bootstrapContents } = bootstrapWindow;
|
|
411
458
|
let initialContents = bootstrapContents;
|
|
412
459
|
if (!initialContents) {
|
|
@@ -532,7 +579,8 @@ function bootstrapFromExistingRollout({
|
|
|
532
579
|
// Expands backwards only until the newest active task has its opening user
|
|
533
580
|
// message. Every expansion reads just the newly needed prefix, so a 30MB file
|
|
534
581
|
// is read at most once rather than once per retry. The hard cap keeps bootstrap
|
|
535
|
-
// work/memory bounded;
|
|
582
|
+
// work/memory bounded; a capped window without a coherent opener comes back
|
|
583
|
+
// with `coherent: false` and must never be replayed as history.
|
|
536
584
|
function readCoherentBootstrapWindow({ rolloutPath, fileSize, fsModule }) {
|
|
537
585
|
const maxBytes = Math.min(fileSize, DEFAULT_BOOTSTRAP_MAX_BYTES);
|
|
538
586
|
let windowBytes = Math.min(fileSize, DEFAULT_BOOTSTRAP_TAIL_BYTES);
|
|
@@ -549,10 +597,16 @@ function readCoherentBootstrapWindow({ rolloutPath, fileSize, fsModule }) {
|
|
|
549
597
|
// some legitimate system/continuation turns have no materialized user row.
|
|
550
598
|
// The opener requirement only protects a truncated tail.
|
|
551
599
|
if (!boundary.hasActiveRun || boundary.hasOpeningUser || tailStart === 0) {
|
|
552
|
-
return { tailStart, contents };
|
|
600
|
+
return { tailStart, contents, coherent: true };
|
|
553
601
|
}
|
|
554
602
|
if (windowBytes >= maxBytes || tailStart === 0) {
|
|
555
|
-
return
|
|
603
|
+
return {
|
|
604
|
+
tailStart,
|
|
605
|
+
contents,
|
|
606
|
+
coherent: false,
|
|
607
|
+
alignedContents,
|
|
608
|
+
boundary,
|
|
609
|
+
};
|
|
556
610
|
}
|
|
557
611
|
|
|
558
612
|
const nextWindowBytes = Math.min(maxBytes, windowBytes * 2);
|
|
@@ -584,12 +638,21 @@ function inspectBootstrapRunBoundary(contents) {
|
|
|
584
638
|
// closing terminal is evidence of an unknown active boundary, not permission
|
|
585
639
|
// to replay a partial conversation.
|
|
586
640
|
let unboundedActivitySinceTerminal = false;
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
641
|
+
// Attach metadata for the incoherent-window case, so the caller never has to
|
|
642
|
+
// re-parse the (up to 64MB) window a second time.
|
|
643
|
+
let newestTaskStartedLineIndex = -1;
|
|
644
|
+
let lastEntryTimestamp = "";
|
|
645
|
+
|
|
646
|
+
const lines = contents.split("\n");
|
|
647
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
648
|
+
const parsed = safeParseJSON(lines[lineIndex].trim());
|
|
590
649
|
if (!parsed) {
|
|
591
650
|
continue;
|
|
592
651
|
}
|
|
652
|
+
const entryTimestamp = readString(parsed.timestamp);
|
|
653
|
+
if (entryTimestamp) {
|
|
654
|
+
lastEntryTimestamp = entryTimestamp;
|
|
655
|
+
}
|
|
593
656
|
const taskEventType = parsed?.type === "event_msg"
|
|
594
657
|
? readString(parsed?.payload?.type)
|
|
595
658
|
: "";
|
|
@@ -611,6 +674,7 @@ function inspectBootstrapRunBoundary(contents) {
|
|
|
611
674
|
hasOpeningUser = pendingUserBeforeStart;
|
|
612
675
|
hasTurnOutputSinceStart = false;
|
|
613
676
|
pendingUserBeforeStart = false;
|
|
677
|
+
newestTaskStartedLineIndex = lineIndex;
|
|
614
678
|
continue;
|
|
615
679
|
}
|
|
616
680
|
if (!activeTurnId) {
|
|
@@ -653,6 +717,8 @@ function inspectBootstrapRunBoundary(contents) {
|
|
|
653
717
|
return {
|
|
654
718
|
hasActiveRun: Boolean(activeTurnId) || unboundedActivitySinceTerminal,
|
|
655
719
|
hasOpeningUser,
|
|
720
|
+
newestTaskStartedLineIndex,
|
|
721
|
+
lastEntryTimestamp,
|
|
656
722
|
};
|
|
657
723
|
}
|
|
658
724
|
|
|
@@ -667,6 +733,58 @@ function isBootstrapNeutralRecord(entry, taskEventType = "") {
|
|
|
667
733
|
|| taskEventType === "context_updated";
|
|
668
734
|
}
|
|
669
735
|
|
|
736
|
+
// Attaches mid-run when the active run's opener is beyond the bounded window:
|
|
737
|
+
// nothing already in the tail is emitted (it stays canonical-history
|
|
738
|
+
// territory), but run state is hydrated so subsequent growth mirrors live.
|
|
739
|
+
// Returns false when the tail proves the visible runs all closed — trailing
|
|
740
|
+
// bytes then belong to an unknown older boundary and stay suppressed.
|
|
741
|
+
function attachToActiveRunFromTruncatedTail({
|
|
742
|
+
contents,
|
|
743
|
+
boundary,
|
|
744
|
+
state,
|
|
745
|
+
rolloutPath,
|
|
746
|
+
fsModule,
|
|
747
|
+
sendApplicationResponse,
|
|
748
|
+
nowMs,
|
|
749
|
+
staleActiveRunMaxAgeMs,
|
|
750
|
+
}) {
|
|
751
|
+
const newestTaskStartedIndex = boundary?.newestTaskStartedLineIndex ?? -1;
|
|
752
|
+
if (newestTaskStartedIndex >= 0) {
|
|
753
|
+
// Hydrate through the shared reducer so parallel-turn and terminal
|
|
754
|
+
// semantics stay authoritative for what is still open at EOF.
|
|
755
|
+
processRolloutLines(contents.split("\n").slice(newestTaskStartedIndex), state, () => {});
|
|
756
|
+
if (!state.activeTurnId) {
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
} else {
|
|
760
|
+
// Mid-turn tail without its task_started: adopt a synthetic turn. The
|
|
761
|
+
// first non-terminal event carrying the real id promotes it, and a
|
|
762
|
+
// mismatched terminal closes it via the synthetic-terminal path.
|
|
763
|
+
state.activeTurnId = buildSyntheticTurnId(state, { timestamp: boundary?.lastEntryTimestamp || "" });
|
|
764
|
+
state.activeTurnIdIsSynthetic = true;
|
|
765
|
+
state.reasoningItemId = buildSyntheticItemId("thinking", state.threadId, state.activeTurnId);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (isRolloutFileStale(rolloutPath, fsModule, nowMs, staleActiveRunMaxAgeMs)) {
|
|
769
|
+
// Same contract as the stale coherent bootstrap: stay silent until real
|
|
770
|
+
// growth proves the desktop process is alive again.
|
|
771
|
+
state.suppressLiveActivityUntilGrowth = true;
|
|
772
|
+
return true;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// A hydrated run that already carries a pending synthetic terminal is
|
|
776
|
+
// closing, not running: announcing it as live would just be followed by the
|
|
777
|
+
// tick's synthetic turn/completed one grace period later.
|
|
778
|
+
if (!state.pendingSyntheticTerminalTurnId) {
|
|
779
|
+
sendApplicationResponse(JSON.stringify(createNotification("turn/activity", {
|
|
780
|
+
threadId: state.threadId,
|
|
781
|
+
turnId: state.activeTurnId,
|
|
782
|
+
id: state.activeTurnId,
|
|
783
|
+
})));
|
|
784
|
+
}
|
|
785
|
+
return true;
|
|
786
|
+
}
|
|
787
|
+
|
|
670
788
|
// After a bounded bootstrap cannot reach the old opener, consume only new
|
|
671
789
|
// bytes. A later real user+task_started boundary safely starts a new live run;
|
|
672
790
|
// everything before it remains canonical-history territory.
|
|
@@ -799,6 +917,26 @@ function synthesizeNotificationsFromRolloutEntry(entry, state, { nowMs = Date.no
|
|
|
799
917
|
const payload = entry.payload || {};
|
|
800
918
|
const eventType = readString(payload.type);
|
|
801
919
|
|
|
920
|
+
if (eventType === "thread_goal_updated") {
|
|
921
|
+
const goal = payload.goal && typeof payload.goal === "object" ? payload.goal : null;
|
|
922
|
+
const threadId = readString(payload.threadId) || readString(goal?.threadId) || state.threadId;
|
|
923
|
+
if (!goal || !threadId) {
|
|
924
|
+
return [];
|
|
925
|
+
}
|
|
926
|
+
return [createNotification("thread/goal/updated", {
|
|
927
|
+
threadId,
|
|
928
|
+
turnId: readString(payload.turnId) || readString(payload.turn_id) || null,
|
|
929
|
+
goal,
|
|
930
|
+
})];
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (eventType === "thread_goal_cleared") {
|
|
934
|
+
const threadId = readString(payload.threadId) || readString(payload.thread_id) || state.threadId;
|
|
935
|
+
return threadId
|
|
936
|
+
? [createNotification("thread/goal/cleared", { threadId })]
|
|
937
|
+
: [];
|
|
938
|
+
}
|
|
939
|
+
|
|
802
940
|
if (eventType === "task_started") {
|
|
803
941
|
notifications.push(...finalizePendingSyntheticTerminal(state));
|
|
804
942
|
const explicitTurnId = readString(payload.turn_id) || readString(payload.turnId);
|
|
@@ -952,7 +1090,7 @@ function synthesizeNotificationsFromRolloutEntry(entry, state, { nowMs = Date.no
|
|
|
952
1090
|
return notifications;
|
|
953
1091
|
}
|
|
954
1092
|
|
|
955
|
-
if (itemType === "functioncalloutput") {
|
|
1093
|
+
if (itemType === "functioncalloutput" || itemType === "customtoolcalloutput") {
|
|
956
1094
|
notifications.push(...toolOutputNotifications(state, payload));
|
|
957
1095
|
return notifications;
|
|
958
1096
|
}
|
|
@@ -1316,6 +1454,16 @@ function customToolStartNotifications(state, payload) {
|
|
|
1316
1454
|
return notifications;
|
|
1317
1455
|
}
|
|
1318
1456
|
|
|
1457
|
+
// Custom tool calls settle through custom_tool_call_output. Without tracking
|
|
1458
|
+
// them the activity row never completes, so it lingers between command groups.
|
|
1459
|
+
if (!isCommandToolName(toolName) && !state.applyPatchCalls.has(callId)) {
|
|
1460
|
+
state.commandCalls.set(callId, {
|
|
1461
|
+
toolName,
|
|
1462
|
+
command: toolName,
|
|
1463
|
+
cwd: readString(state.sessionMeta?.cwd) || "",
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1319
1467
|
return [
|
|
1320
1468
|
...notifications,
|
|
1321
1469
|
createNotification("codex/event/background_event", {
|
|
@@ -1776,8 +1924,19 @@ function genericToolActivityMessage(toolName) {
|
|
|
1776
1924
|
}
|
|
1777
1925
|
}
|
|
1778
1926
|
|
|
1927
|
+
// Mirrors the wording of genericToolActivityMessage so the completion line
|
|
1928
|
+
// supersedes the start line instead of stacking a second row beside it.
|
|
1779
1929
|
function genericToolCompletionMessage(toolName) {
|
|
1780
|
-
|
|
1930
|
+
switch (readString(toolName).toLowerCase()) {
|
|
1931
|
+
case "apply_patch":
|
|
1932
|
+
return "Applied patch";
|
|
1933
|
+
case "write_stdin":
|
|
1934
|
+
return "Wrote to terminal";
|
|
1935
|
+
case "read_thread_terminal":
|
|
1936
|
+
return "Read terminal output";
|
|
1937
|
+
default:
|
|
1938
|
+
return `Completed ${readString(toolName)}`;
|
|
1939
|
+
}
|
|
1781
1940
|
}
|
|
1782
1941
|
|
|
1783
1942
|
function createNotification(method, params = {}) {
|
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
isContextualUserText,
|
|
11
11
|
isUserRoleItem,
|
|
12
12
|
responseItemMessageText: sharedResponseItemMessageText,
|
|
13
|
+
sanitizeUserRoleItem,
|
|
13
14
|
visibleUserPromptText,
|
|
14
15
|
} = require("./desktop-ipc-shared");
|
|
15
16
|
|
|
@@ -862,7 +863,10 @@ function normalizeResponseItemForHistory(payload, lineNumber, { cwd = "", toolCa
|
|
|
862
863
|
item.role = "assistant";
|
|
863
864
|
}
|
|
864
865
|
|
|
865
|
-
|
|
866
|
+
// A single user item can carry injected context next to the real request.
|
|
867
|
+
// Sanitize here so history readers (including the thread/read JSONL merge,
|
|
868
|
+
// which runs after the relay sanitizer) never rebuild the hidden fragments.
|
|
869
|
+
return isUserRoleItem(item) ? sanitizeUserRoleItem(item) : item;
|
|
866
870
|
}
|
|
867
871
|
|
|
868
872
|
function applyHistoryAssistantSourceAlias(item, turnId, occurrencesByBaseKey = new Map()) {
|