@junctionpanel/server 0.1.18 → 0.1.19

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.
@@ -17,7 +17,8 @@ import { projectTimelineRows, selectTimelineWindowByProjectedLimit, } from './ag
17
17
  import { DEFAULT_STRUCTURED_GENERATION_PROVIDERS, StructuredAgentFallbackError, StructuredAgentResponseError, generateStructuredAgentResponseWithFallback, } from './agent/agent-response-loop.js';
18
18
  import { isValidAgentProvider, AGENT_PROVIDER_IDS } from './agent/provider-manifest.js';
19
19
  import { listDirectoryEntries, readExplorerFile, getDownloadableFileInfo, } from './file-explorer/service.js';
20
- import { slugify, validateBranchSlug, listJunctionWorktrees, deleteJunctionWorktree, isJunctionOwnedWorktreeCwd, resolveJunctionWorktreeRootForCwd, createInRepoWorktree, } from '../utils/worktree.js';
20
+ import { slugify, validateBranchSlug, listJunctionWorktrees, deleteJunctionWorktree, isJunctionOwnedWorktreeCwd, resolveJunctionWorktreeRootForCwd, createInRepoWorktree, restoreInRepoWorktree, } from '../utils/worktree.js';
21
+ import { readJunctionWorktreeMetadata } from '../utils/worktree-metadata.js';
21
22
  import { runAsyncWorktreeBootstrap } from './worktree-bootstrap.js';
22
23
  import { getCheckoutDiff, getCheckoutStatus, getCheckoutStatusLite, listBranchSuggestions, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestStatus, resolveBaseRef, } from '../utils/checkout-git.js';
23
24
  import { getProjectIcon } from '../utils/project-icon.js';
