@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.
- package/dist/server/client/daemon-client.d.ts +4 -0
- package/dist/server/client/daemon-client.d.ts.map +1 -1
- package/dist/server/client/daemon-client.js +25 -0
- package/dist/server/client/daemon-client.js.map +1 -1
- package/dist/server/server/agent/agent-storage.d.ts +48 -0
- package/dist/server/server/agent/agent-storage.d.ts.map +1 -1
- package/dist/server/server/agent/agent-storage.js +16 -0
- package/dist/server/server/agent/agent-storage.js.map +1 -1
- package/dist/server/server/session.d.ts +4 -0
- package/dist/server/server/session.d.ts.map +1 -1
- package/dist/server/server/session.js +359 -91
- package/dist/server/server/session.js.map +1 -1
- package/dist/server/shared/messages.d.ts +296 -106
- package/dist/server/shared/messages.d.ts.map +1 -1
- package/dist/server/shared/messages.js +14 -0
- package/dist/server/shared/messages.js.map +1 -1
- package/dist/server/shared/project-grouping.d.ts +6 -0
- package/dist/server/shared/project-grouping.d.ts.map +1 -0
- package/dist/server/shared/project-grouping.js +62 -0
- package/dist/server/shared/project-grouping.js.map +1 -0
- package/dist/server/utils/worktree.d.ts +8 -0
- package/dist/server/utils/worktree.d.ts.map +1 -1
- package/dist/server/utils/worktree.js +61 -0
- package/dist/server/utils/worktree.js.map +1 -1
- package/package.json +2 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
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
|
|
3775
|
+
let projectedFetchLimit = projectedLimit;
|
|
3515
3776
|
let projectedWindow = selectTimelineWindowByProjectedLimit({
|
|
3516
3777
|
rows: timeline.rows,
|
|
3517
|
-
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,
|
|
3535
|
-
if (nextFetchLimit <=
|
|
3795
|
+
const nextFetchLimit = Math.min(maxRows, projectedFetchLimit * 2);
|
|
3796
|
+
if (nextFetchLimit <= projectedFetchLimit) {
|
|
3536
3797
|
break;
|
|
3537
3798
|
}
|
|
3538
|
-
|
|
3539
|
-
timeline =
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
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
|
|
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,
|
|
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,
|
|
3834
|
+
entries = projectTimelineRows(timeline.rows, provider, projection);
|
|
3567
3835
|
}
|
|
3568
3836
|
this.emit({
|
|
3569
3837
|
type: 'fetch_agent_timeline_response',
|