@makerbi/remodex 3.2.0 → 3.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 +160 -30
- package/src/codex-desktop-refresher.js +8 -8
- package/src/codex-runtime-settings.js +113 -0
- package/src/desktop-ipc-action-follower.js +289 -95
- package/src/desktop-ipc-conversation-adapter.js +56 -51
- package/src/desktop-ipc-conversation-projector.js +1 -0
- package/src/desktop-ipc-live-owner.js +189 -164
- package/src/desktop-ipc-shared.js +35 -1
- package/src/git-handler.js +20 -17
- package/src/rollout-live-mirror.js +3 -4
- package/src/runtime-provider-router.js +10 -2
- package/src/thread-activity-projector.js +694 -0
- package/src/thread-activity-store.js +325 -0
- package/src/thread-runtime-settings-store.js +60 -74
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
// FILE: thread-activity-projector.js
|
|
2
|
+
// Purpose: Reduces observed Codex and Desktop state to bounded Activity metadata.
|
|
3
|
+
// Layer: CLI helper
|
|
4
|
+
// Exports: createThreadActivityProjector, projectDesktopThreadActivity
|
|
5
|
+
// Depends on: ./desktop-ipc-shared
|
|
6
|
+
|
|
7
|
+
const { normalizeToken, readString } = require("./desktop-ipc-shared");
|
|
8
|
+
|
|
9
|
+
const APP_SERVER_SOURCE = "app-server";
|
|
10
|
+
const DESKTOP_IPC_SOURCE = "desktop-ipc";
|
|
11
|
+
const APP_SERVER_GENERATION = 1;
|
|
12
|
+
const MAX_APP_SERVER_THREADS = 500;
|
|
13
|
+
const MAX_TERMINAL_TURN_IDS = 64;
|
|
14
|
+
const MAX_DISPLAY_TEXT_CHARS = 160;
|
|
15
|
+
|
|
16
|
+
const APPROVAL_METHODS = new Set([
|
|
17
|
+
"item/commandExecution/requestApproval",
|
|
18
|
+
"item/fileChange/requestApproval",
|
|
19
|
+
"item/fileRead/requestApproval",
|
|
20
|
+
"item/permissions/requestApproval",
|
|
21
|
+
]);
|
|
22
|
+
const USER_INPUT_METHODS = new Set([
|
|
23
|
+
"item/tool/requestUserInput",
|
|
24
|
+
"tool/requestUserInput",
|
|
25
|
+
"mcpServer/elicitation/request",
|
|
26
|
+
]);
|
|
27
|
+
const REMOVAL_METHODS = new Set([
|
|
28
|
+
"thread/archived",
|
|
29
|
+
"thread/deleted",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
function createThreadActivityProjector({ maxAppThreads = MAX_APP_SERVER_THREADS } = {}) {
|
|
33
|
+
const appStatesByThreadId = new Map();
|
|
34
|
+
|
|
35
|
+
function observeAppServer(message) {
|
|
36
|
+
const method = readString(message?.method);
|
|
37
|
+
const threadId = appServerThreadId(message);
|
|
38
|
+
if (!method || !threadId) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (REMOVAL_METHODS.has(method)) {
|
|
42
|
+
appStatesByThreadId.delete(threadId);
|
|
43
|
+
return { removedThreadId: threadId, source: APP_SERVER_SOURCE };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const state = appStatesByThreadId.get(threadId) || createAppServerState(threadId);
|
|
47
|
+
if (!reduceAppServerMessage(state, message)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
rememberAppServerState(appStatesByThreadId, state, maxAppThreads);
|
|
51
|
+
return { entry: projectAppServerState(state) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function forget(threadId, source) {
|
|
55
|
+
if (source === APP_SERVER_SOURCE) {
|
|
56
|
+
appStatesByThreadId.delete(threadId);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
forget,
|
|
62
|
+
observeAppServer,
|
|
63
|
+
projectDesktopState(threadId, state, sourceGeneration) {
|
|
64
|
+
return projectDesktopThreadActivity(threadId, state, sourceGeneration);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createAppServerState(threadId) {
|
|
70
|
+
return {
|
|
71
|
+
threadId,
|
|
72
|
+
title: "",
|
|
73
|
+
cwd: "",
|
|
74
|
+
runtimeStatus: "unknown",
|
|
75
|
+
activeFlags: [],
|
|
76
|
+
freshness: "current",
|
|
77
|
+
activeTurnIds: new Set(),
|
|
78
|
+
runningWithoutTurnId: false,
|
|
79
|
+
approvalRequestIds: new Map(),
|
|
80
|
+
userInputRequestIds: new Map(),
|
|
81
|
+
terminalTurnIds: new Set(),
|
|
82
|
+
settledTurnlessWork: false,
|
|
83
|
+
turnOrderById: new Map(),
|
|
84
|
+
turnStartedAtMsById: new Map(),
|
|
85
|
+
fallbackStartedAtMs: null,
|
|
86
|
+
nextTurnOrder: 1,
|
|
87
|
+
lastOutcomeOrder: 0,
|
|
88
|
+
lastOutcome: null,
|
|
89
|
+
latestItem: null,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function reduceAppServerMessage(state, message) {
|
|
94
|
+
const method = readString(message.method);
|
|
95
|
+
if (APPROVAL_METHODS.has(method)) {
|
|
96
|
+
return rememberRequest(state, state.approvalRequestIds, message);
|
|
97
|
+
}
|
|
98
|
+
if (USER_INPUT_METHODS.has(method)) {
|
|
99
|
+
return rememberRequest(state, state.userInputRequestIds, message);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
switch (method) {
|
|
103
|
+
case "thread/started":
|
|
104
|
+
return reduceThreadStarted(state, message.params?.thread);
|
|
105
|
+
case "thread/name/updated":
|
|
106
|
+
return replaceTitle(state, message.params);
|
|
107
|
+
case "thread/status/changed":
|
|
108
|
+
return replaceAppServerRuntime(state, message.params?.status);
|
|
109
|
+
case "thread/closed":
|
|
110
|
+
state.freshness = "stale";
|
|
111
|
+
return true;
|
|
112
|
+
case "turn/started":
|
|
113
|
+
return reduceTurnStarted(state, message.params || {});
|
|
114
|
+
case "turn/completed":
|
|
115
|
+
return reduceTurnCompleted(state, message.params || {});
|
|
116
|
+
case "item/started":
|
|
117
|
+
case "item/completed":
|
|
118
|
+
return reduceItemLifecycle(state, message.params || {}, method);
|
|
119
|
+
case "serverRequest/resolved":
|
|
120
|
+
return resolveRequest(state, message.params || {});
|
|
121
|
+
default:
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function reduceThreadStarted(state, thread) {
|
|
127
|
+
if (!thread || typeof thread !== "object") {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
let changed = replaceString(state, "title", thread.name || thread.title || thread.preview);
|
|
131
|
+
changed = replaceString(state, "cwd", thread.cwd) || changed;
|
|
132
|
+
changed = replaceAppServerRuntime(state, thread.status) || changed;
|
|
133
|
+
return changed;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function replaceTitle(state, params) {
|
|
137
|
+
return replaceString(
|
|
138
|
+
state,
|
|
139
|
+
"title",
|
|
140
|
+
params?.threadName || params?.thread_name || params?.name || params?.title
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function replaceAppServerRuntime(state, status) {
|
|
145
|
+
const nextRuntime = normalizeRuntime(status);
|
|
146
|
+
if (nextRuntime === "active"
|
|
147
|
+
&& hasKnownTerminalWork(state)
|
|
148
|
+
&& !hasRunningWork(state)) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
const flags = runtimeActiveFlags(status);
|
|
152
|
+
const changed = state.runtimeStatus !== nextRuntime
|
|
153
|
+
|| !sameJSON(state.activeFlags, flags) || state.freshness !== "current";
|
|
154
|
+
state.runtimeStatus = nextRuntime;
|
|
155
|
+
state.activeFlags = flags;
|
|
156
|
+
state.freshness = "current";
|
|
157
|
+
if (nextRuntime === "active" && !hasRunningWork(state)) {
|
|
158
|
+
state.runningWithoutTurnId = true;
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
return changed;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function reduceTurnStarted(state, params) {
|
|
165
|
+
const turn = params.turn && typeof params.turn === "object" ? params.turn : {};
|
|
166
|
+
const turnId = turnIdentity(params, turn);
|
|
167
|
+
if (turnId && state.terminalTurnIds.has(turnId)) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let changed = false;
|
|
172
|
+
const startedAtMs = timestampSecondsToMs(turn.startedAt ?? turn.started_at);
|
|
173
|
+
if (!hasRunningWork(state)) {
|
|
174
|
+
state.latestItem = null;
|
|
175
|
+
}
|
|
176
|
+
state.freshness = "current";
|
|
177
|
+
if (turnId) {
|
|
178
|
+
if (!state.activeTurnIds.has(turnId)) {
|
|
179
|
+
state.activeTurnIds.add(turnId);
|
|
180
|
+
state.turnOrderById.set(turnId, state.nextTurnOrder);
|
|
181
|
+
state.nextTurnOrder += 1;
|
|
182
|
+
changed = true;
|
|
183
|
+
}
|
|
184
|
+
const canonicalStart = startedAtMs ?? state.fallbackStartedAtMs;
|
|
185
|
+
if (canonicalStart != null && state.turnStartedAtMsById.get(turnId) !== canonicalStart) {
|
|
186
|
+
state.turnStartedAtMsById.set(turnId, canonicalStart);
|
|
187
|
+
changed = true;
|
|
188
|
+
}
|
|
189
|
+
if (state.runningWithoutTurnId) {
|
|
190
|
+
state.runningWithoutTurnId = false;
|
|
191
|
+
state.fallbackStartedAtMs = null;
|
|
192
|
+
changed = true;
|
|
193
|
+
}
|
|
194
|
+
} else if (!state.runningWithoutTurnId) {
|
|
195
|
+
state.runningWithoutTurnId = true;
|
|
196
|
+
changed = true;
|
|
197
|
+
}
|
|
198
|
+
if (!turnId && startedAtMs != null && state.fallbackStartedAtMs !== startedAtMs) {
|
|
199
|
+
state.fallbackStartedAtMs = startedAtMs;
|
|
200
|
+
changed = true;
|
|
201
|
+
}
|
|
202
|
+
if (state.runtimeStatus !== "active") {
|
|
203
|
+
state.runtimeStatus = "active";
|
|
204
|
+
changed = true;
|
|
205
|
+
}
|
|
206
|
+
return changed;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function reduceTurnCompleted(state, params) {
|
|
210
|
+
const turn = params.turn && typeof params.turn === "object" ? params.turn : {};
|
|
211
|
+
const turnId = turnIdentity(params, turn);
|
|
212
|
+
if (turnId && state.terminalTurnIds.has(turnId)) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
if (turnId && !state.activeTurnIds.has(turnId) && state.activeTurnIds.size > 0) {
|
|
216
|
+
rememberTerminalTurn(state, turnId);
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
if (!turnId && !state.runningWithoutTurnId && hasKnownTerminalWork(state)) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const startedAtMs = state.turnStartedAtMsById.get(turnId) ?? state.fallbackStartedAtMs;
|
|
224
|
+
let changed = settleTurnRuntime(state, turnId);
|
|
225
|
+
changed = settleRequests(state, turnId) || changed;
|
|
226
|
+
state.freshness = "current";
|
|
227
|
+
if (!turnId) {
|
|
228
|
+
state.settledTurnlessWork = true;
|
|
229
|
+
return changed;
|
|
230
|
+
}
|
|
231
|
+
const order = state.turnOrderById.get(turnId) || state.nextTurnOrder++;
|
|
232
|
+
state.turnOrderById.set(turnId, order);
|
|
233
|
+
rememberTerminalTurn(state, turnId);
|
|
234
|
+
if (order < state.lastOutcomeOrder) {
|
|
235
|
+
return changed;
|
|
236
|
+
}
|
|
237
|
+
const nextOutcome = projectTurnOutcome(turnId, turn, params, startedAtMs);
|
|
238
|
+
if (!sameJSON(state.lastOutcome, nextOutcome)) {
|
|
239
|
+
state.lastOutcome = nextOutcome;
|
|
240
|
+
state.lastOutcomeOrder = order;
|
|
241
|
+
changed = true;
|
|
242
|
+
}
|
|
243
|
+
return changed;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function settleTurnRuntime(state, turnId) {
|
|
247
|
+
let changed = false;
|
|
248
|
+
if (turnId && state.activeTurnIds.delete(turnId)) {
|
|
249
|
+
changed = true;
|
|
250
|
+
}
|
|
251
|
+
if (state.runningWithoutTurnId && (!turnId || state.activeTurnIds.size === 0)) {
|
|
252
|
+
state.runningWithoutTurnId = false;
|
|
253
|
+
state.fallbackStartedAtMs = null;
|
|
254
|
+
changed = true;
|
|
255
|
+
}
|
|
256
|
+
if (!hasRunningWork(state) && state.runtimeStatus === "active") {
|
|
257
|
+
state.runtimeStatus = "idle";
|
|
258
|
+
changed = true;
|
|
259
|
+
}
|
|
260
|
+
if (!hasRunningWork(state)) {
|
|
261
|
+
state.activeFlags = [];
|
|
262
|
+
}
|
|
263
|
+
return changed;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function settleRequests(state, turnId) {
|
|
267
|
+
let changed = false;
|
|
268
|
+
for (const requests of [state.approvalRequestIds, state.userInputRequestIds]) {
|
|
269
|
+
for (const [requestId, requestTurnId] of requests) {
|
|
270
|
+
if ((turnId && requestTurnId === turnId) || !hasRunningWork(state)) {
|
|
271
|
+
requests.delete(requestId);
|
|
272
|
+
changed = true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return changed;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function reduceItemLifecycle(state, params, method) {
|
|
280
|
+
const item = params.item && typeof params.item === "object" ? params.item : {};
|
|
281
|
+
const turnId = readString(params.turnId) || readString(params.turn_id);
|
|
282
|
+
if (turnId && state.terminalTurnIds.has(turnId)) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
if (!turnId && hasKnownTerminalWork(state) && !hasRunningWork(state)) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
let nextItem = projectSemanticItem(item, {
|
|
289
|
+
turnId,
|
|
290
|
+
lifecycleAtMs: method === "item/started"
|
|
291
|
+
? finiteNumber(params.startedAtMs ?? params.started_at_ms)
|
|
292
|
+
: finiteNumber(params.completedAtMs ?? params.completed_at_ms),
|
|
293
|
+
lifecycle: method === "item/started" ? "started" : "completed",
|
|
294
|
+
});
|
|
295
|
+
if (method === "item/completed" && state.latestItem && nextItem) {
|
|
296
|
+
if (state.latestItem.itemId !== nextItem.itemId || state.latestItem.turnId !== nextItem.turnId) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
nextItem = { ...state.latestItem, ...nextItem };
|
|
300
|
+
}
|
|
301
|
+
if (!nextItem || sameJSON(state.latestItem, nextItem)) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
state.latestItem = nextItem;
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function rememberRequest(state, requestIds, message) {
|
|
309
|
+
if (message.id == null || requestIds.has(message.id)) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
const turnId = turnIdentity(message.params, message.params?.turn);
|
|
313
|
+
if ((turnId && state.terminalTurnIds.has(turnId))
|
|
314
|
+
|| (!turnId && hasKnownTerminalWork(state) && !hasRunningWork(state))) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
requestIds.set(message.id, turnId);
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function resolveRequest(state, params) {
|
|
322
|
+
const requestId = params.requestId ?? params.request_id;
|
|
323
|
+
if (requestId == null) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
const removedApproval = state.approvalRequestIds.delete(requestId);
|
|
327
|
+
const removedUserInput = state.userInputRequestIds.delete(requestId);
|
|
328
|
+
return removedApproval || removedUserInput;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function projectAppServerState(state) {
|
|
332
|
+
return compactEntry({
|
|
333
|
+
threadId: state.threadId,
|
|
334
|
+
source: APP_SERVER_SOURCE,
|
|
335
|
+
title: state.title,
|
|
336
|
+
cwd: state.cwd,
|
|
337
|
+
runtime: runtimeForAppServerState(state),
|
|
338
|
+
activeTurnIds: [...state.activeTurnIds],
|
|
339
|
+
runningWithoutTurnId: state.runningWithoutTurnId,
|
|
340
|
+
activeTurns: [...state.activeTurnIds].map((turnId) => compactObject({
|
|
341
|
+
turnId,
|
|
342
|
+
startedAtMs: state.turnStartedAtMsById.get(turnId),
|
|
343
|
+
})),
|
|
344
|
+
runningStartedAtMs: state.runningWithoutTurnId ? state.fallbackStartedAtMs : null,
|
|
345
|
+
...attentionFields(state.approvalRequestIds.size, state.userInputRequestIds.size, state.activeFlags),
|
|
346
|
+
lastOutcome: state.lastOutcome,
|
|
347
|
+
latestItem: state.latestItem,
|
|
348
|
+
freshness: state.freshness,
|
|
349
|
+
sourceGeneration: APP_SERVER_GENERATION,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function projectDesktopThreadActivity(threadId, state, sourceGeneration) {
|
|
354
|
+
const rawState = state && typeof state === "object" ? state : {};
|
|
355
|
+
const turns = Array.isArray(rawState.turns) ? rawState.turns : [];
|
|
356
|
+
const requests = Array.isArray(rawState.requests) ? rawState.requests : [];
|
|
357
|
+
const activeTurnIds = [];
|
|
358
|
+
const activeTurns = [];
|
|
359
|
+
let runningWithoutTurnId = false;
|
|
360
|
+
for (const turn of turns) {
|
|
361
|
+
if (!isActiveStatus(turn?.status)) {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const turnId = desktopTurnId(turn);
|
|
365
|
+
if (turnId) {
|
|
366
|
+
activeTurnIds.push(turnId);
|
|
367
|
+
activeTurns.push(compactObject({ turnId, startedAtMs: desktopTimestampMs(turn, "started") }));
|
|
368
|
+
} else {
|
|
369
|
+
runningWithoutTurnId = true;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const approvalCount = requests.filter(isPendingApprovalRequest).length;
|
|
373
|
+
const userInputCount = requests.filter(isPendingUserInputRequest).length;
|
|
374
|
+
const runtimeStatus = rawState.threadRuntimeStatus || rawState.status;
|
|
375
|
+
// Explicit runtime idle can invalidate stale inProgress history, but cannot
|
|
376
|
+
// turn that history into evidence of successful completion.
|
|
377
|
+
const explicitIdle = normalizeRuntime(runtimeStatus) === "idle";
|
|
378
|
+
if (explicitIdle) {
|
|
379
|
+
activeTurnIds.length = 0;
|
|
380
|
+
activeTurns.length = 0;
|
|
381
|
+
runningWithoutTurnId = false;
|
|
382
|
+
}
|
|
383
|
+
const runtime = activeTurnIds.length > 0 || runningWithoutTurnId
|
|
384
|
+
? "active"
|
|
385
|
+
: normalizeRuntime(runtimeStatus);
|
|
386
|
+
runningWithoutTurnId ||= runtime === "active" && activeTurnIds.length === 0;
|
|
387
|
+
return compactEntry({
|
|
388
|
+
threadId,
|
|
389
|
+
source: DESKTOP_IPC_SOURCE,
|
|
390
|
+
title: readString(rawState.title) || readString(rawState.name),
|
|
391
|
+
cwd: readString(rawState.cwd) || readString(rawState.current_working_directory),
|
|
392
|
+
runtime,
|
|
393
|
+
activeTurnIds,
|
|
394
|
+
activeTurns,
|
|
395
|
+
runningWithoutTurnId,
|
|
396
|
+
...attentionFields(approvalCount, userInputCount, runtimeActiveFlags(runtimeStatus)),
|
|
397
|
+
desktopUnread: projectDesktopUnread(rawState),
|
|
398
|
+
lastOutcome: projectLatestDesktopOutcome(turns),
|
|
399
|
+
latestItem: projectLatestDesktopItem(turns, runtime),
|
|
400
|
+
freshness: "current",
|
|
401
|
+
sourceGeneration: positiveInteger(sourceGeneration),
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function projectLatestDesktopOutcome(turns) {
|
|
406
|
+
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
407
|
+
const turn = turns[index];
|
|
408
|
+
const turnId = desktopTurnId(turn);
|
|
409
|
+
const outcome = normalizeOutcome(turn?.status, turn?.error);
|
|
410
|
+
if (!turnId || !outcome) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
return compactObject({
|
|
414
|
+
turnId,
|
|
415
|
+
outcome,
|
|
416
|
+
startedAtMs: desktopTimestampMs(turn, "started"),
|
|
417
|
+
completedAtMs: desktopTimestampMs(turn, "completed"),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function projectLatestDesktopItem(turns, runtime) {
|
|
424
|
+
const turn = (runtime === "active" && turns.findLast((candidate) => isActiveStatus(candidate?.status)))
|
|
425
|
+
|| turns.at(-1);
|
|
426
|
+
const items = Array.isArray(turn?.items) ? turn.items : [];
|
|
427
|
+
for (let itemIndex = items.length - 1; itemIndex >= 0; itemIndex -= 1) {
|
|
428
|
+
const item = projectSemanticItem(items[itemIndex], {
|
|
429
|
+
turnId: desktopTurnId(turn),
|
|
430
|
+
});
|
|
431
|
+
if (item) {
|
|
432
|
+
return item;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function projectSemanticItem(item, { turnId = "", lifecycle = "", lifecycleAtMs = null } = {}) {
|
|
439
|
+
const itemId = readString(item?.id) || readString(item?.itemId) || readString(item?.item_id);
|
|
440
|
+
const descriptor = semanticItemDescriptor(item?.type);
|
|
441
|
+
if (!itemId || !descriptor) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
const timingKey = lifecycle === "started"
|
|
445
|
+
? "startedAtMs"
|
|
446
|
+
: lifecycle === "completed" ? "completedAtMs" : "";
|
|
447
|
+
return compactObject({
|
|
448
|
+
itemId,
|
|
449
|
+
turnId,
|
|
450
|
+
kind: descriptor.kind,
|
|
451
|
+
label: descriptor.label,
|
|
452
|
+
...(timingKey && lifecycleAtMs != null ? { [timingKey]: lifecycleAtMs } : {}),
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function semanticItemDescriptor(type) {
|
|
457
|
+
const token = normalizeToken(type);
|
|
458
|
+
if (!token || token === "usermessage" || token === "hookprompt") {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
if (token === "reasoning" || token === "plan" || token === "todolist") {
|
|
462
|
+
return { kind: "thinking", label: "Thinking" };
|
|
463
|
+
}
|
|
464
|
+
if (token.includes("command") || token.includes("exec")) {
|
|
465
|
+
return { kind: "command", label: "Running command" };
|
|
466
|
+
}
|
|
467
|
+
if (token.includes("filechange") || token.includes("patch") || token.includes("apply")) {
|
|
468
|
+
return { kind: "fileChange", label: "Editing files" };
|
|
469
|
+
}
|
|
470
|
+
if (token === "agentmessage" || token === "assistantmessage" || token === "message") {
|
|
471
|
+
return { kind: "response", label: "Writing response" };
|
|
472
|
+
}
|
|
473
|
+
return { kind: "tool", label: "Using a tool" };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function projectTurnOutcome(turnId, turn, params, startedAtMs = null) {
|
|
477
|
+
return compactObject({
|
|
478
|
+
turnId,
|
|
479
|
+
outcome: normalizeOutcome(turn.status || params.status, turn.error || params.error) || "completed",
|
|
480
|
+
startedAtMs: timestampSecondsToMs(turn.startedAt ?? turn.started_at) ?? startedAtMs,
|
|
481
|
+
completedAtMs: timestampSecondsToMs(turn.completedAt ?? turn.completed_at),
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function projectDesktopUnread(state) {
|
|
486
|
+
const hasUnreadField = typeof state.hasUnreadTurn === "boolean"
|
|
487
|
+
|| typeof state.has_unread_turn === "boolean";
|
|
488
|
+
const rawCount = state.unreadMessageCount ?? state.unread_message_count;
|
|
489
|
+
const normalizedCount = finiteNumber(rawCount);
|
|
490
|
+
const hasCountField = normalizedCount != null;
|
|
491
|
+
if (!hasUnreadField && !hasCountField) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
const unreadMessageCount = hasCountField ? Math.max(0, Math.floor(normalizedCount)) : 0;
|
|
495
|
+
return {
|
|
496
|
+
hasUnreadTurn: Boolean(state.hasUnreadTurn ?? state.has_unread_turn) || unreadMessageCount > 0,
|
|
497
|
+
unreadMessageCount,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function runtimeForAppServerState(state) {
|
|
502
|
+
if (hasRunningWork(state)) {
|
|
503
|
+
return "active";
|
|
504
|
+
}
|
|
505
|
+
return state.runtimeStatus === "active" ? "unknown" : state.runtimeStatus;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function normalizeRuntime(status) {
|
|
509
|
+
const token = normalizeToken(typeof status === "object" ? status?.type : status);
|
|
510
|
+
if (token === "active" || token === "running" || token === "inprogress" || token === "processing") {
|
|
511
|
+
return "active";
|
|
512
|
+
}
|
|
513
|
+
if (token === "idle") {
|
|
514
|
+
return "idle";
|
|
515
|
+
}
|
|
516
|
+
if (token === "systemerror" || token === "error" || token === "failed") {
|
|
517
|
+
return "systemError";
|
|
518
|
+
}
|
|
519
|
+
return "unknown";
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function normalizeOutcome(status, error) {
|
|
523
|
+
const token = normalizeToken(status);
|
|
524
|
+
if (token === "failed" || token === "error" || error) {
|
|
525
|
+
return "failed";
|
|
526
|
+
}
|
|
527
|
+
if (["interrupted", "cancelled", "canceled", "stopped"].includes(token)) {
|
|
528
|
+
return "interrupted";
|
|
529
|
+
}
|
|
530
|
+
if (token === "completed" || token === "complete" || token === "success" || token === "succeeded") {
|
|
531
|
+
return "completed";
|
|
532
|
+
}
|
|
533
|
+
return "";
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function isActiveStatus(status) {
|
|
537
|
+
return normalizeRuntime(status) === "active";
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function isPendingApprovalRequest(request) {
|
|
541
|
+
return request?.completed !== true && APPROVAL_METHODS.has(readString(request?.method));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function isPendingUserInputRequest(request) {
|
|
545
|
+
return request?.completed !== true && USER_INPUT_METHODS.has(readString(request?.method));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function runtimeActiveFlags(status) {
|
|
549
|
+
if (normalizeRuntime(status) !== "active" || !Array.isArray(status?.activeFlags)) {
|
|
550
|
+
return [];
|
|
551
|
+
}
|
|
552
|
+
return status.activeFlags.map(normalizeToken).filter((flag) => (
|
|
553
|
+
flag === "waitingonapproval" || flag === "waitingonuserinput"
|
|
554
|
+
)).sort();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function attentionFields(approvalCount, userInputCount, flags = []) {
|
|
558
|
+
return {
|
|
559
|
+
approvalRequired: approvalCount > 0 || flags.includes("waitingonapproval"),
|
|
560
|
+
approvalRequestCount: approvalCount,
|
|
561
|
+
userInputRequired: userInputCount > 0 || flags.includes("waitingonuserinput"),
|
|
562
|
+
userInputRequestCount: userInputCount,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function appServerThreadId(message) {
|
|
567
|
+
const params = message?.params || {};
|
|
568
|
+
return readString(params.threadId)
|
|
569
|
+
|| readString(params.thread_id)
|
|
570
|
+
|| readString(params.conversationId)
|
|
571
|
+
|| readString(params.conversation_id)
|
|
572
|
+
|| readString(params.thread?.id);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function turnIdentity(params, turn) {
|
|
576
|
+
return readString(turn?.id)
|
|
577
|
+
|| readString(turn?.turnId)
|
|
578
|
+
|| readString(turn?.turn_id)
|
|
579
|
+
|| readString(params?.turnId)
|
|
580
|
+
|| readString(params?.turn_id);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function desktopTurnId(turn) {
|
|
584
|
+
return readString(turn?.id) || readString(turn?.turnId) || readString(turn?.turn_id);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function desktopTimestampMs(turn, phase) {
|
|
588
|
+
const prefix = phase === "started" ? "started" : "completed";
|
|
589
|
+
return finiteNumber(
|
|
590
|
+
turn?.[`${prefix}AtMs`]
|
|
591
|
+
?? turn?.[`turn${prefix[0].toUpperCase()}${prefix.slice(1)}AtMs`]
|
|
592
|
+
?? turn?.[`${prefix}_at_ms`]
|
|
593
|
+
?? turn?.[`turn_${prefix}_at_ms`]
|
|
594
|
+
) ?? timestampSecondsToMs(turn?.[`${prefix}At`] ?? turn?.[`${prefix}_at`]);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function timestampSecondsToMs(value) {
|
|
598
|
+
const timestamp = finiteNumber(value);
|
|
599
|
+
return timestamp == null ? null : timestamp * 1000;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function finiteNumber(value) {
|
|
603
|
+
if (value == null || value === "") {
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
const number = Number(value);
|
|
607
|
+
return Number.isFinite(number) ? number : null;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function positiveInteger(value) {
|
|
611
|
+
const number = Number(value);
|
|
612
|
+
return Number.isInteger(number) && number > 0 ? number : 1;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function rememberTerminalTurn(state, turnId) {
|
|
616
|
+
state.terminalTurnIds.delete(turnId);
|
|
617
|
+
state.terminalTurnIds.add(turnId);
|
|
618
|
+
while (state.terminalTurnIds.size > MAX_TERMINAL_TURN_IDS) {
|
|
619
|
+
const oldestTurnId = state.terminalTurnIds.keys().next().value;
|
|
620
|
+
state.terminalTurnIds.delete(oldestTurnId);
|
|
621
|
+
state.turnOrderById.delete(oldestTurnId);
|
|
622
|
+
state.turnStartedAtMsById.delete(oldestTurnId);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function rememberAppServerState(states, state, maxThreads) {
|
|
627
|
+
states.delete(state.threadId);
|
|
628
|
+
states.set(state.threadId, state);
|
|
629
|
+
while (states.size > maxThreads) {
|
|
630
|
+
const evictable = [...states.values()].find((candidate) => !isProtectedAppState(candidate));
|
|
631
|
+
if (!evictable) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
states.delete(evictable.threadId);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function isProtectedAppState(state) {
|
|
639
|
+
return hasRunningWork(state)
|
|
640
|
+
|| state.activeFlags.length > 0
|
|
641
|
+
|| state.approvalRequestIds.size > 0
|
|
642
|
+
|| state.userInputRequestIds.size > 0;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function hasRunningWork(state) {
|
|
646
|
+
return state.activeTurnIds.size > 0 || state.runningWithoutTurnId;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function hasKnownTerminalWork(state) {
|
|
650
|
+
return state.settledTurnlessWork
|
|
651
|
+
|| state.terminalTurnIds.size > 0
|
|
652
|
+
|| state.lastOutcome != null;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function replaceString(target, key, value) {
|
|
656
|
+
const nextValue = key === "title" ? truncateDisplayText(readString(value)) : readString(value);
|
|
657
|
+
if (!nextValue || target[key] === nextValue) {
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
target[key] = nextValue;
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function truncateDisplayText(value) {
|
|
665
|
+
if (value.length <= MAX_DISPLAY_TEXT_CHARS) {
|
|
666
|
+
return value;
|
|
667
|
+
}
|
|
668
|
+
return `${value.slice(0, MAX_DISPLAY_TEXT_CHARS - 1).trimEnd()}…`;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function compactEntry(entry) {
|
|
672
|
+
return compactObject({
|
|
673
|
+
...entry,
|
|
674
|
+
title: truncateDisplayText(readString(entry.title)),
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function compactObject(value) {
|
|
679
|
+
return Object.fromEntries(
|
|
680
|
+
Object.entries(value).filter(([, entry]) => entry !== null && entry !== undefined && entry !== "")
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function sameJSON(left, right) {
|
|
685
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
module.exports = {
|
|
689
|
+
APP_SERVER_SOURCE,
|
|
690
|
+
DESKTOP_IPC_SOURCE,
|
|
691
|
+
createThreadActivityProjector,
|
|
692
|
+
projectDesktopThreadActivity,
|
|
693
|
+
projectSemanticItem,
|
|
694
|
+
};
|