@junctionpanel/server 0.1.18 → 0.1.20
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 +8 -2
- package/dist/server/client/daemon-client.d.ts.map +1 -1
- package/dist/server/client/daemon-client.js +27 -1
- 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/agent/provider-manifest.d.ts.map +1 -1
- package/dist/server/server/agent/provider-manifest.js +29 -0
- package/dist/server/server/agent/provider-manifest.js.map +1 -1
- package/dist/server/server/agent/provider-registry.d.ts.map +1 -1
- package/dist/server/server/agent/provider-registry.js +8 -0
- package/dist/server/server/agent/provider-registry.js.map +1 -1
- package/dist/server/server/agent/providers/gemini-agent.d.ts +425 -0
- package/dist/server/server/agent/providers/gemini-agent.d.ts.map +1 -0
- package/dist/server/server/agent/providers/gemini-agent.js +973 -0
- package/dist/server/server/agent/providers/gemini-agent.js.map +1 -0
- package/dist/server/server/persistence-hooks.d.ts.map +1 -1
- package/dist/server/server/persistence-hooks.js +4 -1
- package/dist/server/server/persistence-hooks.js.map +1 -1
- package/dist/server/server/session.d.ts +6 -1
- package/dist/server/server/session.d.ts.map +1 -1
- package/dist/server/server/session.js +434 -146
- package/dist/server/server/session.js.map +1 -1
- package/dist/server/shared/messages.d.ts +314 -106
- package/dist/server/shared/messages.d.ts.map +1 -1
- package/dist/server/shared/messages.js +16 -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,7 @@ 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
|
-
|
|
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
|
-
}
|
|
44
|
+
const DIRTY_WORKTREE_CONFIRMATION_REQUIRED = 'dirty_worktree_confirmation_required';
|
|
102
45
|
class SessionRequestError extends Error {
|
|
103
46
|
constructor(code, message) {
|
|
104
47
|
super(message);
|
|
@@ -431,6 +374,123 @@ export class Session {
|
|
|
431
374
|
labels: record.labels,
|
|
432
375
|
};
|
|
433
376
|
}
|
|
377
|
+
fetchStoredTimeline(record, options) {
|
|
378
|
+
const rows = (record.timelineRows ?? []).map((row) => ({ ...row }));
|
|
379
|
+
const epoch = record.timelineEpoch ?? record.id;
|
|
380
|
+
const nextSeq = record.timelineNextSeq ??
|
|
381
|
+
(rows.length > 0 ? rows[rows.length - 1].seq + 1 : 1);
|
|
382
|
+
const minSeq = rows.length > 0 ? rows[0].seq : 0;
|
|
383
|
+
const maxSeq = rows.length > 0 ? rows[rows.length - 1].seq : 0;
|
|
384
|
+
const direction = options?.direction ?? 'tail';
|
|
385
|
+
const requestedLimit = options?.limit;
|
|
386
|
+
const limit = requestedLimit === undefined
|
|
387
|
+
? DEFAULT_STORED_TIMELINE_FETCH_LIMIT
|
|
388
|
+
: Math.max(0, Math.floor(requestedLimit));
|
|
389
|
+
const cursor = options?.cursor;
|
|
390
|
+
const window = { minSeq, maxSeq, nextSeq };
|
|
391
|
+
if (cursor && cursor.epoch !== epoch) {
|
|
392
|
+
return {
|
|
393
|
+
epoch,
|
|
394
|
+
direction,
|
|
395
|
+
reset: true,
|
|
396
|
+
staleCursor: true,
|
|
397
|
+
gap: false,
|
|
398
|
+
window,
|
|
399
|
+
hasOlder: false,
|
|
400
|
+
hasNewer: false,
|
|
401
|
+
rows,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
const selectAll = limit === 0;
|
|
405
|
+
const cloneRows = (items) => items.map((row) => ({ ...row }));
|
|
406
|
+
if (direction === 'after' && cursor && rows.length > 0 && cursor.seq < minSeq - 1) {
|
|
407
|
+
return {
|
|
408
|
+
epoch,
|
|
409
|
+
direction,
|
|
410
|
+
reset: true,
|
|
411
|
+
staleCursor: false,
|
|
412
|
+
gap: true,
|
|
413
|
+
window,
|
|
414
|
+
hasOlder: false,
|
|
415
|
+
hasNewer: false,
|
|
416
|
+
rows,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
if (rows.length === 0) {
|
|
420
|
+
return {
|
|
421
|
+
epoch,
|
|
422
|
+
direction,
|
|
423
|
+
reset: false,
|
|
424
|
+
staleCursor: false,
|
|
425
|
+
gap: false,
|
|
426
|
+
window,
|
|
427
|
+
hasOlder: false,
|
|
428
|
+
hasNewer: false,
|
|
429
|
+
rows: [],
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
if (direction === 'tail') {
|
|
433
|
+
const selected = selectAll || limit >= rows.length ? rows : rows.slice(rows.length - limit);
|
|
434
|
+
return {
|
|
435
|
+
epoch,
|
|
436
|
+
direction,
|
|
437
|
+
reset: false,
|
|
438
|
+
staleCursor: false,
|
|
439
|
+
gap: false,
|
|
440
|
+
window,
|
|
441
|
+
hasOlder: selected.length > 0 && selected[0].seq > minSeq,
|
|
442
|
+
hasNewer: false,
|
|
443
|
+
rows: cloneRows(selected),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
if (direction === 'after') {
|
|
447
|
+
const baseSeq = cursor?.seq ?? 0;
|
|
448
|
+
const startIdx = rows.findIndex((row) => row.seq > baseSeq);
|
|
449
|
+
if (startIdx < 0) {
|
|
450
|
+
return {
|
|
451
|
+
epoch,
|
|
452
|
+
direction,
|
|
453
|
+
reset: false,
|
|
454
|
+
staleCursor: false,
|
|
455
|
+
gap: false,
|
|
456
|
+
window,
|
|
457
|
+
hasOlder: baseSeq >= minSeq,
|
|
458
|
+
hasNewer: false,
|
|
459
|
+
rows: [],
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
const selected = selectAll ? rows.slice(startIdx) : rows.slice(startIdx, startIdx + limit);
|
|
463
|
+
const lastSelected = selected[selected.length - 1];
|
|
464
|
+
return {
|
|
465
|
+
epoch,
|
|
466
|
+
direction,
|
|
467
|
+
reset: false,
|
|
468
|
+
staleCursor: false,
|
|
469
|
+
gap: false,
|
|
470
|
+
window,
|
|
471
|
+
hasOlder: selected[0].seq > minSeq,
|
|
472
|
+
hasNewer: Boolean(lastSelected && lastSelected.seq < maxSeq),
|
|
473
|
+
rows: cloneRows(selected),
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
const beforeSeq = cursor?.seq ?? nextSeq;
|
|
477
|
+
const endExclusive = rows.findIndex((row) => row.seq >= beforeSeq);
|
|
478
|
+
const boundedRows = endExclusive < 0 ? rows : rows.slice(0, endExclusive);
|
|
479
|
+
const selected = selectAll || limit >= boundedRows.length
|
|
480
|
+
? boundedRows
|
|
481
|
+
: boundedRows.slice(boundedRows.length - limit);
|
|
482
|
+
return {
|
|
483
|
+
epoch,
|
|
484
|
+
direction,
|
|
485
|
+
reset: false,
|
|
486
|
+
staleCursor: false,
|
|
487
|
+
gap: false,
|
|
488
|
+
window,
|
|
489
|
+
hasOlder: selected.length > 0 && selected[0].seq > minSeq,
|
|
490
|
+
hasNewer: endExclusive >= 0,
|
|
491
|
+
rows: cloneRows(selected),
|
|
492
|
+
};
|
|
493
|
+
}
|
|
434
494
|
async ensureAgentLoaded(agentId) {
|
|
435
495
|
const existing = this.agentManager.getAgent(agentId);
|
|
436
496
|
if (existing) {
|
|
@@ -632,6 +692,36 @@ export class Session {
|
|
|
632
692
|
this.sessionLogger.error({ err: error }, 'Failed to emit agent update');
|
|
633
693
|
}
|
|
634
694
|
}
|
|
695
|
+
async forwardStoredAgentRecordUpdate(record) {
|
|
696
|
+
try {
|
|
697
|
+
const subscription = this.agentUpdatesSubscription;
|
|
698
|
+
if (!subscription || record.internal) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
const payload = this.buildStoredAgentPayload(record);
|
|
702
|
+
const project = await this.buildProjectPlacement(payload.cwd);
|
|
703
|
+
const matches = this.matchesAgentFilter({
|
|
704
|
+
agent: payload,
|
|
705
|
+
project,
|
|
706
|
+
filter: subscription.filter,
|
|
707
|
+
});
|
|
708
|
+
if (matches) {
|
|
709
|
+
this.bufferOrEmitAgentUpdate(subscription, {
|
|
710
|
+
kind: 'upsert',
|
|
711
|
+
agent: payload,
|
|
712
|
+
project,
|
|
713
|
+
});
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
this.bufferOrEmitAgentUpdate(subscription, {
|
|
717
|
+
kind: 'remove',
|
|
718
|
+
agentId: payload.id,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
this.sessionLogger.error({ err: error, agentId: record.id }, 'Failed to emit stored agent update');
|
|
723
|
+
}
|
|
724
|
+
}
|
|
635
725
|
/**
|
|
636
726
|
* Main entry point for processing session messages
|
|
637
727
|
*/
|
|
@@ -651,7 +741,10 @@ export class Session {
|
|
|
651
741
|
await this.handleDeleteAgentRequest(msg.agentId, msg.requestId);
|
|
652
742
|
break;
|
|
653
743
|
case 'archive_agent_request':
|
|
654
|
-
await this.handleArchiveAgentRequest(msg.agentId, msg.requestId);
|
|
744
|
+
await this.handleArchiveAgentRequest(msg.agentId, msg.requestId, msg.dirtyWorktreeBehavior ?? 'reject');
|
|
745
|
+
break;
|
|
746
|
+
case 'unarchive_agent_request':
|
|
747
|
+
await this.handleUnarchiveAgentRequest(msg.agentId, msg.requestId);
|
|
655
748
|
break;
|
|
656
749
|
case 'update_agent_request':
|
|
657
750
|
await this.handleUpdateAgentRequest(msg.agentId, msg.name, msg.labels, msg.requestId);
|
|
@@ -821,13 +914,15 @@ export class Session {
|
|
|
821
914
|
const requestId = msg.requestId;
|
|
822
915
|
if (typeof requestId === 'string') {
|
|
823
916
|
try {
|
|
917
|
+
const message = error instanceof SessionRequestError ? error.message : 'Request failed';
|
|
918
|
+
const code = error instanceof SessionRequestError ? error.code : 'handler_error';
|
|
824
919
|
this.emit({
|
|
825
920
|
type: 'rpc_error',
|
|
826
921
|
payload: {
|
|
827
922
|
requestId,
|
|
828
923
|
requestType: msg.type,
|
|
829
|
-
error:
|
|
830
|
-
code
|
|
924
|
+
error: message,
|
|
925
|
+
code,
|
|
831
926
|
},
|
|
832
927
|
});
|
|
833
928
|
}
|
|
@@ -976,9 +1071,10 @@ export class Session {
|
|
|
976
1071
|
});
|
|
977
1072
|
}
|
|
978
1073
|
}
|
|
979
|
-
async handleArchiveAgentRequest(agentId, requestId) {
|
|
1074
|
+
async handleArchiveAgentRequest(agentId, requestId, dirtyWorktreeBehavior) {
|
|
980
1075
|
this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`);
|
|
981
|
-
|
|
1076
|
+
const liveAgent = this.agentManager.getAgent(agentId);
|
|
1077
|
+
if (liveAgent) {
|
|
982
1078
|
await this.interruptAgentIfRunning(agentId);
|
|
983
1079
|
}
|
|
984
1080
|
const archivedAt = new Date().toISOString();
|
|
@@ -997,12 +1093,27 @@ export class Session {
|
|
|
997
1093
|
throw new Error(`Agent not found in storage after snapshot: ${agentId}`);
|
|
998
1094
|
}
|
|
999
1095
|
}
|
|
1096
|
+
const worktreeArchiveTarget = await this.resolveWorktreeArchiveTargetAfterAgentArchive({
|
|
1097
|
+
archivedAgentId: agentId,
|
|
1098
|
+
archivedAgentCwd: archivedRecord.cwd,
|
|
1099
|
+
});
|
|
1100
|
+
if (worktreeArchiveTarget &&
|
|
1101
|
+
dirtyWorktreeBehavior !== 'discard' &&
|
|
1102
|
+
(await this.hasDirtyWorktreeState(worktreeArchiveTarget.targetPath))) {
|
|
1103
|
+
throw new SessionRequestError(DIRTY_WORKTREE_CONFIRMATION_REQUIRED, 'Archiving this agent will discard uncommitted or untracked files in the worktree.');
|
|
1104
|
+
}
|
|
1000
1105
|
archivedRecord = {
|
|
1001
1106
|
...archivedRecord,
|
|
1002
1107
|
archivedAt,
|
|
1108
|
+
archivedWorktree: await this.buildArchivedWorktreeState(archivedRecord.cwd, archivedAt),
|
|
1003
1109
|
};
|
|
1004
1110
|
await this.agentStorage.upsert(archivedRecord);
|
|
1005
|
-
|
|
1111
|
+
if (liveAgent) {
|
|
1112
|
+
this.agentManager.notifyAgentState(agentId);
|
|
1113
|
+
}
|
|
1114
|
+
else {
|
|
1115
|
+
await this.forwardStoredAgentRecordUpdate(archivedRecord);
|
|
1116
|
+
}
|
|
1006
1117
|
this.emit({
|
|
1007
1118
|
type: 'agent_archived',
|
|
1008
1119
|
payload: {
|
|
@@ -1011,11 +1122,124 @@ export class Session {
|
|
|
1011
1122
|
requestId,
|
|
1012
1123
|
},
|
|
1013
1124
|
});
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1125
|
+
if (worktreeArchiveTarget) {
|
|
1126
|
+
try {
|
|
1127
|
+
await this.archiveJunctionWorktree({
|
|
1128
|
+
targetPath: worktreeArchiveTarget.targetPath,
|
|
1129
|
+
repoRoot: worktreeArchiveTarget.repoRoot,
|
|
1130
|
+
requestId,
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
catch (error) {
|
|
1134
|
+
this.sessionLogger.warn({ err: error, agentId, cwd: archivedRecord.cwd }, 'Failed to auto-archive worktree after agent archive');
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
async handleUnarchiveAgentRequest(agentId, requestId) {
|
|
1139
|
+
this.sessionLogger.info({ agentId }, `Unarchiving agent ${agentId}`);
|
|
1140
|
+
const record = await this.agentStorage.get(agentId);
|
|
1141
|
+
if (!record) {
|
|
1142
|
+
throw new Error(`Agent not found: ${agentId}`);
|
|
1143
|
+
}
|
|
1144
|
+
let nextRecord = {
|
|
1145
|
+
...record,
|
|
1146
|
+
archivedAt: null,
|
|
1147
|
+
};
|
|
1148
|
+
let restoredWorktree = null;
|
|
1149
|
+
if (record.archivedWorktree?.cleanupState === 'deleted') {
|
|
1150
|
+
restoredWorktree = await restoreInRepoWorktree({
|
|
1151
|
+
repoRoot: record.archivedWorktree.repoRoot,
|
|
1152
|
+
baseBranch: record.archivedWorktree.baseBranch,
|
|
1153
|
+
branchName: record.archivedWorktree.branchName,
|
|
1154
|
+
worktreeSlug: record.archivedWorktree.worktreeSlug,
|
|
1155
|
+
runSetup: false,
|
|
1156
|
+
});
|
|
1157
|
+
nextRecord = {
|
|
1158
|
+
...nextRecord,
|
|
1159
|
+
cwd: restoredWorktree.worktreePath,
|
|
1160
|
+
archivedWorktree: {
|
|
1161
|
+
...record.archivedWorktree,
|
|
1162
|
+
originalCwd: restoredWorktree.worktreePath,
|
|
1163
|
+
cleanupState: 'active',
|
|
1164
|
+
cleanedUpAt: null,
|
|
1165
|
+
},
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
await this.agentStorage.upsert(nextRecord);
|
|
1169
|
+
const liveAgent = this.agentManager.getAgent(agentId);
|
|
1170
|
+
if (liveAgent) {
|
|
1171
|
+
this.agentManager.notifyAgentState(agentId);
|
|
1172
|
+
}
|
|
1173
|
+
else {
|
|
1174
|
+
await this.forwardStoredAgentRecordUpdate(nextRecord);
|
|
1175
|
+
}
|
|
1176
|
+
this.emit({
|
|
1177
|
+
type: 'agent_unarchived',
|
|
1178
|
+
payload: {
|
|
1179
|
+
agentId,
|
|
1180
|
+
requestId,
|
|
1181
|
+
},
|
|
1018
1182
|
});
|
|
1183
|
+
if (restoredWorktree) {
|
|
1184
|
+
void runAsyncWorktreeBootstrap({
|
|
1185
|
+
agentId,
|
|
1186
|
+
worktree: restoredWorktree,
|
|
1187
|
+
terminalManager: this.terminalManager,
|
|
1188
|
+
appendTimelineItem: (item) => appendTimelineItemIfAgentKnown({
|
|
1189
|
+
agentManager: this.agentManager,
|
|
1190
|
+
agentId,
|
|
1191
|
+
item,
|
|
1192
|
+
}),
|
|
1193
|
+
emitLiveTimelineItem: (item) => emitLiveTimelineItemIfAgentKnown({
|
|
1194
|
+
agentManager: this.agentManager,
|
|
1195
|
+
agentId,
|
|
1196
|
+
item,
|
|
1197
|
+
}),
|
|
1198
|
+
logger: this.sessionLogger,
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
async buildArchivedWorktreeState(cwd, archivedAt) {
|
|
1203
|
+
try {
|
|
1204
|
+
const ownership = await isJunctionOwnedWorktreeCwd(cwd, {
|
|
1205
|
+
junctionHome: this.junctionHome,
|
|
1206
|
+
});
|
|
1207
|
+
if (!ownership.allowed || !ownership.repoRoot) {
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
const resolvedWorktree = await resolveJunctionWorktreeRootForCwd(cwd, {
|
|
1211
|
+
junctionHome: this.junctionHome,
|
|
1212
|
+
});
|
|
1213
|
+
if (!resolvedWorktree) {
|
|
1214
|
+
return null;
|
|
1215
|
+
}
|
|
1216
|
+
const metadata = readJunctionWorktreeMetadata(resolvedWorktree.worktreePath);
|
|
1217
|
+
if (!metadata?.baseRefName) {
|
|
1218
|
+
return null;
|
|
1219
|
+
}
|
|
1220
|
+
const { stdout } = await execAsync('git branch --show-current', {
|
|
1221
|
+
cwd: resolvedWorktree.worktreePath,
|
|
1222
|
+
env: READ_ONLY_GIT_ENV,
|
|
1223
|
+
});
|
|
1224
|
+
const branchName = stdout.trim();
|
|
1225
|
+
if (!branchName) {
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
1228
|
+
return {
|
|
1229
|
+
repoRoot: ownership.repoRoot,
|
|
1230
|
+
baseBranch: metadata.baseRefName,
|
|
1231
|
+
branchName,
|
|
1232
|
+
worktreeSlug: basename(resolvedWorktree.worktreePath),
|
|
1233
|
+
originalCwd: resolvedWorktree.worktreePath,
|
|
1234
|
+
cleanupState: 'active',
|
|
1235
|
+
archivedAt,
|
|
1236
|
+
cleanedUpAt: null,
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
catch (error) {
|
|
1240
|
+
this.sessionLogger.warn({ err: error, cwd }, 'Failed to capture archived worktree restore metadata');
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1019
1243
|
}
|
|
1020
1244
|
async getArchivedAt(agentId) {
|
|
1021
1245
|
const record = await this.agentStorage.get(agentId);
|
|
@@ -1095,6 +1319,11 @@ export class Session {
|
|
|
1095
1319
|
*/
|
|
1096
1320
|
async handleSendAgentMessage(agentId, text, messageId, images, runOptions) {
|
|
1097
1321
|
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)` : ''}`);
|
|
1322
|
+
const archivedAt = await this.getArchivedAt(agentId);
|
|
1323
|
+
if (archivedAt) {
|
|
1324
|
+
this.handleAgentRunError(agentId, new Error(`Agent ${agentId} is archived`), 'Refusing to send prompt to archived agent');
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1098
1327
|
try {
|
|
1099
1328
|
await this.ensureAgentLoaded(agentId);
|
|
1100
1329
|
}
|
|
@@ -1102,11 +1331,6 @@ export class Session {
|
|
|
1102
1331
|
this.handleAgentRunError(agentId, error, 'Failed to initialize agent before sending prompt');
|
|
1103
1332
|
return;
|
|
1104
1333
|
}
|
|
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
1334
|
try {
|
|
1111
1335
|
await this.interruptAgentIfRunning(agentId);
|
|
1112
1336
|
}
|
|
@@ -2695,58 +2919,59 @@ export class Session {
|
|
|
2695
2919
|
});
|
|
2696
2920
|
}
|
|
2697
2921
|
}
|
|
2698
|
-
async
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
return false;
|
|
2718
|
-
}
|
|
2719
|
-
return this.isPathWithinRoot(targetPath, record.cwd);
|
|
2720
|
-
});
|
|
2721
|
-
if (hasRemainingNonArchivedRecord) {
|
|
2722
|
-
return;
|
|
2922
|
+
async resolveWorktreeArchiveTargetAfterAgentArchive(options) {
|
|
2923
|
+
const ownership = await isJunctionOwnedWorktreeCwd(options.archivedAgentCwd, {
|
|
2924
|
+
junctionHome: this.junctionHome,
|
|
2925
|
+
});
|
|
2926
|
+
if (!ownership.allowed) {
|
|
2927
|
+
return null;
|
|
2928
|
+
}
|
|
2929
|
+
const resolvedWorktree = await resolveJunctionWorktreeRootForCwd(options.archivedAgentCwd, {
|
|
2930
|
+
junctionHome: this.junctionHome,
|
|
2931
|
+
});
|
|
2932
|
+
if (!resolvedWorktree) {
|
|
2933
|
+
return null;
|
|
2934
|
+
}
|
|
2935
|
+
const records = await this.agentStorage.list();
|
|
2936
|
+
const recordsById = new Map(records.map((record) => [record.id, record]));
|
|
2937
|
+
const targetPath = resolvedWorktree.worktreePath;
|
|
2938
|
+
const hasRemainingNonArchivedRecord = records.some((record) => {
|
|
2939
|
+
if (record.id === options.archivedAgentId || record.archivedAt) {
|
|
2940
|
+
return false;
|
|
2723
2941
|
}
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
return
|
|
2732
|
-
});
|
|
2733
|
-
if (hasUnknownLiveAgent) {
|
|
2734
|
-
return;
|
|
2942
|
+
return this.isPathWithinRoot(targetPath, record.cwd);
|
|
2943
|
+
});
|
|
2944
|
+
if (hasRemainingNonArchivedRecord) {
|
|
2945
|
+
return null;
|
|
2946
|
+
}
|
|
2947
|
+
const hasUnknownLiveAgent = this.agentManager.listAgents().some((agent) => {
|
|
2948
|
+
if (agent.id === options.archivedAgentId) {
|
|
2949
|
+
return false;
|
|
2735
2950
|
}
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
this.sessionLogger.warn({ agentId: options.archivedAgentId, worktreePath: targetPath }, 'Unable to resolve repo root for auto-archive after agent archive');
|
|
2739
|
-
return;
|
|
2951
|
+
if (!this.isPathWithinRoot(targetPath, agent.cwd)) {
|
|
2952
|
+
return false;
|
|
2740
2953
|
}
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
});
|
|
2954
|
+
return !recordsById.has(agent.id);
|
|
2955
|
+
});
|
|
2956
|
+
if (hasUnknownLiveAgent) {
|
|
2957
|
+
return null;
|
|
2746
2958
|
}
|
|
2747
|
-
|
|
2748
|
-
|
|
2959
|
+
const repoRoot = ownership.repoRoot;
|
|
2960
|
+
if (!repoRoot) {
|
|
2961
|
+
this.sessionLogger.warn({ agentId: options.archivedAgentId, worktreePath: targetPath }, 'Unable to resolve repo root for auto-archive after agent archive');
|
|
2962
|
+
return null;
|
|
2749
2963
|
}
|
|
2964
|
+
return {
|
|
2965
|
+
targetPath,
|
|
2966
|
+
repoRoot,
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
async hasDirtyWorktreeState(cwd) {
|
|
2970
|
+
const { stdout } = await execAsync('git status --porcelain', {
|
|
2971
|
+
cwd,
|
|
2972
|
+
env: READ_ONLY_GIT_ENV,
|
|
2973
|
+
});
|
|
2974
|
+
return stdout.trim().length > 0;
|
|
2750
2975
|
}
|
|
2751
2976
|
async archiveJunctionWorktree(options) {
|
|
2752
2977
|
let targetPath = options.targetPath;
|
|
@@ -2757,16 +2982,32 @@ export class Session {
|
|
|
2757
2982
|
targetPath = resolvedWorktree.worktreePath;
|
|
2758
2983
|
}
|
|
2759
2984
|
const removedAgents = new Set();
|
|
2985
|
+
const preservedArchivedRecords = new Map();
|
|
2986
|
+
const cleanedUpAt = new Date().toISOString();
|
|
2760
2987
|
const agents = this.agentManager.listAgents();
|
|
2761
2988
|
for (const agent of agents) {
|
|
2762
2989
|
if (this.isPathWithinRoot(targetPath, agent.cwd)) {
|
|
2763
|
-
removedAgents.add(agent.id);
|
|
2764
2990
|
try {
|
|
2765
2991
|
await this.agentManager.closeAgent(agent.id);
|
|
2766
2992
|
}
|
|
2767
2993
|
catch {
|
|
2768
2994
|
// ignore cleanup errors
|
|
2769
2995
|
}
|
|
2996
|
+
const record = await this.agentStorage.get(agent.id);
|
|
2997
|
+
if (record?.archivedAt) {
|
|
2998
|
+
preservedArchivedRecords.set(agent.id, {
|
|
2999
|
+
...record,
|
|
3000
|
+
archivedWorktree: record.archivedWorktree
|
|
3001
|
+
? {
|
|
3002
|
+
...record.archivedWorktree,
|
|
3003
|
+
cleanupState: 'deleted',
|
|
3004
|
+
cleanedUpAt,
|
|
3005
|
+
}
|
|
3006
|
+
: record.archivedWorktree,
|
|
3007
|
+
});
|
|
3008
|
+
continue;
|
|
3009
|
+
}
|
|
3010
|
+
removedAgents.add(agent.id);
|
|
2770
3011
|
try {
|
|
2771
3012
|
await this.agentStorage.remove(agent.id);
|
|
2772
3013
|
}
|
|
@@ -2778,6 +3019,19 @@ export class Session {
|
|
|
2778
3019
|
const registryRecords = await this.agentStorage.list();
|
|
2779
3020
|
for (const record of registryRecords) {
|
|
2780
3021
|
if (this.isPathWithinRoot(targetPath, record.cwd)) {
|
|
3022
|
+
if (record.archivedAt) {
|
|
3023
|
+
preservedArchivedRecords.set(record.id, {
|
|
3024
|
+
...record,
|
|
3025
|
+
archivedWorktree: record.archivedWorktree
|
|
3026
|
+
? {
|
|
3027
|
+
...record.archivedWorktree,
|
|
3028
|
+
cleanupState: 'deleted',
|
|
3029
|
+
cleanedUpAt,
|
|
3030
|
+
}
|
|
3031
|
+
: record.archivedWorktree,
|
|
3032
|
+
});
|
|
3033
|
+
continue;
|
|
3034
|
+
}
|
|
2781
3035
|
removedAgents.add(record.id);
|
|
2782
3036
|
try {
|
|
2783
3037
|
await this.agentStorage.remove(record.id);
|
|
@@ -2793,6 +3047,9 @@ export class Session {
|
|
|
2793
3047
|
worktreePath: targetPath,
|
|
2794
3048
|
junctionHome: this.junctionHome,
|
|
2795
3049
|
});
|
|
3050
|
+
for (const record of preservedArchivedRecords.values()) {
|
|
3051
|
+
await this.agentStorage.upsert(record);
|
|
3052
|
+
}
|
|
2796
3053
|
for (const agentId of removedAgents) {
|
|
2797
3054
|
this.emit({
|
|
2798
3055
|
type: 'agent_deleted',
|
|
@@ -3496,14 +3753,38 @@ export class Session {
|
|
|
3496
3753
|
}
|
|
3497
3754
|
: undefined;
|
|
3498
3755
|
try {
|
|
3499
|
-
const
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3756
|
+
const liveAgent = this.agentManager.getAgent(msg.agentId);
|
|
3757
|
+
const storedRecord = liveAgent ? null : await this.agentStorage.get(msg.agentId);
|
|
3758
|
+
const fetchLimit = shouldLimitByProjectedWindow && typeof requestedLimit === 'number'
|
|
3759
|
+
? Math.max(1, Math.floor(requestedLimit))
|
|
3760
|
+
: limit;
|
|
3761
|
+
let provider;
|
|
3762
|
+
let timeline;
|
|
3763
|
+
if (liveAgent) {
|
|
3764
|
+
provider = liveAgent.provider;
|
|
3765
|
+
timeline = this.agentManager.fetchTimeline(msg.agentId, {
|
|
3766
|
+
direction,
|
|
3767
|
+
cursor,
|
|
3768
|
+
limit: fetchLimit,
|
|
3769
|
+
});
|
|
3770
|
+
}
|
|
3771
|
+
else if (storedRecord?.archivedAt) {
|
|
3772
|
+
provider = coerceAgentProvider(this.sessionLogger, storedRecord.provider, storedRecord.id);
|
|
3773
|
+
timeline = this.fetchStoredTimeline(storedRecord, {
|
|
3774
|
+
direction,
|
|
3775
|
+
cursor,
|
|
3776
|
+
limit: fetchLimit,
|
|
3777
|
+
});
|
|
3778
|
+
}
|
|
3779
|
+
else {
|
|
3780
|
+
const snapshot = await this.ensureAgentLoaded(msg.agentId);
|
|
3781
|
+
provider = snapshot.provider;
|
|
3782
|
+
timeline = this.agentManager.fetchTimeline(msg.agentId, {
|
|
3783
|
+
direction,
|
|
3784
|
+
cursor,
|
|
3785
|
+
limit: fetchLimit,
|
|
3786
|
+
});
|
|
3787
|
+
}
|
|
3507
3788
|
let hasOlder = timeline.hasOlder;
|
|
3508
3789
|
let hasNewer = timeline.hasNewer;
|
|
3509
3790
|
let startCursor = null;
|
|
@@ -3511,10 +3792,10 @@ export class Session {
|
|
|
3511
3792
|
let entries;
|
|
3512
3793
|
if (shouldLimitByProjectedWindow) {
|
|
3513
3794
|
const projectedLimit = Math.max(1, Math.floor(requestedLimit));
|
|
3514
|
-
let
|
|
3795
|
+
let projectedFetchLimit = projectedLimit;
|
|
3515
3796
|
let projectedWindow = selectTimelineWindowByProjectedLimit({
|
|
3516
3797
|
rows: timeline.rows,
|
|
3517
|
-
provider
|
|
3798
|
+
provider,
|
|
3518
3799
|
direction,
|
|
3519
3800
|
limit: projectedLimit,
|
|
3520
3801
|
collapseToolLifecycle: false,
|
|
@@ -3531,26 +3812,33 @@ export class Session {
|
|
|
3531
3812
|
break;
|
|
3532
3813
|
}
|
|
3533
3814
|
const maxRows = Math.max(0, timeline.window.maxSeq - timeline.window.minSeq + 1);
|
|
3534
|
-
const nextFetchLimit = Math.min(maxRows,
|
|
3535
|
-
if (nextFetchLimit <=
|
|
3815
|
+
const nextFetchLimit = Math.min(maxRows, projectedFetchLimit * 2);
|
|
3816
|
+
if (nextFetchLimit <= projectedFetchLimit) {
|
|
3536
3817
|
break;
|
|
3537
3818
|
}
|
|
3538
|
-
|
|
3539
|
-
timeline =
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3819
|
+
projectedFetchLimit = nextFetchLimit;
|
|
3820
|
+
timeline =
|
|
3821
|
+
storedRecord?.archivedAt && !liveAgent
|
|
3822
|
+
? this.fetchStoredTimeline(storedRecord, {
|
|
3823
|
+
direction,
|
|
3824
|
+
cursor,
|
|
3825
|
+
limit: projectedFetchLimit,
|
|
3826
|
+
})
|
|
3827
|
+
: this.agentManager.fetchTimeline(msg.agentId, {
|
|
3828
|
+
direction,
|
|
3829
|
+
cursor,
|
|
3830
|
+
limit: projectedFetchLimit,
|
|
3831
|
+
});
|
|
3544
3832
|
projectedWindow = selectTimelineWindowByProjectedLimit({
|
|
3545
3833
|
rows: timeline.rows,
|
|
3546
|
-
provider
|
|
3834
|
+
provider,
|
|
3547
3835
|
direction,
|
|
3548
3836
|
limit: projectedLimit,
|
|
3549
3837
|
collapseToolLifecycle: false,
|
|
3550
3838
|
});
|
|
3551
3839
|
}
|
|
3552
3840
|
const selectedRows = projectedWindow.selectedRows;
|
|
3553
|
-
entries = projectTimelineRows(selectedRows,
|
|
3841
|
+
entries = projectTimelineRows(selectedRows, provider, projection);
|
|
3554
3842
|
if (projectedWindow.minSeq !== null && projectedWindow.maxSeq !== null) {
|
|
3555
3843
|
startCursor = { epoch: timeline.epoch, seq: projectedWindow.minSeq };
|
|
3556
3844
|
endCursor = { epoch: timeline.epoch, seq: projectedWindow.maxSeq };
|
|
@@ -3563,7 +3851,7 @@ export class Session {
|
|
|
3563
3851
|
const lastRow = timeline.rows[timeline.rows.length - 1];
|
|
3564
3852
|
startCursor = firstRow ? { epoch: timeline.epoch, seq: firstRow.seq } : null;
|
|
3565
3853
|
endCursor = lastRow ? { epoch: timeline.epoch, seq: lastRow.seq } : null;
|
|
3566
|
-
entries = projectTimelineRows(timeline.rows,
|
|
3854
|
+
entries = projectTimelineRows(timeline.rows, provider, projection);
|
|
3567
3855
|
}
|
|
3568
3856
|
this.emit({
|
|
3569
3857
|
type: 'fetch_agent_timeline_response',
|