@makerbi/remodex 2.3.2 → 2.5.6
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/bin/remodex.js +25 -2
- package/package.json +1 -1
- package/src/bridge.js +80 -61
- package/src/codex-desktop-refresher.js +173 -8
- package/src/codex-tool-wrapper.js +523 -0
- package/src/desktop-ipc-action-follower.js +412 -129
- package/src/desktop-ipc-live-owner.js +148 -6
- package/src/desktop-ipc-owner-transport.js +143 -73
- package/src/desktop-ipc-shared.js +273 -46
- package/src/git-handler.js +189 -108
- package/src/index.js +2 -0
- package/src/macos-launch-agent.js +65 -34
- package/src/rollout-live-mirror.js +282 -22
- package/src/rollout-watch.js +176 -54
- package/src/session-jsonl-history.js +77 -33
- package/src/thread-list-provenance.js +105 -0
- package/src/thread-row-enrichment.js +43 -0
- package/src/thread-runtime-settings-store.js +3 -21
- package/src/worktree-origin.js +192 -0
package/src/rollout-watch.js
CHANGED
|
@@ -20,6 +20,13 @@ const DEFAULT_CONTEXT_READ_SCAN_BYTES = 512 * 1024;
|
|
|
20
20
|
const DEFAULT_CONTEXT_READ_CANDIDATE_LIMIT = 128;
|
|
21
21
|
const DEFAULT_RECENT_ROLLOUT_CANDIDATE_LIMIT = 24;
|
|
22
22
|
const DEFAULT_RECENT_ROLLOUT_LOOKBACK_MS = 15 * 60 * 1000;
|
|
23
|
+
const ROLLOUT_CANDIDATE_CACHE_TTL_MS = 2_000;
|
|
24
|
+
const ROLLOUT_THREAD_POSITIVE_CACHE_TTL_MS = 2_000;
|
|
25
|
+
const ROLLOUT_THREAD_NEGATIVE_CACHE_TTL_MS = 1_500;
|
|
26
|
+
const ROLLOUT_LOOKUP_CACHE_MAX_ROOTS = 32;
|
|
27
|
+
const ROLLOUT_THREAD_MEMO_MAX_SIZE = 2_000;
|
|
28
|
+
const ROLLOUT_THREAD_CONTENT_CACHE_MAX_SIZE = 4_000;
|
|
29
|
+
const rolloutLookupCachesByFsModule = new WeakMap();
|
|
23
30
|
|
|
24
31
|
// Polls one rollout file until it materializes and then reports size growth.
|
|
25
32
|
function createThreadRolloutActivityWatcher({
|
|
@@ -350,12 +357,35 @@ function findRecentRolloutFileForContextRead(
|
|
|
350
357
|
threadLookupScanBytes = DEFAULT_THREAD_LOOKUP_SCAN_BYTES,
|
|
351
358
|
} = {}
|
|
352
359
|
) {
|
|
360
|
+
const currentTime = now();
|
|
361
|
+
const cache = rolloutLookupCacheForFsModule(fsModule);
|
|
362
|
+
const threadMemoKey = threadId && !turnId
|
|
363
|
+
? rolloutThreadMemoKey(root, threadId)
|
|
364
|
+
: "";
|
|
365
|
+
if (threadMemoKey) {
|
|
366
|
+
const positive = cache.positiveThreadPaths.get(threadMemoKey);
|
|
367
|
+
if (positive) {
|
|
368
|
+
if (positive.expiresAt > currentTime && fsModule.existsSync(positive.filePath)) {
|
|
369
|
+
return positive.filePath;
|
|
370
|
+
}
|
|
371
|
+
cache.positiveThreadPaths.delete(threadMemoKey);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const negativeExpiresAt = cache.negativeThreadLookups.get(threadMemoKey) || 0;
|
|
375
|
+
if (negativeExpiresAt > currentTime) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
cache.negativeThreadLookups.delete(threadMemoKey);
|
|
379
|
+
}
|
|
380
|
+
|
|
353
381
|
const candidates = collectRecentRolloutFiles(root, {
|
|
354
382
|
fsModule,
|
|
355
383
|
candidateLimit,
|
|
356
384
|
modifiedAfterMs: 0,
|
|
385
|
+
now,
|
|
357
386
|
});
|
|
358
387
|
if (candidates.length === 0) {
|
|
388
|
+
rememberNegativeThreadLookup(cache, threadMemoKey, currentTime);
|
|
359
389
|
return null;
|
|
360
390
|
}
|
|
361
391
|
|
|
@@ -373,8 +403,10 @@ function findRecentRolloutFileForContextRead(
|
|
|
373
403
|
if (threadId) {
|
|
374
404
|
const threadScopedRollout = findPreferredRolloutFileForThread(root, candidates, threadId, {
|
|
375
405
|
fsModule,
|
|
406
|
+
now,
|
|
376
407
|
});
|
|
377
408
|
if (threadScopedRollout) {
|
|
409
|
+
rememberPositiveThreadPath(cache, threadMemoKey, threadScopedRollout, currentTime);
|
|
378
410
|
return threadScopedRollout;
|
|
379
411
|
}
|
|
380
412
|
|
|
@@ -383,23 +415,33 @@ function findRecentRolloutFileForContextRead(
|
|
|
383
415
|
fsModule,
|
|
384
416
|
scanBytes: threadLookupScanBytes,
|
|
385
417
|
})) {
|
|
418
|
+
rememberPositiveThreadPath(cache, threadMemoKey, candidate.filePath, currentTime);
|
|
386
419
|
return candidate.filePath;
|
|
387
420
|
}
|
|
388
421
|
}
|
|
389
422
|
}
|
|
390
423
|
|
|
424
|
+
rememberNegativeThreadLookup(cache, threadMemoKey, currentTime);
|
|
391
425
|
return null;
|
|
392
426
|
}
|
|
393
427
|
|
|
394
428
|
// Keeps the fast "recent files first" path, but falls back to a full-tree scan
|
|
395
429
|
// so older valid thread rollouts still recover after many newer sessions exist.
|
|
396
|
-
function findPreferredRolloutFileForThread(
|
|
430
|
+
function findPreferredRolloutFileForThread(
|
|
431
|
+
root,
|
|
432
|
+
candidates,
|
|
433
|
+
threadId,
|
|
434
|
+
{
|
|
435
|
+
fsModule = fs,
|
|
436
|
+
now = () => Date.now(),
|
|
437
|
+
} = {}
|
|
438
|
+
) {
|
|
397
439
|
const recentMatch = findMostRecentRolloutFileForThread(candidates, threadId);
|
|
398
440
|
if (recentMatch) {
|
|
399
441
|
return recentMatch;
|
|
400
442
|
}
|
|
401
443
|
|
|
402
|
-
return findNewestRolloutFileForThread(root, threadId, { fsModule });
|
|
444
|
+
return findNewestRolloutFileForThread(root, threadId, { fsModule, now });
|
|
403
445
|
}
|
|
404
446
|
|
|
405
447
|
// Prefers the newest filename-scoped rollout for a thread instead of the first
|
|
@@ -413,45 +455,29 @@ function findMostRecentRolloutFileForThread(candidates, threadId) {
|
|
|
413
455
|
return match?.filePath || null;
|
|
414
456
|
}
|
|
415
457
|
|
|
416
|
-
//
|
|
458
|
+
// Uses the complete sorted candidate set when the recent slice missed the
|
|
417
459
|
// thread, still preferring the newest matching rollout instead of the first hit.
|
|
418
|
-
function findNewestRolloutFileForThread(
|
|
419
|
-
|
|
460
|
+
function findNewestRolloutFileForThread(
|
|
461
|
+
root,
|
|
462
|
+
threadId,
|
|
463
|
+
{
|
|
464
|
+
fsModule = fs,
|
|
465
|
+
now = () => Date.now(),
|
|
466
|
+
} = {}
|
|
467
|
+
) {
|
|
468
|
+
if (!threadId) {
|
|
420
469
|
return null;
|
|
421
470
|
}
|
|
422
471
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if (entry.isDirectory()) {
|
|
433
|
-
stack.push(fullPath);
|
|
434
|
-
continue;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
if (!entry.isFile()
|
|
438
|
-
|| !entry.name.startsWith("rollout-")
|
|
439
|
-
|| !entry.name.endsWith(".jsonl")
|
|
440
|
-
|| !entry.name.includes(threadId)) {
|
|
441
|
-
continue;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
const candidate = {
|
|
445
|
-
filePath: fullPath,
|
|
446
|
-
mtimeMs: fsModule.statSync(fullPath).mtimeMs,
|
|
447
|
-
};
|
|
448
|
-
if (!newestMatch || compareRolloutFileOrder(candidate, newestMatch) < 0) {
|
|
449
|
-
newestMatch = candidate;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
return newestMatch?.filePath || null;
|
|
472
|
+
return findMostRecentRolloutFileForThread(
|
|
473
|
+
collectRecentRolloutFiles(root, {
|
|
474
|
+
fsModule,
|
|
475
|
+
candidateLimit: Number.POSITIVE_INFINITY,
|
|
476
|
+
modifiedAfterMs: 0,
|
|
477
|
+
now,
|
|
478
|
+
}),
|
|
479
|
+
threadId
|
|
480
|
+
);
|
|
455
481
|
}
|
|
456
482
|
|
|
457
483
|
function collectRecentRolloutFiles(
|
|
@@ -460,12 +486,33 @@ function collectRecentRolloutFiles(
|
|
|
460
486
|
fsModule = fs,
|
|
461
487
|
candidateLimit = DEFAULT_RECENT_ROLLOUT_CANDIDATE_LIMIT,
|
|
462
488
|
modifiedAfterMs = 0,
|
|
489
|
+
now = () => Date.now(),
|
|
463
490
|
} = {}
|
|
464
491
|
) {
|
|
492
|
+
const cache = rolloutLookupCacheForFsModule(fsModule);
|
|
493
|
+
const currentTime = now();
|
|
494
|
+
const cached = cache.candidatesByRoot.get(root);
|
|
495
|
+
let candidates = cached?.candidates;
|
|
496
|
+
if (!cached || currentTime - cached.createdAt >= ROLLOUT_CANDIDATE_CACHE_TTL_MS) {
|
|
497
|
+
candidates = scanRolloutFiles(root, fsModule);
|
|
498
|
+
cache.candidatesByRoot.delete(root);
|
|
499
|
+
cache.candidatesByRoot.set(root, {
|
|
500
|
+
candidates,
|
|
501
|
+
createdAt: currentTime,
|
|
502
|
+
});
|
|
503
|
+
evictOldestCacheEntries(cache.candidatesByRoot, ROLLOUT_LOOKUP_CACHE_MAX_ROOTS);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const filtered = modifiedAfterMs > 0
|
|
507
|
+
? candidates.filter(({ mtimeMs }) => mtimeMs >= modifiedAfterMs)
|
|
508
|
+
: candidates;
|
|
509
|
+
return filtered.slice(0, candidateLimit);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function scanRolloutFiles(root, fsModule) {
|
|
465
513
|
if (!fsModule.existsSync(root)) {
|
|
466
514
|
return [];
|
|
467
515
|
}
|
|
468
|
-
|
|
469
516
|
const stack = [root];
|
|
470
517
|
const candidates = [];
|
|
471
518
|
|
|
@@ -487,10 +534,6 @@ function collectRecentRolloutFiles(
|
|
|
487
534
|
}
|
|
488
535
|
|
|
489
536
|
const stat = fsModule.statSync(fullPath);
|
|
490
|
-
if (modifiedAfterMs > 0 && stat.mtimeMs < modifiedAfterMs) {
|
|
491
|
-
continue;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
537
|
candidates.push({
|
|
495
538
|
filePath: fullPath,
|
|
496
539
|
mtimeMs: stat.mtimeMs,
|
|
@@ -503,17 +546,7 @@ function collectRecentRolloutFiles(
|
|
|
503
546
|
|| path.basename(rhs.filePath).localeCompare(path.basename(lhs.filePath))
|
|
504
547
|
|| rhs.filePath.localeCompare(lhs.filePath)
|
|
505
548
|
);
|
|
506
|
-
return candidates
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Keeps rollout selection deterministic when filesystem timestamp resolution
|
|
510
|
-
// reports equal mtimes for several rollout candidates.
|
|
511
|
-
function compareRolloutFileOrder(lhs, rhs) {
|
|
512
|
-
if (lhs.mtimeMs !== rhs.mtimeMs) {
|
|
513
|
-
return rhs.mtimeMs - lhs.mtimeMs;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
return rhs.filePath.localeCompare(lhs.filePath);
|
|
549
|
+
return candidates;
|
|
517
550
|
}
|
|
518
551
|
|
|
519
552
|
function rolloutFileContainsTurnId(
|
|
@@ -555,6 +588,11 @@ function rolloutFileContainsThreadId(
|
|
|
555
588
|
}
|
|
556
589
|
|
|
557
590
|
const stat = fsModule.statSync(filePath);
|
|
591
|
+
const cache = rolloutLookupCacheForFsModule(fsModule);
|
|
592
|
+
const cacheKey = `${filePath}\0${threadId}\0${stat.size}\0${scanBytes}`;
|
|
593
|
+
if (cache.threadContentMatches.has(cacheKey)) {
|
|
594
|
+
return cache.threadContentMatches.get(cacheKey);
|
|
595
|
+
}
|
|
558
596
|
const chunk = readFileSlice(
|
|
559
597
|
filePath,
|
|
560
598
|
Math.max(0, stat.size - Math.min(stat.size, scanBytes)),
|
|
@@ -562,15 +600,98 @@ function rolloutFileContainsThreadId(
|
|
|
562
600
|
fsModule
|
|
563
601
|
);
|
|
564
602
|
if (!chunk) {
|
|
603
|
+
rememberThreadContentMatch(cache, cacheKey, false);
|
|
565
604
|
return false;
|
|
566
605
|
}
|
|
567
606
|
|
|
568
|
-
|
|
607
|
+
const matches = (
|
|
569
608
|
chunk.includes(`"thread_id":"${threadId}"`)
|
|
570
609
|
|| chunk.includes(`"threadId":"${threadId}"`)
|
|
571
610
|
|| chunk.includes(`"conversation_id":"${threadId}"`)
|
|
572
611
|
|| chunk.includes(`"conversationId":"${threadId}"`)
|
|
573
612
|
);
|
|
613
|
+
rememberThreadContentMatch(cache, cacheKey, matches);
|
|
614
|
+
return matches;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function rolloutLookupCacheForFsModule(fsModule) {
|
|
618
|
+
let cache = rolloutLookupCachesByFsModule.get(fsModule);
|
|
619
|
+
if (!cache) {
|
|
620
|
+
cache = {
|
|
621
|
+
candidatesByRoot: new Map(),
|
|
622
|
+
positiveThreadPaths: new Map(),
|
|
623
|
+
negativeThreadLookups: new Map(),
|
|
624
|
+
threadContentMatches: new Map(),
|
|
625
|
+
};
|
|
626
|
+
rolloutLookupCachesByFsModule.set(fsModule, cache);
|
|
627
|
+
}
|
|
628
|
+
return cache;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function rolloutThreadMemoKey(root, threadId) {
|
|
632
|
+
return `${root}\0${threadId}`;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function rememberPositiveThreadPath(cache, memoKey, filePath, currentTime) {
|
|
636
|
+
if (!memoKey) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
cache.negativeThreadLookups.delete(memoKey);
|
|
640
|
+
cache.positiveThreadPaths.delete(memoKey);
|
|
641
|
+
cache.positiveThreadPaths.set(memoKey, {
|
|
642
|
+
filePath,
|
|
643
|
+
expiresAt: currentTime + ROLLOUT_THREAD_POSITIVE_CACHE_TTL_MS,
|
|
644
|
+
});
|
|
645
|
+
evictOldestCacheEntries(cache.positiveThreadPaths, ROLLOUT_THREAD_MEMO_MAX_SIZE);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function rememberNegativeThreadLookup(cache, memoKey, currentTime) {
|
|
649
|
+
if (!memoKey) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
cache.negativeThreadLookups.delete(memoKey);
|
|
653
|
+
cache.negativeThreadLookups.set(
|
|
654
|
+
memoKey,
|
|
655
|
+
currentTime + ROLLOUT_THREAD_NEGATIVE_CACHE_TTL_MS
|
|
656
|
+
);
|
|
657
|
+
evictOldestCacheEntries(cache.negativeThreadLookups, ROLLOUT_THREAD_MEMO_MAX_SIZE);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function rememberThreadContentMatch(cache, cacheKey, matches) {
|
|
661
|
+
cache.threadContentMatches.set(cacheKey, matches);
|
|
662
|
+
evictOldestCacheEntries(
|
|
663
|
+
cache.threadContentMatches,
|
|
664
|
+
ROLLOUT_THREAD_CONTENT_CACHE_MAX_SIZE
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function evictOldestCacheEntries(cache, maxSize) {
|
|
669
|
+
while (cache.size > maxSize) {
|
|
670
|
+
cache.delete(cache.keys().next().value);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function invalidateRolloutLookupCache({
|
|
675
|
+
root = "",
|
|
676
|
+
threadId = "",
|
|
677
|
+
fsModule = fs,
|
|
678
|
+
} = {}) {
|
|
679
|
+
const cache = rolloutLookupCachesByFsModule.get(fsModule);
|
|
680
|
+
if (!cache) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (!root) {
|
|
684
|
+
rolloutLookupCachesByFsModule.delete(fsModule);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
cache.candidatesByRoot.delete(root);
|
|
689
|
+
if (!threadId) {
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
const memoKey = rolloutThreadMemoKey(root, threadId);
|
|
693
|
+
cache.positiveThreadPaths.delete(memoKey);
|
|
694
|
+
cache.negativeThreadLookups.delete(memoKey);
|
|
574
695
|
}
|
|
575
696
|
|
|
576
697
|
function formatRolloutLine(rawLine) {
|
|
@@ -845,4 +966,5 @@ module.exports = {
|
|
|
845
966
|
resolveSessionsRoot,
|
|
846
967
|
findRolloutFileForThread,
|
|
847
968
|
findRecentRolloutFileForContextRead,
|
|
969
|
+
invalidateRolloutLookupCache,
|
|
848
970
|
};
|
|
@@ -4,12 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const { buildApplyPatchFileChangeItem } = require("./apply-patch-changes");
|
|
7
|
+
const {
|
|
8
|
+
expandExecWrapperToolCall,
|
|
9
|
+
isOrchestrationWaitCall,
|
|
10
|
+
} = require("./codex-tool-wrapper");
|
|
7
11
|
const { terminalEventClosesTrackedTurn } = require("./rollout-turn-semantics");
|
|
8
12
|
const {
|
|
9
13
|
buildRemodexSourceItemKey,
|
|
10
14
|
isContextualUserText,
|
|
11
15
|
isUserRoleItem,
|
|
12
16
|
responseItemMessageText: sharedResponseItemMessageText,
|
|
17
|
+
sanitizeUserRoleItem,
|
|
13
18
|
visibleUserPromptText,
|
|
14
19
|
} = require("./desktop-ipc-shared");
|
|
15
20
|
|
|
@@ -165,7 +170,7 @@ function readSessionJsonlMetadataFromFile(filePath, {
|
|
|
165
170
|
metadataHeadBytes = DEFAULT_SESSION_JSONL_METADATA_HEAD_BYTES,
|
|
166
171
|
} = {}) {
|
|
167
172
|
if (!filePath) {
|
|
168
|
-
return
|
|
173
|
+
return emptySessionJsonlMetadata();
|
|
169
174
|
}
|
|
170
175
|
if (!supportsBoundedSessionJsonlReads(fsModule)) {
|
|
171
176
|
return parseSessionJsonlMetadata(fsModule.readFileSync(filePath, "utf8"));
|
|
@@ -174,7 +179,7 @@ function readSessionJsonlMetadataFromFile(filePath, {
|
|
|
174
179
|
const stat = fsModule.statSync(filePath);
|
|
175
180
|
const snapshotSize = Math.max(0, Number(stat?.size) || 0);
|
|
176
181
|
if (snapshotSize === 0) {
|
|
177
|
-
return
|
|
182
|
+
return emptySessionJsonlMetadata();
|
|
178
183
|
}
|
|
179
184
|
const fileHandle = fsModule.openSync(filePath, "r");
|
|
180
185
|
try {
|
|
@@ -185,12 +190,23 @@ function readSessionJsonlMetadataFromFile(filePath, {
|
|
|
185
190
|
fsModule
|
|
186
191
|
);
|
|
187
192
|
const metadata = parseSessionJsonlInitialMetadata(head.toString("utf8"));
|
|
188
|
-
return {
|
|
193
|
+
return {
|
|
194
|
+
threadId: metadata.threadId,
|
|
195
|
+
cwd: metadata.cwd,
|
|
196
|
+
forkedFromId: metadata.forkedFromId,
|
|
197
|
+
threadSource: metadata.threadSource,
|
|
198
|
+
};
|
|
189
199
|
} finally {
|
|
190
200
|
fsModule.closeSync(fileHandle);
|
|
191
201
|
}
|
|
192
202
|
}
|
|
193
203
|
|
|
204
|
+
// Every exit path of readSessionJsonlMetadataFromFile returns this shape so callers
|
|
205
|
+
// can read provenance without knowing which branch produced the result.
|
|
206
|
+
function emptySessionJsonlMetadata() {
|
|
207
|
+
return { threadId: "", cwd: "", forkedFromId: "", threadSource: "" };
|
|
208
|
+
}
|
|
209
|
+
|
|
194
210
|
function supportsBoundedSessionJsonlReads(fsModule) {
|
|
195
211
|
return typeof fsModule?.statSync === "function"
|
|
196
212
|
&& typeof fsModule?.openSync === "function"
|
|
@@ -286,6 +302,11 @@ function parseSessionJsonlInitialMetadata(content) {
|
|
|
286
302
|
let threadId = "";
|
|
287
303
|
let cwd = "";
|
|
288
304
|
let timeZone = "";
|
|
305
|
+
// Provenance app-server keeps in the rollout but leaves null on thread/list
|
|
306
|
+
// rows: without it a forked automation thread is indistinguishable from the
|
|
307
|
+
// origin it copied its name and preview from.
|
|
308
|
+
let forkedFromId = "";
|
|
309
|
+
let threadSource = "";
|
|
289
310
|
const raw = String(content || "");
|
|
290
311
|
let lineStart = 0;
|
|
291
312
|
while (lineStart < raw.length) {
|
|
@@ -313,18 +334,24 @@ function parseSessionJsonlInitialMetadata(content) {
|
|
|
313
334
|
timeZone = normalizeString(payload?.timezone)
|
|
314
335
|
|| normalizeString(payload?.timeZone)
|
|
315
336
|
|| normalizeString(payload?.time_zone);
|
|
337
|
+
forkedFromId = normalizeString(payload?.forked_from_id)
|
|
338
|
+
|| normalizeString(payload?.forkedFromId);
|
|
339
|
+
threadSource = normalizeString(payload?.thread_source)
|
|
340
|
+
|| normalizeString(payload?.threadSource);
|
|
316
341
|
break;
|
|
317
342
|
} catch {
|
|
318
343
|
// Metadata is expected at the head; an incomplete oversized line is not trusted.
|
|
319
344
|
}
|
|
320
345
|
}
|
|
321
|
-
return { threadId, cwd, timeZone };
|
|
346
|
+
return { threadId, cwd, timeZone, forkedFromId, threadSource };
|
|
322
347
|
}
|
|
323
348
|
|
|
324
349
|
// Extracts thread-level context that app-server history can omit for desktop-origin runs.
|
|
325
350
|
function parseSessionJsonlMetadata(content) {
|
|
326
351
|
let threadId = "";
|
|
327
352
|
let cwd = "";
|
|
353
|
+
let forkedFromId = "";
|
|
354
|
+
let threadSource = "";
|
|
328
355
|
|
|
329
356
|
const raw = String(content || "");
|
|
330
357
|
let lineStart = 0;
|
|
@@ -357,13 +384,17 @@ function parseSessionJsonlMetadata(content) {
|
|
|
357
384
|
cwd ||= normalizeString(payload?.cwd)
|
|
358
385
|
|| normalizeString(payload?.current_working_directory)
|
|
359
386
|
|| normalizeString(payload?.working_directory);
|
|
387
|
+
forkedFromId ||= normalizeString(payload?.forked_from_id)
|
|
388
|
+
|| normalizeString(payload?.forkedFromId);
|
|
389
|
+
threadSource ||= normalizeString(payload?.thread_source)
|
|
390
|
+
|| normalizeString(payload?.threadSource);
|
|
360
391
|
|
|
361
392
|
if (threadId && cwd) {
|
|
362
393
|
break;
|
|
363
394
|
}
|
|
364
395
|
}
|
|
365
396
|
|
|
366
|
-
return { threadId, cwd };
|
|
397
|
+
return { threadId, cwd, forkedFromId, threadSource };
|
|
367
398
|
}
|
|
368
399
|
|
|
369
400
|
function parseSessionJsonlTurns(content, {
|
|
@@ -558,36 +589,39 @@ function parseSessionJsonlTurns(content, {
|
|
|
558
589
|
if (!payload) {
|
|
559
590
|
continue;
|
|
560
591
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
const turn = ensureTurn(
|
|
566
|
-
turns,
|
|
567
|
-
turnsById,
|
|
568
|
-
responseItemTurnId(payload) || activeTurnId || `turn-line-${sourceLineNumber}`,
|
|
569
|
-
sessionThreadId,
|
|
570
|
-
entry.timestamp
|
|
571
|
-
);
|
|
572
|
-
applyHistoryTimeZone(turn, sessionTimeZone);
|
|
573
|
-
const item = normalizeResponseItemForHistory(payload, sourceLineNumber, {
|
|
574
|
-
cwd: sessionCwd,
|
|
575
|
-
toolCallsByCallId,
|
|
576
|
-
});
|
|
577
|
-
if (item) {
|
|
578
|
-
if (shouldSkipDuplicateProposedPlanMessage(turn, item)) {
|
|
592
|
+
const projectedPayloads = expandExecWrapperToolCall(payload);
|
|
593
|
+
for (const projectedPayload of projectedPayloads) {
|
|
594
|
+
rememberToolCallForHistory(projectedPayload, toolCallsByCallId);
|
|
595
|
+
if (shouldSkipResponseItemForHistory(projectedPayload, skippedCallIds)) {
|
|
579
596
|
continue;
|
|
580
597
|
}
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
598
|
+
const turn = ensureTurn(
|
|
599
|
+
turns,
|
|
600
|
+
turnsById,
|
|
601
|
+
responseItemTurnId(projectedPayload) || activeTurnId || `turn-line-${sourceLineNumber}`,
|
|
602
|
+
sessionThreadId,
|
|
603
|
+
entry.timestamp
|
|
604
|
+
);
|
|
605
|
+
applyHistoryTimeZone(turn, sessionTimeZone);
|
|
606
|
+
const item = normalizeResponseItemForHistory(projectedPayload, sourceLineNumber, {
|
|
607
|
+
cwd: sessionCwd,
|
|
608
|
+
toolCallsByCallId,
|
|
609
|
+
});
|
|
610
|
+
if (item) {
|
|
611
|
+
if (shouldSkipDuplicateProposedPlanMessage(turn, item)) {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
const itemTimestamp = historyItemTimestamp(item, entry.timestamp);
|
|
615
|
+
if (itemTimestamp && !item.createdAt) {
|
|
616
|
+
item.createdAt = itemTimestamp;
|
|
617
|
+
}
|
|
618
|
+
if (itemTimestamp && !item.timestamp) {
|
|
619
|
+
item.timestamp = itemTimestamp;
|
|
620
|
+
}
|
|
621
|
+
applyHistoryTimeZone(item, sessionTimeZone);
|
|
622
|
+
applyHistoryAssistantSourceAlias(item, turn.id, assistantAliasOccurrencesByBaseKey);
|
|
623
|
+
addHistoryItemToTurn(turn, item);
|
|
587
624
|
}
|
|
588
|
-
applyHistoryTimeZone(item, sessionTimeZone);
|
|
589
|
-
applyHistoryAssistantSourceAlias(item, turn.id, assistantAliasOccurrencesByBaseKey);
|
|
590
|
-
addHistoryItemToTurn(turn, item);
|
|
591
625
|
}
|
|
592
626
|
}
|
|
593
627
|
}
|
|
@@ -862,7 +896,10 @@ function normalizeResponseItemForHistory(payload, lineNumber, { cwd = "", toolCa
|
|
|
862
896
|
item.role = "assistant";
|
|
863
897
|
}
|
|
864
898
|
|
|
865
|
-
|
|
899
|
+
// A single user item can carry injected context next to the real request.
|
|
900
|
+
// Sanitize here so history readers (including the thread/read JSONL merge,
|
|
901
|
+
// which runs after the relay sanitizer) never rebuild the hidden fragments.
|
|
902
|
+
return isUserRoleItem(item) ? sanitizeUserRoleItem(item) : item;
|
|
866
903
|
}
|
|
867
904
|
|
|
868
905
|
function applyHistoryAssistantSourceAlias(item, turnId, occurrencesByBaseKey = new Map()) {
|
|
@@ -1306,6 +1343,13 @@ function shouldSkipResponseItemForHistory(payload, skippedCallIds) {
|
|
|
1306
1343
|
return true;
|
|
1307
1344
|
}
|
|
1308
1345
|
|
|
1346
|
+
if (type === "tool_call" && isOrchestrationWaitCall(payload)) {
|
|
1347
|
+
if (callId) {
|
|
1348
|
+
skippedCallIds.add(callId);
|
|
1349
|
+
}
|
|
1350
|
+
return true;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1309
1353
|
if (type === "tool_call" && isSubagentOrchestrationCall(payload)) {
|
|
1310
1354
|
if (callId) {
|
|
1311
1355
|
skippedCallIds.add(callId);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// FILE: thread-list-provenance.js
|
|
2
|
+
// Purpose: Restores fork/automation provenance that app-server drops from thread list rows.
|
|
3
|
+
// Layer: CLI helper
|
|
4
|
+
// Exports: createThreadListProvenanceEnricher
|
|
5
|
+
// Depends on: fs, ./session-jsonl-history, ./thread-row-enrichment
|
|
6
|
+
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const { readSessionJsonlMetadataFromFile } = require("./session-jsonl-history");
|
|
9
|
+
const { forEachThreadRowInResponse } = require("./thread-row-enrichment");
|
|
10
|
+
|
|
11
|
+
// An entry is two short strings, so the cache stays far cheaper than re-reading
|
|
12
|
+
// rollout heads. Sized well above any single thread/list page: a full unpaginated
|
|
13
|
+
// list must not evict rows it is still walking, or every refresh re-reads the disk.
|
|
14
|
+
const DEFAULT_MAX_ENTRIES = 4096;
|
|
15
|
+
|
|
16
|
+
// app-server returns `forkedFromId: null` / `threadSource: null` on thread rows even
|
|
17
|
+
// when the rollout's session_meta carries both. A thread forked by a desktop automation
|
|
18
|
+
// copies the origin's name and preview, so without this the phone shows two identical
|
|
19
|
+
// sidebar rows with no way to tell which is which. The rollout row already carries the
|
|
20
|
+
// file path, and session_meta is written once at session start, so a bounded cache keyed
|
|
21
|
+
// by thread id keeps this to one small head read per thread ever seen.
|
|
22
|
+
function createThreadListProvenanceEnricher({
|
|
23
|
+
fsModule = fs,
|
|
24
|
+
maxEntries = DEFAULT_MAX_ENTRIES,
|
|
25
|
+
} = {}) {
|
|
26
|
+
const cache = new Map();
|
|
27
|
+
|
|
28
|
+
function readProvenance(threadId, filePath) {
|
|
29
|
+
if (cache.has(threadId)) {
|
|
30
|
+
const cached = cache.get(threadId);
|
|
31
|
+
// Refresh insertion order so the Map behaves as an LRU.
|
|
32
|
+
cache.delete(threadId);
|
|
33
|
+
cache.set(threadId, cached);
|
|
34
|
+
return cached;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let provenance = { forkedFromId: "", threadSource: "" };
|
|
38
|
+
let readSessionMeta = false;
|
|
39
|
+
try {
|
|
40
|
+
const metadata = readSessionJsonlMetadataFromFile(filePath, { fsModule });
|
|
41
|
+
// The thread id only appears once session_meta is on disk; without it the read
|
|
42
|
+
// hit a rotated, unreadable, or still-being-written rollout.
|
|
43
|
+
readSessionMeta = Boolean(normalizeString(metadata?.threadId));
|
|
44
|
+
provenance = {
|
|
45
|
+
forkedFromId: normalizeString(metadata?.forkedFromId),
|
|
46
|
+
threadSource: normalizeString(metadata?.threadSource),
|
|
47
|
+
};
|
|
48
|
+
} catch {
|
|
49
|
+
// A rotated, half-written, or unreadable rollout simply leaves the row as-is.
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Only a real session_meta is worth remembering: it never changes again. Caching a
|
|
53
|
+
// failed read instead would strand a freshly forked thread without its badge until
|
|
54
|
+
// the bridge restarts, so those are retried on the next list.
|
|
55
|
+
if (!readSessionMeta) {
|
|
56
|
+
return provenance;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
cache.set(threadId, provenance);
|
|
60
|
+
while (cache.size > Math.max(1, maxEntries)) {
|
|
61
|
+
cache.delete(cache.keys().next().value);
|
|
62
|
+
}
|
|
63
|
+
return provenance;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function attachToThread(thread) {
|
|
67
|
+
const threadId = normalizeString(thread?.id) || normalizeString(thread?.threadId);
|
|
68
|
+
const filePath = normalizeString(thread?.path) || normalizeString(thread?.rolloutPath);
|
|
69
|
+
if (!thread || typeof thread !== "object" || !threadId || !filePath) {
|
|
70
|
+
return thread;
|
|
71
|
+
}
|
|
72
|
+
// Never overwrite what app-server did resolve; this only fills the gaps.
|
|
73
|
+
if (normalizeString(thread.forkedFromId) && normalizeString(thread.threadSource)) {
|
|
74
|
+
return thread;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const provenance = readProvenance(threadId, filePath);
|
|
78
|
+
if (!normalizeString(thread.forkedFromId) && provenance.forkedFromId) {
|
|
79
|
+
thread.forkedFromId = provenance.forkedFromId;
|
|
80
|
+
}
|
|
81
|
+
if (!normalizeString(thread.threadSource) && provenance.threadSource) {
|
|
82
|
+
thread.threadSource = provenance.threadSource;
|
|
83
|
+
}
|
|
84
|
+
return thread;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function enrichResponse(method, envelope) {
|
|
88
|
+
return forEachThreadRowInResponse(method, envelope, attachToThread);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
attachToThread,
|
|
93
|
+
enrichResponse,
|
|
94
|
+
// Exposed for focused tests so cache growth stays provable.
|
|
95
|
+
cacheSize: () => cache.size,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normalizeString(value) {
|
|
100
|
+
return typeof value === "string" ? value.trim() : "";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
createThreadListProvenanceEnricher,
|
|
105
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// FILE: thread-row-enrichment.js
|
|
2
|
+
// Purpose: One walk over the thread rows carried by thread/list, thread/read, and
|
|
3
|
+
// thread/resume responses, so every enricher shares the same shape handling
|
|
4
|
+
// and a response is traversed once no matter how many enrichers run on it.
|
|
5
|
+
// Layer: CLI helper
|
|
6
|
+
// Exports: forEachThreadRowInResponse
|
|
7
|
+
// Depends on: (none)
|
|
8
|
+
|
|
9
|
+
const THREAD_LIST_ROW_KEYS = ["data", "items", "threads"];
|
|
10
|
+
|
|
11
|
+
function forEachThreadRowInResponse(method, envelope, visit) {
|
|
12
|
+
if (typeof visit !== "function") {
|
|
13
|
+
return envelope;
|
|
14
|
+
}
|
|
15
|
+
if (!envelope || typeof envelope !== "object" || envelope.error) {
|
|
16
|
+
return envelope;
|
|
17
|
+
}
|
|
18
|
+
const result = envelope.result;
|
|
19
|
+
if (!result || typeof result !== "object") {
|
|
20
|
+
return envelope;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (method === "thread/read" || method === "thread/resume") {
|
|
24
|
+
if (result.thread && typeof result.thread === "object") {
|
|
25
|
+
visit(result.thread);
|
|
26
|
+
}
|
|
27
|
+
return envelope;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (method === "thread/list") {
|
|
31
|
+
const key = THREAD_LIST_ROW_KEYS.find((candidate) => Array.isArray(result[candidate]));
|
|
32
|
+
for (const thread of key ? result[key] : []) {
|
|
33
|
+
if (thread && typeof thread === "object") {
|
|
34
|
+
visit(thread);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return envelope;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
forEachThreadRowInResponse,
|
|
43
|
+
};
|