@@ -26,11 +27,13 @@ import { searchHomeDirectories, searchWorkspaceEntries, searchGitRepositories, c
26
27
  import { cloneRepository } from '../utils/git-clone.js';
27
28
  import { initRepository } from '../utils/git-init.js';
28
29
  import { resolveClientMessageId } from './client-message-id.js';
30
+ import { deriveProjectGroupingKey, deriveProjectGroupingName } from '../shared/project-grouping.js';
29
31
  const execAsync = promisify(exec);
30
32
  const READ_ONLY_GIT_ENV = {
31
33
  ...process.env,
32
34
  GIT_OPTIONAL_LOCKS: '0',
33
35
  };
36
+ const DEFAULT_STORED_TIMELINE_FETCH_LIMIT = 200;
34
37
  const pendingAgentInitializations = new Map();
35
38
  const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_IDS[0];
36
39
  const CHECKOUT_DIFF_WATCH_DEBOUNCE_MS = 150;
@@ -38,67 +41,6 @@ const CHECKOUT_DIFF_FALLBACK_REFRESH_MS = 5000;
38
41
  const TERMINAL_STREAM_WINDOW_BYTES = 256 * 1024;
39
42
  const TERMINAL_STREAM_MAX_PENDING_BYTES = 2 * 1024 * 1024;
40
43
  const TERMINAL_STREAM_MAX_PENDING_CHUNKS = 2048;
41
- function deriveRemoteProjectKey(remoteUrl) {
42
- if (!remoteUrl) {
43
- return null;
44
- }
45
- const trimmed = remoteUrl.trim();
46
- if (!trimmed) {
47
- return null;
48
- }
49
- let host = null;
50
- let path = null;
51
- const scpLike = trimmed.match(/^[^@]+@([^:]+):(.+)$/);
52
- if (scpLike) {
53
- host = scpLike[1] ?? null;
54
- path = scpLike[2] ?? null;
55
- }
56
- else if (trimmed.includes('://')) {
57
- try {
58
- const parsed = new URL(trimmed);
59
- host = parsed.hostname || null;
60
- path = parsed.pathname ? parsed.pathname.replace(/^\//, '') : null;
61
- }
62
- catch {
63
- return null;
64
- }
65
- }
66
- if (!host || !path) {
67
- return null;
68
- }
69
- let cleanedPath = path.trim().replace(/^\/+/, '').replace(/\/+$/, '');
70
- if (cleanedPath.endsWith('.git')) {
71
- cleanedPath = cleanedPath.slice(0, -4);
72
- }
73
- if (!cleanedPath.includes('/')) {
74
- return null;
75
- }
76
- const cleanedHost = host.toLowerCase();
77
- if (cleanedHost === 'github.com') {
78
- return `remote:github.com/${cleanedPath}`;
79
- }
80
- return `remote:${cleanedHost}/${cleanedPath}`;
81
- }
82
- function deriveProjectGroupingKey(options) {
83
- const remoteKey = deriveRemoteProjectKey(options.remoteUrl);
84
- if (remoteKey) {
85
- return remoteKey;
86
- }
87
- const worktreeMarker = '.junction/';
88
- const idx = options.cwd.indexOf(worktreeMarker);
89
- if (idx !== -1) {
90
- return options.cwd.slice(0, idx).replace(/\/$/, '');
91
- }
92
- return options.cwd;
93
- }
94
- function deriveProjectGroupingName(projectKey) {
95
- const githubRemotePrefix = 'remote:github.com/';
96
- if (projectKey.startsWith(githubRemotePrefix)) {
97
- return projectKey.slice(githubRemotePrefix.length) || projectKey;
98
- }
99
- const segments = projectKey.split(/[\\/]/).filter(Boolean);
100
- return segments[segments.length - 1] || projectKey;
101
- }
102
44
  class SessionRequestError extends Error {
103
45
  constructor(code, message) {
104
46
  super(message);
@@ -431,6 +373,123 @@ export class Session {
431
373
  labels: record.labels,
432
374
  };
433
375
  }
376
+ fetchStoredTimeline(record, options) {
377
+ const rows = (record.timelineRows ?? []).map((row) => ({ ...row }));
378
+ const epoch = record.timelineEpoch ?? record.id;
379
+ const nextSeq = record.timelineNextSeq ??
380
+ (rows.length > 0 ? rows[rows.length - 1].seq + 1 : 1);
381
+ const minSeq = rows.length > 0 ? rows[0].seq : 0;
382
+ const maxSeq = rows.length > 0 ? rows[rows.length - 1].seq : 0;
383
+ const direction = options?.direction ?? 'tail';
384
+ const requestedLimit = options?.limit;
385
+ const limit = requestedLimit === undefined
386
+ ? DEFAULT_STORED_TIMELINE_FETCH_LIMIT
387
+ : Math.max(0, Math.floor(requestedLimit));
388
+ const cursor = options?.cursor;
389
+ const window = { minSeq, maxSeq, nextSeq };
390
+ if (cursor && cursor.epoch !== epoch) {
391
+ return {
392
+ epoch,
393
+ direction,
394
+ reset: true,
395
+ staleCursor: true,
396
+ gap: false,
397
+ window,
398
+ hasOlder: false,
399
+ hasNewer: false,
400
+ rows,
401
+ };
402
+ }
403
+ const selectAll = limit === 0;
404
+ const cloneRows = (items) => items.map((row) => ({ ...row }));
405
+ if (direction === 'after' && cursor && rows.length > 0 && cursor.seq < minSeq - 1) {
406
+ return {
407
+ epoch,
408
+ direction,
409
+ reset: true,
410
+ staleCursor: false,
411
+ gap: true,
412
+ window,
413
+ hasOlder: false,
414
+ hasNewer: false,
415
+ rows,
416
+ };
417
+ }
418
+ if (rows.length === 0) {
419
+ return {
420
+ epoch,
421
+ direction,
422
+ reset: false,
423
+ staleCursor: false,
424
+ gap: false,
425
+ window,
426
+ hasOlder: false,
427
+ hasNewer: false,
428
+ rows: [],
429
+ };
430
+ }
431
+ if (direction === 'tail') {
432
+ const selected = selectAll || limit >= rows.length ? rows : rows.slice(rows.length - limit);
433
+ return {
434
+ epoch,
435
+ direction,
436
+ reset: false,
437
+ staleCursor: false,
438
+ gap: false,
439
+ window,
440
+ hasOlder: selected.length > 0 && selected[0].seq > minSeq,
441
+ hasNewer: false,
442
+ rows: cloneRows(selected),
443
+ };
444
+ }
445
+ if (direction === 'after') {
446
+ const baseSeq = cursor?.seq ?? 0;
447
+ const startIdx = rows.findIndex((row) => row.seq > baseSeq);
448
+ if (startIdx < 0) {
449
+ return {
450
+ epoch,
451
+ direction,
452
+ reset: false,
453
+ staleCursor: false,
454
+ gap: false,
455
+ window,
456
+ hasOlder: baseSeq >= minSeq,
457
+ hasNewer: false,
458
+ rows: [],
459
+ };
460
+ }
461
+ const selected = selectAll ? rows.slice(startIdx) : rows.slice(startIdx, startIdx + limit);
462
+ const lastSelected = selected[selected.length - 1];
463
+ return {
464
+ epoch,
465
+ direction,
466
+ reset: false,
467
+ staleCursor: false,
468
+ gap: false,
469
+ window,
470
+ hasOlder: selected[0].seq > minSeq,
471
+ hasNewer: Boolean(lastSelected && lastSelected.seq < maxSeq),
472
+ rows: cloneRows(selected),
473
+ };
474
+ }
475
+ const beforeSeq = cursor?.seq ?? nextSeq;
476
+ const endExclusive = rows.findIndex((row) => row.seq >= beforeSeq);
477
+ const boundedRows = endExclusive < 0 ? rows : rows.slice(0, endExclusive);
478
+ const selected = selectAll || limit >= boundedRows.length
479
+ ? boundedRows
480
+ : boundedRows.slice(boundedRows.length - limit);
481
+ return {
482
+ epoch,
483
+ direction,
484
+ reset: false,
485
+ staleCursor: false,
486
+ gap: false,
487
+ window,
488
+ hasOlder: selected.length > 0 && selected[0].seq > minSeq,
489
+ hasNewer: endExclusive >= 0,
490
+ rows: cloneRows(selected),
491
+ };
492
+ }
434
493
  async ensureAgentLoaded(agentId) {
435
494
  const existing = this.agentManager.getAgent(agentId);
436
495
  if (existing) {
@@ -632,6 +691,36 @@ export class Session {
632
691
  this.sessionLogger.error({ err: error }, 'Failed to emit agent update');
633
692
  }
634
693
  }
694
+ async forwardStoredAgentRecordUpdate(record) {
695
+ try {
696
+ const subscription = this.agentUpdatesSubscription;
697
+ if (!subscription || record.internal) {
698
+ return;
699
+ }
700
+ const payload = this.buildStoredAgentPayload(record);
701
+ const project = await this.buildProjectPlacement(payload.cwd);
702
+ const matches = this.matchesAgentFilter({
703
+ agent: payload,
704
+ project,
705
+ filter: subscription.filter,
706
+ });
707
+ if (matches) {
708
+ this.bufferOrEmitAgentUpdate(subscription, {
709
+ kind: 'upsert',
710
+ agent: payload,
711
+ project,
712
+ });
713
+ return;
714
+ }
715
+ this.bufferOrEmitAgentUpdate(subscription, {
716
+ kind: 'remove',
717
+ agentId: payload.id,
718
+ });
719
+ }
720
+ catch (error) {
721
+ this.sessionLogger.error({ err: error, agentId: record.id }, 'Failed to emit stored agent update');
722
+ }
723
+ }
635
724
  /**
636
725
  * Main entry point for processing session messages
637
726
  */
@@ -653,6 +742,9 @@ export class Session {
653
742
  case 'archive_agent_request':
654
743
  await this.handleArchiveAgentRequest(msg.agentId, msg.requestId);
655
744
  break;
745
+ case 'unarchive_agent_request':
746
+ await this.handleUnarchiveAgentRequest(msg.agentId, msg.requestId);
747
+ break;
656
748
  case 'update_agent_request':
657
749
  await this.handleUpdateAgentRequest(msg.agentId, msg.name, msg.labels, msg.requestId);
658
750
  break;
@@ -978,7 +1070,8 @@ export class Session {
978
1070
  }
979
1071
  async handleArchiveAgentRequest(agentId, requestId) {
980
1072
  this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`);
981
- if (this.agentManager.getAgent(agentId)) {
1073
+ const liveAgent = this.agentManager.getAgent(agentId);
1074
+ if (liveAgent) {
982
1075
  await this.interruptAgentIfRunning(agentId);
983
1076
  }
984
1077
  const archivedAt = new Date().toISOString();
@@ -1000,9 +1093,15 @@ export class Session {
1000
1093
  archivedRecord = {
1001
1094
  ...archivedRecord,
1002
1095
  archivedAt,
1096
+ archivedWorktree: await this.buildArchivedWorktreeState(archivedRecord.cwd, archivedAt),
1003
1097
  };
1004
1098
  await this.agentStorage.upsert(archivedRecord);
1005
- this.agentManager.notifyAgentState(agentId);
1099
+ if (liveAgent) {
1100
+ this.agentManager.notifyAgentState(agentId);
1101
+ }
1102
+ else {
1103
+ await this.forwardStoredAgentRecordUpdate(archivedRecord);
1104
+ }
1006
1105
  this.emit({
1007
1106
  type: 'agent_archived',
1008
1107
  payload: {
@@ -1017,6 +1116,112 @@ export class Session {
1017
1116
  requestId,
1018
1117
  });
1019
1118
  }
1119
+ async handleUnarchiveAgentRequest(agentId, requestId) {
1120
+ this.sessionLogger.info({ agentId }, `Unarchiving agent ${agentId}`);
1121
+ const record = await this.agentStorage.get(agentId);
1122
+ if (!record) {
1123
+ throw new Error(`Agent not found: ${agentId}`);
1124
+ }
1125
+ let nextRecord = {
1126
+ ...record,
1127
+ archivedAt: null,
1128
+ };
1129
+ let restoredWorktree = null;
1130
+ if (record.archivedWorktree?.cleanupState === 'deleted') {
1131
+ restoredWorktree = await restoreInRepoWorktree({
1132
+ repoRoot: record.archivedWorktree.repoRoot,
1133
+ baseBranch: record.archivedWorktree.baseBranch,
1134
+ branchName: record.archivedWorktree.branchName,
1135
+ worktreeSlug: record.archivedWorktree.worktreeSlug,
1136
+ runSetup: false,
1137
+ });
1138
+ nextRecord = {
1139
+ ...nextRecord,
1140
+ cwd: restoredWorktree.worktreePath,
1141
+ archivedWorktree: {
1142
+ ...record.archivedWorktree,
1143
+ originalCwd: restoredWorktree.worktreePath,
1144
+ cleanupState: 'active',
1145
+ cleanedUpAt: null,
1146
+ },
1147
+ };
1148
+ }
1149
+ await this.agentStorage.upsert(nextRecord);
1150
+ const liveAgent = this.agentManager.getAgent(agentId);
1151
+ if (liveAgent) {
1152
+ this.agentManager.notifyAgentState(agentId);
1153
+ }
1154
+ else {
1155
+ await this.forwardStoredAgentRecordUpdate(nextRecord);
1156
+ }
1157
+ this.emit({
1158
+ type: 'agent_unarchived',
1159
+ payload: {
1160
+ agentId,
1161
+ requestId,
1162
+ },
1163
+ });
1164
+ if (restoredWorktree) {
1165
+ void runAsyncWorktreeBootstrap({
1166
+ agentId,
1167
+ worktree: restoredWorktree,
1168
+ terminalManager: this.terminalManager,
1169
+ appendTimelineItem: (item) => appendTimelineItemIfAgentKnown({
1170
+ agentManager: this.agentManager,
1171
+ agentId,
1172
+ item,
1173
+ }),
1174
+ emitLiveTimelineItem: (item) => emitLiveTimelineItemIfAgentKnown({
1175
+ agentManager: this.agentManager,
1176
+ agentId,
1177
+ item,
1178
+ }),
1179
+ logger: this.sessionLogger,
1180
+ });
1181
+ }
1182
+ }
1183
+ async buildArchivedWorktreeState(cwd, archivedAt) {
1184
+ try {
1185
+ const ownership = await isJunctionOwnedWorktreeCwd(cwd, {
1186
+ junctionHome: this.junctionHome,
1187
+ });
1188
+ if (!ownership.allowed || !ownership.repoRoot) {
1189
+ return null;
1190
+ }
1191
+ const resolvedWorktree = await resolveJunctionWorktreeRootForCwd(cwd, {
1192
+ junctionHome: this.junctionHome,
1193
+ });
1194
+ if (!resolvedWorktree) {
1195
+ return null;
1196
+ }
1197
+ const metadata = readJunctionWorktreeMetadata(resolvedWorktree.worktreePath);
1198
+ if (!metadata?.baseRefName) {
1199
+ return null;
1200
+ }
1201
+ const { stdout } = await execAsync('git branch --show-current', {
1202
+ cwd: resolvedWorktree.worktreePath,
1203
+ env: READ_ONLY_GIT_ENV,
1204
+ });
1205
+ const branchName = stdout.trim();
1206
+ if (!branchName) {
1207
+ return null;
1208
+ }
1209
+ return {
1210
+ repoRoot: ownership.repoRoot,
1211
+ baseBranch: metadata.baseRefName,
1212
+ branchName,
1213
+ worktreeSlug: basename(resolvedWorktree.worktreePath),
1214
+ originalCwd: resolvedWorktree.worktreePath,
1215
+ cleanupState: 'active',
1216
+ archivedAt,
1217
+ cleanedUpAt: null,
1218
+ };
1219
+ }
1220
+ catch (error) {
1221
+ this.sessionLogger.warn({ err: error, cwd }, 'Failed to capture archived worktree restore metadata');
1222
+ return null;
1223
+ }
1224
+ }
1020
1225
  async getArchivedAt(agentId) {
1021
1226
  const record = await this.agentStorage.get(agentId);
1022
1227
  return record?.archivedAt ?? null;
@@ -1095,6 +1300,11 @@ export class Session {
1095
1300
  */
1096
1301
  async handleSendAgentMessage(agentId, text, messageId, images, runOptions) {
1097
1302
  this.sessionLogger.info({ agentId, textPreview: text.substring(0, 50), imageCount: images?.length ?? 0 }, `Sending text to agent ${agentId}${images && images.length > 0 ? ` with ${images.length} image attachment(s)` : ''}`);
1303
+ const archivedAt = await this.getArchivedAt(agentId);
1304
+ if (archivedAt) {
1305
+ this.handleAgentRunError(agentId, new Error(`Agent ${agentId} is archived`), 'Refusing to send prompt to archived agent');
1306
+ return;
1307
+ }
1098
1308
  try {
1099
1309
  await this.ensureAgentLoaded(agentId);
1100
1310
  }
@@ -1102,11 +1312,6 @@ export class Session {
1102
1312
  this.handleAgentRunError(agentId, error, 'Failed to initialize agent before sending prompt');
1103
1313
  return;
1104
1314
  }
1105
- const archivedAt = await this.getArchivedAt(agentId);
1106
- if (archivedAt) {
1107
- this.handleAgentRunError(agentId, new Error(`Agent ${agentId} is archived`), 'Refusing to send prompt to archived agent');
1108
- return;
1109
- }
1110
1315
  try {
1111
1316
  await this.interruptAgentIfRunning(agentId);
1112
1317
  }
@@ -2757,16 +2962,32 @@ export class Session {
2757
2962
  targetPath = resolvedWorktree.worktreePath;
2758
2963
  }
2759
2964
  const removedAgents = new Set();
2965
+ const preservedArchivedRecords = new Map();
2966
+ const cleanedUpAt = new Date().toISOString();
2760
2967
  const agents = this.agentManager.listAgents();
2761
2968
  for (const agent of agents) {
2762
2969
  if (this.isPathWithinRoot(targetPath, agent.cwd)) {
2763
- removedAgents.add(agent.id);
2764
2970
  try {
2765
2971
  await this.agentManager.closeAgent(agent.id);
2766
2972
  }
2767
2973
  catch {
2768
2974
  // ignore cleanup errors
2769
2975
  }
2976
+ const record = await this.agentStorage.get(agent.id);
2977
+ if (record?.archivedAt) {
2978
+ preservedArchivedRecords.set(agent.id, {
2979
+ ...record,
2980
+ archivedWorktree: record.archivedWorktree
2981
+ ? {
2982
+ ...record.archivedWorktree,
2983
+ cleanupState: 'deleted',
2984
+ cleanedUpAt,
2985
+ }
2986
+ : record.archivedWorktree,
2987
+ });
2988
+ continue;
2989
+ }
2990
+ removedAgents.add(agent.id);
2770
2991
  try {
2771
2992
  await this.agentStorage.remove(agent.id);
2772
2993
  }
@@ -2778,6 +2999,19 @@ export class Session {
2778
2999
  const registryRecords = await this.agentStorage.list();
2779
3000
  for (const record of registryRecords) {
2780
3001
  if (this.isPathWithinRoot(targetPath, record.cwd)) {
3002
+ if (record.archivedAt) {
3003
+ preservedArchivedRecords.set(record.id, {
3004
+ ...record,
3005
+ archivedWorktree: record.archivedWorktree
3006
+ ? {
3007
+ ...record.archivedWorktree,
3008
+ cleanupState: 'deleted',
3009
+ cleanedUpAt,
3010
+ }
3011
+ : record.archivedWorktree,
3012
+ });
3013
+ continue;
3014
+ }
2781
3015
  removedAgents.add(record.id);
2782
3016
  try {
2783
3017
  await this.agentStorage.remove(record.id);
@@ -2793,6 +3027,9 @@ export class Session {
2793
3027
  worktreePath: targetPath,
2794
3028
  junctionHome: this.junctionHome,
2795
3029
  });
3030
+ for (const record of preservedArchivedRecords.values()) {
3031
+ await this.agentStorage.upsert(record);
3032
+ }
2796
3033
  for (const agentId of removedAgents) {
2797
3034
  this.emit({
2798
3035
  type: 'agent_deleted',
@@ -3496,14 +3733,38 @@ export class Session {
3496
3733
  }
3497
3734
  : undefined;
3498
3735
  try {
3499
- const snapshot = await this.ensureAgentLoaded(msg.agentId);
3500
- let timeline = this.agentManager.fetchTimeline(msg.agentId, {
3501
- direction,
3502
- cursor,
3503
- limit: shouldLimitByProjectedWindow && typeof requestedLimit === 'number'
3504
- ? Math.max(1, Math.floor(requestedLimit))
3505
- : limit,
3506
- });
3736
+ const liveAgent = this.agentManager.getAgent(msg.agentId);
3737
+ const storedRecord = liveAgent ? null : await this.agentStorage.get(msg.agentId);
3738
+ const fetchLimit = shouldLimitByProjectedWindow && typeof requestedLimit === 'number'
3739
+ ? Math.max(1, Math.floor(requestedLimit))
3740
+ : limit;
3741
+ let provider;
3742
+ let timeline;
3743
+ if (liveAgent) {
3744
+ provider = liveAgent.provider;
3745
+ timeline = this.agentManager.fetchTimeline(msg.agentId, {
3746
+ direction,
3747
+ cursor,
3748
+ limit: fetchLimit,
3749
+ });
3750
+ }
3751
+ else if (storedRecord?.archivedAt) {
3752
+ provider = coerceAgentProvider(this.sessionLogger, storedRecord.provider, storedRecord.id);
3753
+ timeline = this.fetchStoredTimeline(storedRecord, {
3754
+ direction,
3755
+ cursor,
3756
+ limit: fetchLimit,
3757
+ });
3758
+ }
3759
+ else {
3760
+ const snapshot = await this.ensureAgentLoaded(msg.agentId);
3761
+ provider = snapshot.provider;
3762
+ timeline = this.agentManager.fetchTimeline(msg.agentId, {
3763
+ direction,
3764
+ cursor,
3765
+ limit: fetchLimit,
3766
+ });
3767
+ }
3507
3768
  let hasOlder = timeline.hasOlder;
3508
3769
  let hasNewer = timeline.hasNewer;
3509
3770
  let startCursor = null;
@@ -3511,10 +3772,10 @@ export class Session {
3511
3772
  let entries;
3512
3773
  if (shouldLimitByProjectedWindow) {
3513
3774
  const projectedLimit = Math.max(1, Math.floor(requestedLimit));
3514
- let fetchLimit = projectedLimit;
3775
+ let projectedFetchLimit = projectedLimit;
3515
3776
  let projectedWindow = selectTimelineWindowByProjectedLimit({
3516
3777
  rows: timeline.rows,
3517
- provider: snapshot.provider,
3778
+ provider,
3518
3779
  direction,
3519
3780
  limit: projectedLimit,
3520
3781
  collapseToolLifecycle: false,
@@ -3531,26 +3792,33 @@ export class Session {
3531
3792
  break;
3532
3793
  }
3533
3794
  const maxRows = Math.max(0, timeline.window.maxSeq - timeline.window.minSeq + 1);
3534
- const nextFetchLimit = Math.min(maxRows, fetchLimit * 2);
3535
- if (nextFetchLimit <= fetchLimit) {
3795
+ const nextFetchLimit = Math.min(maxRows, projectedFetchLimit * 2);
3796
+ if (nextFetchLimit <= projectedFetchLimit) {
3536
3797
  break;
3537
3798
  }
3538
- fetchLimit = nextFetchLimit;
3539
- timeline = this.agentManager.fetchTimeline(msg.agentId, {
3540
- direction,
3541
- cursor,
3542
- limit: fetchLimit,
3543
- });
3799
+ projectedFetchLimit = nextFetchLimit;
3800
+ timeline =
3801
+ storedRecord?.archivedAt && !liveAgent
3802
+ ? this.fetchStoredTimeline(storedRecord, {
3803
+ direction,
3804
+ cursor,
3805
+ limit: projectedFetchLimit,
3806
+ })
3807
+ : this.agentManager.fetchTimeline(msg.agentId, {
3808
+ direction,
3809
+ cursor,
3810
+ limit: projectedFetchLimit,
3811
+ });
3544
3812
  projectedWindow = selectTimelineWindowByProjectedLimit({
3545
3813
  rows: timeline.rows,
3546
- provider: snapshot.provider,
3814
+ provider,
3547
3815
  direction,
3548
3816
  limit: projectedLimit,
3549
3817
  collapseToolLifecycle: false,
3550
3818
  });
3551
3819
  }
3552
3820
  const selectedRows = projectedWindow.selectedRows;
3553
- entries = projectTimelineRows(selectedRows, snapshot.provider, projection);
3821
+ entries = projectTimelineRows(selectedRows, provider, projection);
3554
3822
  if (projectedWindow.minSeq !== null && projectedWindow.maxSeq !== null) {
3555
3823
  startCursor = { epoch: timeline.epoch, seq: projectedWindow.minSeq };
3556
3824
  endCursor = { epoch: timeline.epoch, seq: projectedWindow.maxSeq };
@@ -3563,7 +3831,7 @@ export class Session {
3563
3831
  const lastRow = timeline.rows[timeline.rows.length - 1];
3564
3832
  startCursor = firstRow ? { epoch: timeline.epoch, seq: firstRow.seq } : null;
3565
3833
  endCursor = lastRow ? { epoch: timeline.epoch, seq: lastRow.seq } : null;
3566
- entries = projectTimelineRows(timeline.rows, snapshot.provider, projection);
3834
+ entries = projectTimelineRows(timeline.rows, provider, projection);
3567
3835
  }
3568
3836
  this.emit({
3569
3837
  type: 'fetch_agent_timeline_response',