@makerbi/remodex 2.4.0 → 3.1.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/bin/remodex.js +25 -2
- package/package.json +1 -1
- package/src/bridge.js +46 -61
- package/src/codex-desktop-refresher.js +173 -8
- package/src/codex-tool-wrapper.js +523 -0
- package/src/desktop-ipc-action-follower.js +316 -59
- package/src/desktop-ipc-live-owner.js +148 -6
- package/src/desktop-ipc-owner-transport.js +143 -73
- package/src/desktop-ipc-shared.js +125 -8
- 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 +142 -13
- package/src/rollout-watch.js +176 -54
- package/src/session-jsonl-history.js +72 -32
- 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,6 +4,10 @@
|
|
|
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,
|
|
@@ -166,7 +170,7 @@ function readSessionJsonlMetadataFromFile(filePath, {
|
|
|
166
170
|
metadataHeadBytes = DEFAULT_SESSION_JSONL_METADATA_HEAD_BYTES,
|
|
167
171
|
} = {}) {
|
|
168
172
|
if (!filePath) {
|
|
169
|
-
return
|
|
173
|
+
return emptySessionJsonlMetadata();
|
|
170
174
|
}
|
|
171
175
|
if (!supportsBoundedSessionJsonlReads(fsModule)) {
|
|
172
176
|
return parseSessionJsonlMetadata(fsModule.readFileSync(filePath, "utf8"));
|
|
@@ -175,7 +179,7 @@ function readSessionJsonlMetadataFromFile(filePath, {
|
|
|
175
179
|
const stat = fsModule.statSync(filePath);
|
|
176
180
|
const snapshotSize = Math.max(0, Number(stat?.size) || 0);
|
|
177
181
|
if (snapshotSize === 0) {
|
|
178
|
-
return
|
|
182
|
+
return emptySessionJsonlMetadata();
|
|
179
183
|
}
|
|
180
184
|
const fileHandle = fsModule.openSync(filePath, "r");
|
|
181
185
|
try {
|
|
@@ -186,12 +190,23 @@ function readSessionJsonlMetadataFromFile(filePath, {
|
|
|
186
190
|
fsModule
|
|
187
191
|
);
|
|
188
192
|
const metadata = parseSessionJsonlInitialMetadata(head.toString("utf8"));
|
|
189
|
-
return {
|
|
193
|
+
return {
|
|
194
|
+
threadId: metadata.threadId,
|
|
195
|
+
cwd: metadata.cwd,
|
|
196
|
+
forkedFromId: metadata.forkedFromId,
|
|
197
|
+
threadSource: metadata.threadSource,
|
|
198
|
+
};
|
|
190
199
|
} finally {
|
|
191
200
|
fsModule.closeSync(fileHandle);
|
|
192
201
|
}
|
|
193
202
|
}
|
|
194
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
|
+
|
|
195
210
|
function supportsBoundedSessionJsonlReads(fsModule) {
|
|
196
211
|
return typeof fsModule?.statSync === "function"
|
|
197
212
|
&& typeof fsModule?.openSync === "function"
|
|
@@ -287,6 +302,11 @@ function parseSessionJsonlInitialMetadata(content) {
|
|
|
287
302
|
let threadId = "";
|
|
288
303
|
let cwd = "";
|
|
289
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 = "";
|
|
290
310
|
const raw = String(content || "");
|
|
291
311
|
let lineStart = 0;
|
|
292
312
|
while (lineStart < raw.length) {
|
|
@@ -314,18 +334,24 @@ function parseSessionJsonlInitialMetadata(content) {
|
|
|
314
334
|
timeZone = normalizeString(payload?.timezone)
|
|
315
335
|
|| normalizeString(payload?.timeZone)
|
|
316
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);
|
|
317
341
|
break;
|
|
318
342
|
} catch {
|
|
319
343
|
// Metadata is expected at the head; an incomplete oversized line is not trusted.
|
|
320
344
|
}
|
|
321
345
|
}
|
|
322
|
-
return { threadId, cwd, timeZone };
|
|
346
|
+
return { threadId, cwd, timeZone, forkedFromId, threadSource };
|
|
323
347
|
}
|
|
324
348
|
|
|
325
349
|
// Extracts thread-level context that app-server history can omit for desktop-origin runs.
|
|
326
350
|
function parseSessionJsonlMetadata(content) {
|
|
327
351
|
let threadId = "";
|
|
328
352
|
let cwd = "";
|
|
353
|
+
let forkedFromId = "";
|
|
354
|
+
let threadSource = "";
|
|
329
355
|
|
|
330
356
|
const raw = String(content || "");
|
|
331
357
|
let lineStart = 0;
|
|
@@ -358,13 +384,17 @@ function parseSessionJsonlMetadata(content) {
|
|
|
358
384
|
cwd ||= normalizeString(payload?.cwd)
|
|
359
385
|
|| normalizeString(payload?.current_working_directory)
|
|
360
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);
|
|
361
391
|
|
|
362
392
|
if (threadId && cwd) {
|
|
363
393
|
break;
|
|
364
394
|
}
|
|
365
395
|
}
|
|
366
396
|
|
|
367
|
-
return { threadId, cwd };
|
|
397
|
+
return { threadId, cwd, forkedFromId, threadSource };
|
|
368
398
|
}
|
|
369
399
|
|
|
370
400
|
function parseSessionJsonlTurns(content, {
|
|
@@ -559,36 +589,39 @@ function parseSessionJsonlTurns(content, {
|
|
|
559
589
|
if (!payload) {
|
|
560
590
|
continue;
|
|
561
591
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
const turn = ensureTurn(
|
|
567
|
-
turns,
|
|
568
|
-
turnsById,
|
|
569
|
-
responseItemTurnId(payload) || activeTurnId || `turn-line-${sourceLineNumber}`,
|
|
570
|
-
sessionThreadId,
|
|
571
|
-
entry.timestamp
|
|
572
|
-
);
|
|
573
|
-
applyHistoryTimeZone(turn, sessionTimeZone);
|
|
574
|
-
const item = normalizeResponseItemForHistory(payload, sourceLineNumber, {
|
|
575
|
-
cwd: sessionCwd,
|
|
576
|
-
toolCallsByCallId,
|
|
577
|
-
});
|
|
578
|
-
if (item) {
|
|
579
|
-
if (shouldSkipDuplicateProposedPlanMessage(turn, item)) {
|
|
592
|
+
const projectedPayloads = expandExecWrapperToolCall(payload);
|
|
593
|
+
for (const projectedPayload of projectedPayloads) {
|
|
594
|
+
rememberToolCallForHistory(projectedPayload, toolCallsByCallId);
|
|
595
|
+
if (shouldSkipResponseItemForHistory(projectedPayload, skippedCallIds)) {
|
|
580
596
|
continue;
|
|
581
597
|
}
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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);
|
|
588
624
|
}
|
|
589
|
-
applyHistoryTimeZone(item, sessionTimeZone);
|
|
590
|
-
applyHistoryAssistantSourceAlias(item, turn.id, assistantAliasOccurrencesByBaseKey);
|
|
591
|
-
addHistoryItemToTurn(turn, item);
|
|
592
625
|
}
|
|
593
626
|
}
|
|
594
627
|
}
|
|
@@ -1310,6 +1343,13 @@ function shouldSkipResponseItemForHistory(payload, skippedCallIds) {
|
|
|
1310
1343
|
return true;
|
|
1311
1344
|
}
|
|
1312
1345
|
|
|
1346
|
+
if (type === "tool_call" && isOrchestrationWaitCall(payload)) {
|
|
1347
|
+
if (callId) {
|
|
1348
|
+
skippedCallIds.add(callId);
|
|
1349
|
+
}
|
|
1350
|
+
return true;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1313
1353
|
if (type === "tool_call" && isSubagentOrchestrationCall(payload)) {
|
|
1314
1354
|
if (callId) {
|
|
1315
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
|
+
};
|